개발/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. 흔한 실수와 주의사항
- 부수 효과(side-effect) 가 있는 클로저를 contains에 넣지 마세요. 존재 확인은 순수 검사여야 합니다.
- 컬렉션이 아닌 시퀀스(재사용 불가) 로부터 filter를 여러 번 호출하면 비용이 반복됩니다. 필요 시 lazy 를 고려하세요.
let lazyResult = numbers.lazy.filter { $0 % 2 == 0 }
let firstEvenExists = lazyResult.first != nil // 조기 종료 O(k)
- 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. 참조 링크
- contains(where:) (Sequence)
https://developer.apple.com/documentation/swift/sequence/contains(where:) - filter(:) (Sequence)
https://developer.apple.com/documentation/swift/sequence/filter(:) - first(where:) (Sequence)
https://developer.apple.com/documentation/swift/sequence/first(where:) - Set.contains(:)
https://developer.apple.com/documentation/swift/set/contains(:)
10. 결론
- “존재 확인” 목적에서는 contains(where:) 가 성능·메모리·가독성 모두에서 우위입니다.
- “매칭 결과 수집”이 필요할 때만 filter를 사용하고, 결과의 존재 여부는 isEmpty로 확인하세요.
- 첫 매칭 요소가 필요하면 first(where:)로 직접 접근하세요.
반응형