개발/Swift

Swift에서 contains(where:) vs filter().count > 0

까칠코더 2025. 11. 10. 16:46
반응형

Swift에서 contains(where:) vs filter().count > 0

 

Swift에서 컬렉션에 조건을 만족하는 요소가 하나라도 존재하는지를 확인할 때는 보통 두 가지 표현이 등장합니다.

  • 권장: contains(where:)
  • 비권장: filter { ... }.count > 0

이 문서는 두 방식의 의미, 성능, 메모리 사용, 엣지 케이스, 대안 API까지 실무 관점으로 정리하고 예제를 제공합니다.

 

1. 핵심

  • 존재 여부만 확인할 때는 항상 contains(where:) 를 사용하세요.
  • filter().count > 0는 전체 순회를 강제하고 임시 배열을 만들어 메모리를 낭비합니다.
  • 읽기에도 contains가 더 자연스럽고 의도를 직접적으로 드러냅니다.

 

2. 의미 차이

목적 권장 표현 비권장 표현
조건을 만족하는 요소가 하나라도 있는지 array.contains(where: predicate) array.filter(predicate).count > 0
모든 매칭 요소를 수집하고 싶음 array.filter(predicate) array.filter(predicate)

즉, 존재 확인 수집은 다른 문제입니다. 존재만 확인하는데 filter로 수집부터 하면 낭비가 발생합니다.

 

3. 성능과 메모리 복잡도

항목 contains(where:) filter().count > 0
순회 복잡도 평균 O(k) (조기 종료; k ≤ n) O(n) (항상 끝까지)
메모리 O(1) O(m) (매칭된 m개 요소 저장)
의도 표현 존재 확인에 직관적 존재 확인에 우회적

contains(where:)는 첫 true에서 즉시 종료(early exit) 하므로 평균적으로 더 빠르며, 추가 메모리를 전혀 사용하지 않습니다.

 

4. 예제


존재 여부만 확인

struct User { let id: Int; let age: Int }

let users = [
    User(id: 1, age: 21),
    User(id: 2, age: 35),
    User(id: 3, age: 28)
]

// 권장
if users.contains(where: { $0.age >= 30 }) {
    print("30세 이상 사용자 존재")
}

// 비권장
if users.filter({ $0.age >= 30 }).count > 0 {
    print("30세 이상 사용자 존재")
}

일부만 검사해도 되는 상황 (조기 종료 효과)

let big = (0...1_000_000)

// contains(where:)는 조건 만족 시 즉시 종료
let hasSquare = big.contains(where: { n in
    let r = Int.sqrtApprox(n) // 가정: 빠른 정수 제곱근 추정
    return r * r == n
})

// filter().count > 0 는 모든 요소를 끝까지 순회 후 임시 배열을 만든다
let hasSquare2 = big.filter { n in
    let r = Int.sqrtApprox(n)
    return r * r == n
}.count > 0

모든 매칭 요소가 필요한 경우에는 filter 사용

let over30 = users.filter { $0.age >= 30 }     // ✅ 매칭된 User 배열 전부 필요
let hasOver30 = !over30.isEmpty                // 필요하다면 여기서 존재 여부 체크

 

5. 대안/변형 API

  • 첫 매칭 요소의 이 필요하면 first(where:)
    존재 확인 + 첫 결과 활용이 동시에 가능합니다.
if let found = users.first(where: { $0.age >= 30 }) {
    print("첫 매칭 사용자 id:", found.id)
}
  • 키 경합이 빈번하면 Set/Dictionary 구조로 설계하여 contains/keys.contains를 O(1)에 가깝게 만들 수 있습니다.
let allowed: Set<String> = ["admin", "editor", "guest"]
if allowed.contains("editor") { /* ... */ }   // 평균 O(1)

 

6. 가독성/리뷰 관점

  • contains(where:)는 “조건을 만족하는 게 있느냐”를 문장 그대로 표현합니다.
  • 반면 filter().count > 0는 “일단 걸러서 모은 뒤 크기가 0보다 크냐”로 우회 표현입니다.
  • 코드 리뷰 시 contains는 의도를 빠르게 파악할 수 있습니다.

 

7. 흔한 실수와 주의사항

  1. 부수 효과(side-effect) 가 있는 클로저를 contains에 넣지 마세요. 존재 확인은 순수 검사여야 합니다.
  2. 컬렉션이 아닌 시퀀스(재사용 불가) 로부터 filter를 여러 번 호출하면 비용이 반복됩니다. 필요 시 lazy 를 고려하세요.
let lazyResult = numbers.lazy.filter { $0 % 2 == 0 }
let firstEvenExists = lazyResult.first != nil // 조기 종료 O(k)
  1. filter().first 대신 first(where:)를 쓰세요. 불필요한 배열 생성을 피합니다.
// 비권장
if let v = array.filter({ $0 > 10 }).first { ... }

// 권장
if let v = array.first(where: { $0 > 10 }) { ... }

 

8. 간단 벤치마크 스케치 (개념 설명용)

import Foundation

let values = Array(0...5_000_000)
let needle = 4_999_999

// 1) contains(where:)
let t1 = CFAbsoluteTimeGetCurrent()
let r1 = values.contains(where: { $0 == needle })
let t2 = CFAbsoluteTimeGetCurrent()

// 2) filter().count > 0
let t3 = CFAbsoluteTimeGetCurrent()
let r2 = values.filter({ $0 == needle }).count > 0
let t4 = CFAbsoluteTimeGetCurrent()

print("contains:", r1, "time:", t2 - t1)      // 더 빠르고 메모리 덜 사용
print("filter+count>0:", r2, "time:", t4 - t3)

실제 숫자는 환경(릴리즈 빌드/최적화/플랫폼)에 따라 다르지만,

원리상 contains(where:)가 평균적으로 더 빠르고 메모리 효율적입니다.

 

9. 참조 링크

 

10. 결론 

  • “존재 확인” 목적에서는 contains(where:)  성능·메모리·가독성 모두에서 우위입니다.
  • “매칭 결과 수집”이 필요할 때만 filter를 사용하고, 결과의 존재 여부는 isEmpty로 확인하세요.
  • 첫 매칭 요소가 필요하면 first(where:)로 직접 접근하세요.
반응형