개발/Swift

Swift에서 Set를 사용해서 포함(contains) 검사

까칠코더 2025. 11. 11. 12:23
반응형

Swift에서 Set를 사용해서 포함(contains) 검사

 

의도·성능·실무 예제를 중심으로 왜 Array 대신 Set를 써야 하는지, 그리고 어떻게 안전하게 쓰는지를 정리했습니다.

 

1. 핵심 요약

항목 Set Array Dictionary
포함 검사 contains(_:) 평균 O(1) (해시) O(n) 키 존재 확인 O(1)
중복 허용 불가(자동 제거) 가능 키는 불가, 값은 가능
순서 보존 보장 안 함 보통 삽입 순서 키 순서 보장 안 함
대표 용도 화이트/블랙리스트, 빠른 조회, 중복 제거 순서가 중요한 목록, UI 표시 키 ↔ 값 매핑

요점: “포함되어 있나?”만 빠르게 확인할 땐 Set이 최적입니다.

 

2. 언제 Set를 써야 하나?

  • 화이트리스트/블랙리스트로 허용/차단 여부를 빠르게 판단
  • 대량 데이터 중복 제거 (Unique 보장)
  • 교집합/차집합 등 집합 연산이 핵심일 때
  • 키-값 매핑이 필요 없다면 Dictionary보다 가볍고 간결

 

3. 기본 문법

// 생성
let allowed: Set<String> = ["admin", "editor", "guest"]
var visited = Set<Int>()

// 기본 연산
allowed.contains("editor")  // true  (평균 O(1))
visited.insert(42)          // (inserted: Bool, memberAfterInsert: Int)
visited.remove(42)          // Optional(42) 또는 nil
visited.update(with: 100)   // 존재하면 교체, 없으면 삽입
visited.isEmpty
visited.count

순서가 필요하면?

  • Array(set)로 배열로 변환하거나, 정렬이 필요하면 Array(set).sorted()를 사용하세요.
  • 삽입 순서를 보존해야 한다면 Swift Collections 패키지의 OrderedSet을 고려하세요.

 

4. 실무 예제


4.1 화이트리스트/블랙리스트

let whitelist: Set<String> = ["KR", "US", "JP"]
let blacklist: Set<String> = ["RU", "IR"]

func canAccess(country: String) -> Bool {
    if blacklist.contains(country) { return false }
    return whitelist.contains(country)
}

4.2 중복 제거(데이터 클린업)

let raw = ["a", "b", "a", "c", "b", "d"]
let unique = Array(Set(raw))         // 순서 보존 X
let uniqueSorted = Array(Set(raw)).sorted()

4.3 빠른 교집합/차집합

let a: Set<Int> = [1,2,3,4,5]
let b: Set<Int> = [4,5,6,7]

let inter = a.intersection(b)          // {4,5}
let uni = a.union(b)                   // {1,2,3,4,5,6,7}
let diff = a.subtracting(b)            // {1,2,3}
let sym = a.symmetricDifference(b)     // {1,2,3,6,7}

4.4 대량 포함 검사 최적화 (Array → Set로 승격)

let emails = loadEmails()                 // [String] (대량)
let banned = Set(loadBannedEmails())      // Set으로 보관

let filtered = emails.filter { !banned.contains($0) } // 매우 빠름

4.5 앱 권한/플래그 토글

var enabledFlags: Set<String> = ["analytics", "push"]

func toggle(_ flag: String) {
    if !enabledFlags.insert(flag).inserted {
        enabledFlags.remove(flag)
    }
}

4.6 교차 여부만 빠르게(조기 종료)

let required: Set<String> = ["camera", "microphone"]
let granted: Set<String> = ["camera"]

let missing = required.subtracting(granted) // {"microphone"}
if missing.isEmpty { /* 모두 허용됨 */ }

 

5. 커스텀 타입에서 Hashable 구현

Set에 담을 타입은 Hashable이어야 합니다. (Equatable도 자동 요구됨)

struct User: Hashable {
    let id: Int
    var name: String

    // Swift 4.1+에서는 멤버별 Hashable 자동 합성 (아래 수동 구현 불필요)
    // static func == (lhs: User, rhs: User) -> Bool { lhs.id == rhs.id }
    // func hash(into hasher: inout Hasher) { hasher.combine(id) }
}

let u1 = User(id: 1, name: "A")
let u2 = User(id: 1, name: "B")   // id만 같아도 동일 원소로 간주됨(자동 합성 규칙에 따름)
let s: Set<User> = [u1, u2]       // {User(id:1,name:"A")} 1개

동등성 기준을 id만으로 하고 싶다면 Equatable/Hashable를 명시적으로 구현하여 의도를 분명히 하세요.

 

6. 성능 메모

  • contains, insert, remove, update는 평균 O(1), 최악 O(n) (충돌 상황).
  • 큰 데이터 병합은 formUnion(_:), formIntersection(_:), subtract(_:) 같은 in-place 연산이 더 빠릅니다.swift var s1: Set<Int> = [1,2,3] let s2: Set<Int> = [3,4,5] s1.formUnion(s2) // s1 = {1,2,3,4,5}

 

7. Set vs Array vs Dictionary 선택 가이드

상황 추천 이유
단순 멤버십(포함/제외) Set 평균 O(1) 조회
키‑값 매핑 필요 Dictionary 값 접근 O(1)
순서가 중요/중복 허용 Array UI 표시, 정렬/인덱스 접근
순서 + 빠른 포함 검사 OrderedSet (Swift Collections) 순서 유지 + 해시

 

8. 고급 주의사항

  1. 순서가 필요하면 Array로 변환·정렬: Set 자체는 순서가 없습니다.
  2. Hashable 안정성: 해시와 동등성 기준은 논리적으로 불변이어야 합니다. (변경 가능한 속성으로 해시를 만들면 Set 무결성 문제)
  3. 브리징(NSSet): Obj‑C와 상호 운용 시 Set  NSSet 브리징이 일어납니다. 타입 캐스팅/브리징 비용에 유의하세요.
  4. 메모리: 중복 없는 큰 데이터는 Set가 더 작을 수 있으나, 해시 테이블 오버헤드가 있어 작은 데이터에서는 차이가 미미할 수 있습니다.

 

9. 간단 벤치마크 스케치

import Foundation

let universe = (0..<2_000_00).map { "u\($0)" }
let needle = Set((0..<50_000).map { "u\($0 * 3)" }) // 1/3 주기로 포함
let arrayNeedle = Array(needle)

func measure(_ name: String, _ block: () -> Void) {
    let t1 = CFAbsoluteTimeGetCurrent(); block(); let t2 = CFAbsoluteTimeGetCurrent()
    print(name, ":", t2 - t1, "sec")
}

// Set 멤버십
measure("Set.contains") {
    _ = universe.filter { needle.contains($0) }
}

// Array 멤버십
measure("Array.contains") {
    _ = universe.filter { arrayNeedle.contains($0) } // 선형 탐색
}

일반적으로 Set.contains가 큰 차이로 빠릅니다. (환경/최적화에 따라 수치는 달라집니다)

 

10. 참조 링크

 

11. 결론

  • 포함 검사 목적이라면 Array 대신 Set이 정답입니다.
  • 순서가 필요 없고, 중복을 막고 싶으며, 큰 데이터에서 빠른 조회가 중요하다면 Set이 가장 단순하고 확실한 선택입니다.
  • 교집합/차집합/대칭차 등 집합 연산은 읽기 좋고 빠르게 문제를 풀어 줍니다.
반응형