생성된 Codable 구현은 컴파일 타임에 이용 가능한 정적 유형 정보에만 의존합니다. 기본 클래스 참조를 통해 클래스 인스턴스의 이질적인 컬렉션을 인코딩할 때, 컴파일러는 기본 클래스 유형에서 볼 수 있는 속성만 직렬화하는 encode(to:) 코드를 생성합니다. 결과적으로 서브클래스 전용 속성은 JSON 출력에서 생략되고, 디코딩 중에 런타임은 올바른 서브클래스를 인스턴스화하는 데 필요한 메타데이터가 부족해져 기본 클래스로 기본값을 설정하고 유형별 데이터를 잃게 됩니다.
우리는 포트폴리오 관리를 위해 다양한 거래 유형을 처리하는 금융 분석 대시보드를 구축하고 있었습니다. 도메인 모델은 Transaction이 기본 클래스인 클래스 계층 구조를 사용하며, StockTrade, DividendPayment, FeeCharge와 같은 서브클래스가 tickerSymbol 또는 dividendRate와 같은 특정 속성을 추가합니다. 백엔드 API는 이러한 거래의 혼합된 JSON 배열을 반환하며, 각 거래에는 transactionType 판별자 필드가 포함되어 있습니다.
우리는 처음에 스위프트의 자동 Codable 합성을 사용하여 다형성 배열 **[Transaction]**을 처리할 것이라고 가정했습니다. 그러나 통합 테스트 중에 [StockTrade] 배열을 **[Transaction]**으로 캐스트하면 id 및 amount와 같은 기본 클래스 필드만 포함된 JSON이 생성되었고, tickerSymbol은 완전히 생략되었습니다. 반대로, 이 JSON을 디코딩하면 기본 Transaction 인스턴스만 재생성되어 서브클래스 전용 속성에 접근하려고 할 때 앱이 중단되었습니다.
이 제한 사항을 해결하기 위해 세 가지 접근 방식을 고려했습니다. 첫 번째는 수동 Codable 구현으로, 인코딩 컨테이너에 transactionType 필드를 명시적으로 추가하고, 이 판별자에 대해 스위칭하여 올바른 서브클래스를 인스턴스화하는 사용자 정의 **init(from:)**을 구현하는 것이었습니다. 이 접근 방식은 완전한 유형 안전성을 제공하고 기존 객체 그래프를 유지하지만, 모든 새로운 거래 유형에 대해 상당한 보일러플레이트 코드를 작성하고 유지해야 하며, 기능을 추가할 때 개발자 오류의 위험이 증가했습니다.
두 번째 솔루션은 유형 제거된 AnyCodable 래퍼나 존재론적 유형(any TransactionProtocol)이 있는 프로토콜 지향 접근 방식을 사용하는 것이었습니다. 이 방법은 상속 없이 이질적인 유형을 배열에 저장할 수 있었지만, 컴파일 타임 유형 안전성을 희생하고 존재론적 박싱과 동적 디스패치에서 런타임 오버헤드를 초래했습니다. 또한 소비자에게 유형 제거 아티팩트와 캐스팅을 처리하도록 강요하여 API 계약을 복잡하게 만들어 코드의 명확성을 줄였습니다.
세 번째 옵션은 클래스 계층을 단일 열거형으로 리팩토링하는 것으로, 쉼표로 구분된 값을 가진 **enum Transaction { case stock(StockData), case dividend(DividendData) }**와 같은 형태로 바꾸는 것이었습니다. 열거형은 생성된 Codable을 통해 자연스럽게 다형적 직렬화를 지원합니다. 컴파일러는 자동으로 판별자 필드를 생성합니다. 그러나 이는 기존 Core Data 모델과 애플리케이션 전반의 비즈니스 로직을 대대적으로 리팩토링해야 하며, 프로덕션 시스템에 대한 불가피한 회귀 위험을 동반하게 됩니다.
우리는 첫 번째 솔루션—판별자 필드가 있는 수동 Codable 구현—을 선택했습니다. 이는 기존 아키텍처나 데이터베이스 스키마를 방해하지 않고 직렬화 레이어의 변경을 국지화했습니다. 우리는 기본 클래스에서 유형 식별자를 먼저 디코딩한 다음, 문자열 값을 기반으로 적절한 서브클래스 초기화 프로그램에 위임하는 팩토리 메소드를 구현했습니다.
그 결과는 완전한 유형 충실도로 다형적 API 응답을 올바르게 처리하는 강력한 직렬화 파이프라인이 되었습니다. 수동 파싱 코드에 약 200줄이 필요했지만, 기존 기능과의 하위 호환성을 유지하고, 개발자가 새로운 거래 유형을 추가할 때 디코딩 로직을 업데이트하는 것을 잊으면 컴파일 타임 오류를 명확하게 제공하여 런타임 실패를 예방했습니다.
왜 JSONEncoder로 인코딩하기 전에 [Subclass]를 [BaseClass]로 캐스팅하는 것이 서브클래스 전용 속성에 대한 데이터 손실을 초래하나요?
생성된 encode(to:) 메소드는 컬렉션의 값의 컴파일 타임 유형에 따라 정적으로 호출됩니다. **[BaseClass]**로 캐스팅할 때 컴파일러는 BaseClass의 생성된 구현을 선택하며, 이는 BaseClass에서 선언된 속성만 반복합니다. 서브클래스 속성은 이 구현에 보이지 않기 때문에 정적 디스패치 메커니즘이 생성된 메서드에 대해 동적 유형의 메타데이터를 검토하지 않습니다. 모든 속성을 유지하려면 구체적인 유형을 사용하여 인코딩하거나 판별자 필드를 통해 동적 유형 해상을 수동으로 구현해야 합니다.
필수 초기화자의 요구 사항이 클래스 계층에서 Decodable 준수와 어떻게 상호 작용하며, 이는 자동 서브클래스 인스턴스화를 방해하는 이유는 무엇인가요?
Decodable은 init(from: Decoder) 초기화자를 요구합니다. 클래스의 경우, 이는 서브클래스가 준수를 상속할 수 있도록 기본 클래스에 required로 표시되어야 합니다. 그러나 기본 클래스의 생성된 구현은 판별자 필드와 같은 외부 데이터를 기반으로 어떤 서브클래스를 인스턴스화할 것인지 동적으로 결정할 수 없습니다. 디코더가 서브클래스를 나타내는 데이터를 발견하면 기본 클래스의 **init(from:)**을 호출하는데, 이는 기본 클래스 부분만 초기화하는 방법만 알고 있습니다. 다형적 디코딩을 지원하려면 개발자는 모든 서브클래스에서 **init(from:)**을 재정의하고 인스턴스화 전에 디코더의 컨테이너를 조사하는 팩토리 메소드를 구현해야 합니다.
스위프트의 생성된 Codable이 관련 값이 있는 열거형과 클래스 상속을 처리하는 방식의 근본적인 차이는 무엇이며, 이는 왜 열거형이 다형적 직렬화에 적합하게 만드는가요?
스위프트는 관련 값이 있는 열거형에 대해 Codable을 생성할 때 판별자 키를 생성합니다. 인코딩은 케이스 이름을 문자열 키로 포함하며, 디코딩 구현은 이 키에 따라 전환하여 올바른 케이스와 그와 관련된 페이로드를 재구성합니다. 이는 열거형이 컴파일 타임에 철저하게 알려진 폐쇄된 봉인된 유형 계층을 형성하기 때문에 가능합니다. 이에 따라 컴파일러는 완전한 switch 문을 생성할 수 있습니다. 반면에 클래스는 다른 모듈에 새 서브클래스를 추가할 수 있는 열린 계층을 형성합니다. 컴파일러는 기본 클래스의 Codable 준수를 생성할 때 가능한 모든 서브클래스에 대한 철저한 switch를 생성할 수 없으며, 수동 개입 없이 다형성을 자동으로 처리하는 것이 불가능해집니다.