객체가 딕셔너리 키인 동안 동등성과 해싱에 사용되는 속성을 안정적으로 유지하세요. Mac에서 로컬 Swift 계산을 할 때는 조회가 어떤 의미를 가져야 하는지에 따라 객체 식별성, 변경 불가능한 ID 또는 값 스냅샷을 선택하세요.

이 안내에서는 합성 사각형과 메모리 안에서만 수행되는 간단한 면적 계산을 사용합니다. 실제 목표는 사각형의 레이블이나 치수가 바뀌어도 연결된 딕셔너리 항목을 잃지 않으면서, 조회가 성공했다는 사실을 최신 계산 결과와 혼동하지 않는 것입니다.

객체를 변경하면 딕셔너리 조회가 왜 깨질 수 있나요?

Swift의 Dictionary는 키가 Hashable을 준수하는 해시 테이블입니다. 키 기반 첨자는 요청한 키가 없을 수도 있으므로 옵셔널을 반환합니다. 해싱은 항목을 찾는 데 도움을 주고, 동등성은 어떤 키들이 동등한 것으로 취급되는지 결정합니다. Apple의 Dictionary 문서를 참고하세요.

Hashable 계약은 이 두 연산을 연결합니다. 서로 동등한 인스턴스는 Hasher에 동일한 핵심 구성 요소를 같은 순서로 전달해야 합니다. 사용자 지정 키를 구현할 때는 동등성을 정의하는 속성을 hash(into:)에서 사용하세요. Apple은 Hashable에서 이 요구 사항을 설명합니다.

참조 타입 키에는 고려할 사항이 하나 더 있습니다. 클래스 인스턴스를 할당하면 별도의 객체가 만들어지는 것이 아니라 기존 객체에 대한 참조가 전달됩니다. 참조를 let로 선언해도 변경 가능한 속성이 고정되지는 않습니다. 이는 구조체와 클래스에 문서화된 언어 규칙입니다.

편집 가능한 레이블을 비교하고 해싱하는 사각형 클래스를 생각해 보세요. 레이블이 \"Tile A\"일 때 객체를 삽입한 다음 레이블을 \"Tile B\"로 바꿉니다. 이제 저장된 참조는 키를 정의하는 상태가 달라진 객체를 나타냅니다. 딕셔너리에서의 원래 위치와 이후 조회 사이의 일관성이 더는 유지되지 않습니다.

해시 테이블과 동등성 계약에서 설계 규칙을 도출할 수 있습니다. 키가 딕셔너리에 있는 동안 동등성을 정의하는 상태를 안정적으로 유지하세요. Apple은 Foundation 객체에 적용되는 관련 요구 사항을 명시합니다. 객체의 해시값은 해당 해시를 배치에 사용하는 컬렉션에 속해 있는 동안 바뀌면 안 됩니다. NSObjectProtocol.hash를 참고하세요.

유효하지 않은 변경을 한 직후 모든 경우에 nil이 반환된다고 가정하는 테스트를 만들지 마세요. 키 계약이 깨진 뒤에는 조회가 우연히 성공해도 설계가 올바르다는 증거가 되지 않습니다. 대신 의도한 동등성 규칙과 안정적인 동작을 테스트하세요.

작업에 맞는 키는 무엇인가요?

준수를 구현하기 전에 딕셔너리 항목이 어떤 의미인지 정하세요. 사각형 면적 도구에서는 다음 세 질문에 따라 서로 다른 설계가 나옵니다.

항목은 무엇을 나타내야 하나요? 적합한 키 사각형이 바뀌면 어떻게 되나요?
바로 이 사각형 객체 식별성 기반 클래스 키 항목은 해당 인스턴스에 계속 연결됩니다
현재 객체와 별개로 식별되는 논리적 사각형 변경 불가능한 ID 값 항목은 ID에 계속 연결됩니다
특정 치수에 대한 계산 결과 변경 불가능한 치수 스냅샷 치수가 바뀌면 다른 조회 키가 사용됩니다

이 표는 Swift의 식별성, 값, 해싱 규칙에서 도출한 설계 참고 자료입니다. 또한 두 가지 의무를 구분합니다. 연결 항목에 계속 접근할 수 있어야 한다는 점과 계산 결과가 최신 상태여야 한다는 점입니다. 안정적인 식별성 키는 첫 번째를 해결하고, 재계산이나 완전한 입력 스냅샷은 두 번째를 해결합니다.

항목이 특정 인스턴스에 속할 때는 객체 식별성을 사용하세요

편집을 거쳐도 특정 객체 하나를 따라가야 하는 메모리 내 연결이라면 ===으로 동등성을 정의하고 ObjectIdentifier를 해싱하세요. Swift의 식별성 연산자는 참조가 같은 인스턴스를 가리키는지 확인합니다. Apple은 ObjectIdentifier를 인스턴스의 수명 동안 비교할 수 있는 해시 가능한 식별자로 문서화합니다. 구조체와 클래스 및 ObjectIdentifier를 참고하세요.

다음은 완전한 동기식 프로그램입니다. Swift 명령줄 도구를 사용할 수 있는 Mac이 필요하며, 자동으로 가져오는 Swift 표준 라이브러리만 사용합니다. IdentityKeys.swift로 저장한 뒤 터미널에서 swift IdentityKeys.swift를 실행하세요. 예제는 검토했지만 조사 환경에서 Swift를 사용할 수 없어 실행하지 못했습니다.

final class Rectangle: Hashable {
    var label: String
    var width: Int
    var height: Int

    init(label: String, width: Int, height: Int) {
        self.label = label
        self.width = width
        self.height = height
    }

    static func == (lhs: Rectangle, rhs: Rectangle) -> Bool {
        lhs === rhs
    }

    func hash(into hasher: inout Hasher) {
        hasher.combine(ObjectIdentifier(self))
    }
}

func demonstrateIdentityKeys() {
    let rectangle = Rectangle(label: "Tile A", width: 4, height: 3)
    let alias = rectangle
    let separate = Rectangle(label: "Tile A", width: 4, height: 3)

    var areas: [Rectangle: Int] = [
        rectangle: rectangle.width * rectangle.height
    ]

    rectangle.label = "Tile B"
    assert(areas[alias] == 12)
    assert(areas[separate] == nil)

    rectangle.width = 5
    assert(areas[rectangle] == 12) // Still the previous calculation.

    areas[rectangle] = rectangle.width * rectangle.height
    assert(areas[rectangle] == 15)
    assert(areas.count == 1)

    print("Renamed lookup: 12")
    print("Recalculated area: \(areas[rectangle]!)")
}

demonstrateIdentityKeys()

예상 출력은 Renamed lookup: 12 다음에 Recalculated area: 15입니다. 단언문은 유용한 계약을 표현합니다. 별칭으로 기존 항목을 찾을 수 있고, 별도로 만든 사각형은 찾지 못하며, 원래 객체를 편집해도 새 키가 생기지 않습니다.

폭을 바꾸어도 프로그램이 15을 할당하기 전까지 저장된 면적은 의도적으로 12인 상태로 남습니다. 식별성은 안정적이지만 딕셔너리는 산술적 종속성을 감지하거나 값을 다시 계산하지 않습니다. 편집 직후 애플리케이션이 값을 갱신할지, 다음 계산 때까지 값을 제거할지 정하세요.

식별성 동등성은 타입 전체에 적용되는 결정입니다. 이제 이 클래스의 모든 == 비교는 “같은 인스턴스”를 뜻합니다. 모델이 이미 치수나 다른 값으로 동등성을 정의한다면, 딕셔너리 하나를 위해 그 의미를 바꾸지 말고 별도의 키 설계를 사용하세요.

딕셔너리 키로 ObjectIdentifier를 직접 사용할 수도 있지만, 이 식별자만으로는 이후 작업에 필요한 사각형을 얻을 수 없습니다. 식별자를 비교에 사용하는 동안 관련 객체를 계속 살아 있게 유지하고, 더 이상 필요하지 않은 항목은 제거하세요. 문서화된 수명 제한에 따라 이 방식은 범위가 한정된 메모리 내 식별 메커니즘입니다. ObjectIdentifier를 참고하세요.

항목이 계산 결과를 나타낼 때는 값 스냅샷을 사용하세요

재사용 가능한 면적 결과에는 치수 자체가 적절한 키인 경우가 많습니다. 4에 3를 곱한 크기의 사각형 두 개는 레이블이 다르고 서로 다른 객체여도 같은 결과를 공유할 수 있습니다. 치수가 바뀌면 다른 항목을 선택해야 합니다.

Swift 구조체는 값 의미론을 가지며, 저장된 속성이 해시 가능하면 컴파일러가 Hashable을 합성할 수 있습니다. 따라서 변경 불가능한 정수를 담는 스냅샷은 이 계산에 간단한 키가 됩니다. 구조체와 클래스 및 Hashable을 참고하세요.

이 완전한 대안 예제를 별도로 swift SnapshotKeys.swift로 실행하세요. 합성 입력은 양수인 작은 정수이므로 범용 숫자 입력 인터페이스가 필요하지 않습니다.

final class EditableRectangle {
    var label: String
    var width: Int
    var height: Int

    init(label: String, width: Int, height: Int) {
        self.label = label
        self.width = width
        self.height = height
    }
}

struct AreaInputs: Hashable, Sendable {
    let width: Int
    let height: Int
}

func demonstrateSnapshotKeys() {
    let rectangle = EditableRectangle(
        label: "Tile A", width: 4, height: 3
    )
    var areas: [AreaInputs: Int] = [:]

    let original = AreaInputs(
        width: rectangle.width, height: rectangle.height
    )
    areas[original] = original.width * original.height

    rectangle.label = "Tile B"
    rectangle.width = 5

    let current = AreaInputs(
        width: rectangle.width, height: rectangle.height
    )
    assert(areas[original] == 12)
    assert(areas[current] == nil)

    areas[current] = current.width * current.height
    assert(areas[current] == 15)
    assert(areas.count == 2)

    print("Original area: \(areas[original]!)")
    print("Current area: \(areas[current]!)")
}

demonstrateSnapshotKeys()

예상 출력은 Original area: 12 다음에 Current area: 15입니다. 프로그램이 서로 다른 스냅샷 두 개를 삽입했기 때문에 두 결과 모두 계속 조회할 수 있습니다. 이전 결과는 이전 치수를 올바르게 나타냅니다.

계산에 영향을 주는 모든 입력을 스냅샷에 포함하세요. 레이블은 사각형 면적과 무관하므로 여기서는 제외했습니다. 나중에 다른 설정이 계산에 영향을 준다면 그 설정을 키에 추가하세요. 그렇지 않으면 서로 다른 계산이 항목을 잘못 공유할 수 있습니다. 이는 키가 Hashable을 준수하는지와는 별개인 계산 설계 요구 사항입니다.

변경 가능한 클래스 참조만 감싸는 구조체가 해당 객체의 고정된 스냅샷을 자동으로 만들지는 않습니다. 이 예제처럼 관련 값을 스냅샷에 복사하세요. 또한 편집이 반복되어 더는 필요하지 않은 결과가 쌓이는 경우 오래된 스냅샷을 언제 버릴지도 정하세요.

변경 불가능한 ID는 언제 사용해야 하나요?

여러 객체 인스턴스가 같은 논리적 항목을 가리켜야 할 때는 ID를 사용하세요. 이 합성 도구에서는 변경 불가능한 정수 ID로 충분할 수 있습니다. [Int: Int]에 결과를 저장하고, 사각형의 let id: Int로 접근하며, 편집 가능한 레이블과 치수는 키 밖에 둡니다.

이 설계에서는 ID가 7인 두 객체가 의도적으로 같은 딕셔너리 항목을 가리킵니다. 서로 다른 논리적 사각형에 같은 ID가 잘못 부여되지 않도록 생성 규칙을 마련해야 합니다. Dictionary 문서에 설명되어 있듯이 기존 키를 갱신하면 연결된 값이 대체됩니다.

ID 기반 면적 테이블에서도 치수가 바뀐 뒤 적용할 갱신 정책이 필요합니다. 서로 다른 입력 조합마다 결과를 보관하려면 스냅샷을 사용하세요. 각 논리적 사각형의 최신 결과만 보관하려면 ID와 명시적 재계산을 조합하는 편이 낫습니다.

Foundation 딕셔너리나 액터가 변경 가능한 키 문제를 해결하나요?

Foundation의 삽입 계약은 다릅니다. NSMutableDictionary.setObject(_:forKey:)는 NSCopying 키를 요구하며 삽입 시 이를 복사합니다. 복사 구현에서 안정적인 키 상태를 제공하면 키 복사본을 통해 스냅샷을 만들 수 있습니다. 이후 조회는 저장된 복사본과 일치해야 합니다. Apple의 setObject(_:forKey:)를 참고하세요.

이 동작을 근거로 Swift의 네이티브 딕셔너리가 키로 사용된 클래스 인스턴스를 독립적으로 복사한다고 추론하지 마세요. Swift 클래스 할당은 참조 의미론을 따릅니다. 사용자 지정 Foundation 키에서는 복사, 동등성, 해싱 동작을 함께 확인하세요. Foundation의 해시 요구 사항도 여전히 적용됩니다.

동시성은 별개의 문제입니다. 액터는 격리된 변경 가능 상태에 대한 접근을 조정하고, Sendable은 요구 사항에 따라 동시성 영역을 넘나들 수 있는 값을 설명합니다. 이 둘 모두 키의 동등성 의미를 바꾸거나 삽입 후 정의 상태가 변한 키를 바로잡지는 않습니다. Swift의 동시성 장 및 Apple의 Sendable 문서를 참고하세요.

스냅샷 예제는 저장된 정수가 프로토콜의 값 타입 요구 사항을 충족하므로 Sendable을 명시적으로 선언합니다. 나중에 면적 계산을 액터로 옮긴다면 완전한 스냅샷을 보내고 액터가 결과 딕셔너리를 소유하게 하세요. 변경 가능한 사각형은 기존 격리 영역에 둘 수 있습니다. 이 동기식 예제에는 액터가 필요하지 않습니다.

조회에 의존하기 전에 계약을 확인하세요

선택한 설계를 검증하려면 작고 결정적인 순서를 사용하세요.

  1. 항목을 삽입하고 초기 조회를 확인합니다.
  2. 동등성 및 해싱에서 제외된 속성을 바꾼 뒤 같은 항목에 계속 접근할 수 있는지 확인합니다.
  3. 별칭과 별도로 만든 객체 또는 스냅샷을 비교해 동등성의 의도한 의미와 결과가 일치하는지 확인합니다.
  4. 계산 입력을 바꾸고 결과가 갱신되거나 제거되는지, 또는 새 스냅샷을 통해 접근되는지 확인합니다.
  5. 딕셔너리 개수를 확인해 대체와 삽입이 의도대로 동작하는지 확인합니다.

기존 구현에서 키를 정의하는 상태를 변경한다면 추가 조회에 의존하기 전에 키 모델을 수정하세요. 이처럼 간단한 로컬 계산에서는 이후 딕셔너리 연산을 복구 수단으로 취급하기보다 별도의 합성 입력 목록에서 결과를 다시 만드는 편이 더 명확합니다. 찾을 수 없는 항목을 조용히 버리는 대신 재계산할 수 있도록 원본 입력을 충분히 보존하세요.

살아 있는 객체 하나와 연결할 때는 식별성을, 논리적 항목 하나에는 변경 불가능한 ID를, 계산 하나에는 값 스냅샷을 선택하세요. 그리고 결과 갱신 정책을 명확히 정하세요. 이 결정을 함께 내리면 편집 가능한 사각형이 바뀌어도 조회의 의미를 유지할 수 있습니다.

출처