-
Swift Phantom TypesSwift 2025. 8. 15. 09:01
안녕하세요. 그린입니다 🍏
이번 포스팅은 Swift의 고급 타입 시스템 기법인 Phantom Types에 대해 깊이 있게 알아보겠습니다 🚀
Phantom Types Deep Dive
이런 경험 있으신가요?
런타임에 발생하는 타입 관련 버그들의 예시에요.
- "이 ID는 사용자 ID인데 왜 상품 ID로 사용했지? 😱"
- "암호화된 데이터를 일반 문자열로 처리해서 보안 오류 발생"
- "단위가 다른 값들을 잘못 연산해서 계산 오류"
- "상태가 잘못된 객체에 잘못된 메서드 호출"
이런 문제들은 코드가 복잡해질수록 더 자주 발생하고, 런타임에서야 발견되는 경우가 많죠.
Phantom Types는 이런 문제들을 컴파일 타임에 완전히 차단할 수 있는 강력한 기법입니다.
Why Phantom Types Matter More Than Ever?
🎯 타입 안전성 강화
- 런타임 에러를 컴파일 타임으로 전환
- 잘못된 타입 사용을 원천 차단
- 비즈니스 로직의 타입 레벨 검증
⚡ 최신 Swift의 타입 시스템 활용
- Generic Parameter Packs (Swift 5.7)
- Primary Associated Types (Swift 5.7)
- some/any 키워드 활용 (Swift 5.6+)
- Opaque Result Types와의 시너지
Phantom Types?
Phantom Type은 제네릭 타입 매개변수를 실제 저장하지 않으면서도 컴파일 타임 타입 체크에만 사용하는 기법
// 기본 개념 struct PhantomWrapper<T> { let value: String // T 타입은 실제로 저장되지 않음 init(_ value: String) { self.value = value } } // 타입 별칭으로 의미 부여 typealias UserID = PhantomWrapper<User> typealias ProductID = PhantomWrapper<Product> // 사용 let userID = UserID("user123") let productID = ProductID("prod456") // 컴파일 에러! 다른 타입이므로 할당 불가 // let wrongID: UserID = productID ❌실제 메모리에서는 어떻게 동작할까?
import Foundation struct PhantomWrapper<T> { let value: String } typealias UserID = PhantomWrapper<User> typealias ProductID = PhantomWrapper<Product> // 런타임에서 크기 확인 print(MemoryLayout<UserID>.size) // 16 bytes (String) print(MemoryLayout<ProductID>.size) // 16 bytes (String) print(MemoryLayout<String>.size) // 16 bytes (String) // 메모리 사용량은 동일하지만 타입 안전성은 완전히 다름!
적용 사례
💡 ID 타입 안전성
기존 방식의 문제점
// 🚫 위험한 방식 func getUserProfile(userID: String) -> User? { ... } func getProduct(productID: String) -> Product? { ... } let userID = "user123" let productID = "prod456" // 런타임에서야 발견되는 실수 let profile = getUserProfile(userID: productID) // 잘못된 ID 전달!Phantom Types로 해결
// ✅ 타입 안전한 방식 struct ID<Entity> { let value: String init(_ value: String) { self.value = value } } // 엔티티별 타입 정의 struct User { } struct Product { } struct Order { } typealias UserID = ID<User> typealias ProductID = ID<Product> typealias OrderID = ID<Order> // 타입 안전한 API func getUserProfile(userID: UserID) -> User? { ... } func getProduct(productID: ProductID) -> Product? { ... } // 사용 let userID = UserID("user123") let productID = ProductID("prod456") // 컴파일 에러로 실수 방지! // let profile = getUserProfile(userID: productID) ❌ let profile = getUserProfile(userID: userID) // ✅
💡 데이터 상태 관리
// 데이터 처리 상태를 타입으로 표현 struct DataState<State> { let data: Data fileprivate init(_ data: Data) { self.data = data } } // 상태 마커 타입들 enum Raw { } enum Validated { } enum Encrypted { } enum Compressed { } typealias RawData = DataState<Raw> typealias ValidatedData = DataState<Validated> typealias EncryptedData = DataState<Encrypted> typealias CompressedData = DataState<Compressed> // 상태별 처리 함수들 func validate(raw: RawData) throws -> ValidatedData { // 유효성 검사 로직 guard isValid(raw.data) else { throw ValidationError.invalid } return ValidatedData(raw.data) } func encrypt(validated: ValidatedData) -> EncryptedData { let encryptedData = performEncryption(validated.data) return EncryptedData(encryptedData) } func compress(validated: ValidatedData) -> CompressedData { let compressedData = performCompression(validated.data) return CompressedData(compressedData) } // 최종 저장은 암호화된 데이터만 가능 func saveToServer(encrypted: EncryptedData) { // 서버 저장 로직 } // 사용 예시 let rawData = RawData(someData) do { let validatedData = try validate(raw: rawData) let encryptedData = encrypt(validated: validatedData) // 타입 안전성으로 실수 방지 // saveToServer(encrypted: validatedData) ❌ 컴파일 에러 saveToServer(encrypted: encryptedData) // ✅ } catch { print("검증 실패: \(error)") }
💡 단위 시스템
// 물리 단위 시스템 struct Measurement<Unit> { let value: Double init(_ value: Double) { self.value = value } } // 단위 마커 타입들 enum Meter { } enum Kilogram { } enum Second { } enum MeterPerSecond { } typealias Distance = Measurement<Meter> typealias Mass = Measurement<Kilogram> typealias Time = Measurement<Second> typealias Velocity = Measurement<MeterPerSecond> // 단위별 연산 확장 extension Measurement where Unit == Meter { static func + (lhs: Distance, rhs: Distance) -> Distance { return Distance(lhs.value + rhs.value) } func divided(by time: Time) -> Velocity { return Velocity(self.value / time.value) } } extension Measurement where Unit == MeterPerSecond { func multiplied(by time: Time) -> Distance { return Distance(self.value * time.value) } } // 사용 예시 let distance1 = Distance(100.0) // 100m let distance2 = Distance(50.0) // 50m let time = Time(10.0) // 10초 let totalDistance = distance1 + distance2 // 150m let velocity = distance1.divided(by: time) // 10 m/s // 잘못된 연산은 컴파일 에러 // let wrongResult = distance1 + time ❌
High Patterns
🚀 State Machine with Phantom Types
복잡한 상태 머신을 타입 안전하게 구현
// 네트워크 요청 상태 머신 struct NetworkRequest { let urlRequest: URLRequest private let session: URLSession fileprivate init(urlRequest: URLRequest, session: URLSession = .shared) { self.urlRequest = urlRequest self.session = session } } // 상태 정의 enum Created { } enum Authenticated { } enum Executed { } enum Completed { } typealias CreatedRequest = NetworkRequest typealias AuthenticatedRequest = NetworkRequest typealias ExecutedRequest = NetworkRequest typealias CompletedRequest = NetworkRequest // 상태별 전이 메서드들 extension NetworkRequest where State == Created { func authenticate(token: String) -> AuthenticatedRequest { var authenticatedRequest = urlRequest authenticatedRequest.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") return AuthenticatedRequest(urlRequest: authenticatedRequest, session: session) } } extension NetworkRequest where State == Authenticated { func execute() async throws -> ExecutedRequest { let (data, response) = try await session.data(for: urlRequest) // 실행 결과 처리... return ExecutedRequest(urlRequest: urlRequest, session: session) } } extension NetworkRequest where State == Executed { func complete(with handler: @escaping (Data) -> Void) -> CompletedRequest { // 완료 처리... return CompletedRequest(urlRequest: urlRequest, session: session) } } // 사용법 - 순서대로만 호출 가능 let request = CreatedRequest(urlRequest: URLRequest(url: URL(string: "https://api.example.com")!)) Task { let authenticated = request.authenticate(token: "abc123") let executed = try await authenticated.execute() let completed = executed.complete { data in print("요청 완료: \(data)") } // 잘못된 순서로 호출하면 컴파일 에러 // let wrong = request.execute() ❌ }
🚀 Builder Pattern with Type Safety
// 타입 안전한 빌더 패턴 struct QueryBuilder<HasSelect, HasFrom, HasWhere> { private var selectClause: String = "" private var fromClause: String = "" private var whereClause: String = "" fileprivate init() { } fileprivate init(select: String, from: String, where: String) { self.selectClause = select self.fromClause = from self.whereClause = where } } // 상태 마커 enum HasSelectClause { } enum HasFromClause { } enum HasWhereClause { } enum Missing { } typealias EmptyQuery = QueryBuilder<Missing, Missing, Missing> typealias QueryWithSelect = QueryBuilder<HasSelectClause, Missing, Missing> typealias QueryWithSelectFrom = QueryBuilder<HasSelectClause, HasFromClause, Missing> typealias CompleteQuery = QueryBuilder<HasSelectClause, HasFromClause, HasWhereClause> // 단계별 빌더 메서드 extension QueryBuilder where HasSelect == Missing { func select(_ columns: String) -> QueryWithSelect { return QueryBuilder<HasSelectClause, HasFrom, HasWhere>( select: "SELECT \(columns)", from: fromClause, where: whereClause ) } } extension QueryBuilder where HasSelect == HasSelectClause, HasFrom == Missing { func from(_ table: String) -> QueryWithSelectFrom { return QueryBuilder<HasSelectClause, HasFromClause, HasWhere>( select: selectClause, from: "FROM \(table)", where: whereClause ) } } extension QueryBuilder where HasSelect == HasSelectClause, HasFrom == HasFromClause, HasWhere == Missing { func `where`(_ condition: String) -> CompleteQuery { return QueryBuilder<HasSelectClause, HasFromClause, HasWhereClause>( select: selectClause, from: fromClause, where: "WHERE \(condition)" ) } } // 최종 실행은 완전한 쿼리에서만 가능 extension QueryBuilder where HasSelect == HasSelectClause, HasFrom == HasFromClause { func build() -> String { var query = "\(selectClause) \(fromClause)" if !whereClause.isEmpty { query += " \(whereClause)" } return query } } // 사용법 let query = EmptyQuery() .select("name, email") .from("users") .where("age > 18") .build() print(query) // "SELECT name, email FROM users WHERE age > 18" // 잘못된 순서로는 호출 불가 // let wrong = EmptyQuery().from("users") ❌
Swift 5.7+ 기능과의 조합
🔮 Primary Associated Types 활용
// Swift 5.7의 Primary Associated Types와 결합 protocol IdentifiableEntity<ID> { associatedtype ID var id: ID { get } } struct TypedEntity<EntityType, IDType>: IdentifiableEntity { let id: IDType let data: EntityType // Primary Associated Type 지정 typealias ID = IDType } // 사용 struct User { let name: String let email: String } let userEntity = TypedEntity<User, UserID>( id: UserID("user123"), data: User(name: "John", email: "john@example.com") )
🔮 some/any 키워드와의 조합
// Opaque Result Types와 Phantom Types protocol Processable<Input, Output> { associatedtype Input associatedtype Output func process(_ input: Input) -> Output } struct DataProcessor<InputState, OutputState>: Processable { typealias Input = DataState<InputState> typealias Output = DataState<OutputState> func process(_ input: Input) -> Output { // 처리 로직 return DataState<OutputState>(input.data) } } // some을 사용한 타입 은닉 func createValidator() -> some Processable<RawData, ValidatedData> { return DataProcessor<Raw, Validated>() } func createEncryptor() -> some Processable<ValidatedData, EncryptedData> { return DataProcessor<Validated, Encrypted>() }
성능 최적화
⚡ Zero-Cost Abstractions
Phantom Types는 런타임 오버헤드가 전혀 없는 Zero-Cost Abstraction
import Foundation // 성능 테스트용 구조체 struct RegularString { let value: String } struct PhantomString<T> { let value: String } typealias UserString = PhantomString<User> // 성능 측정 func measurePerformance() { let iterations = 1_000_000 // Regular String 테스트 let start1 = CFAbsoluteTimeGetCurrent() for i in 0..<iterations { let regular = RegularString(value: "test\(i)") _ = regular.value.count } let time1 = CFAbsoluteTimeGetCurrent() - start1 // Phantom Type 테스트 let start2 = CFAbsoluteTimeGetCurrent() for i in 0..<iterations { let phantom = UserString(value: "test\(i)") _ = phantom.value.count } let time2 = CFAbsoluteTimeGetCurrent() - start2 print("Regular String: \(time1)s") print("Phantom Type: \(time2)s") print("Difference: \(abs(time1 - time2))s") // 거의 차이 없음! }
⚡ 컴파일 타임 최적화
복잡한 Phantom Type 계층구조에서 컴파일 성능을 위한 팁
// ✅ 좋은 방법: 타입 별칭 활용 typealias ValidatedUserData = DataState<Validated> typealias EncryptedUserData = DataState<Encrypted> // ❌ 피해야 할 방법: 중첩된 제네릭 // DataState<ValidationState<UserValidation<StrictMode>>> // ✅ 좋은 방법: 간단한 마커 타입 enum UserScope { } enum AdminScope { } // ❌ 피해야 할 방법: 복잡한 마커 타입 // struct ComplexMarker<A, B, C, D> { }
실제 프로젝트 적용 사례
📱 네트워킹 레이어 타입 안전성
// API 엔드포인트 타입 안전성 struct APIEndpoint { let path: String let method: HTTPMethod let headers: [String: String]? init(path: String, method: HTTPMethod = .GET, headers: [String: String]? = nil) { self.path = path self.method = method self.headers = headers } } // 응답 타입별 엔드포인트 struct UserResponse: Codable { let id: String let name: String let email: String } struct ProductResponse: Codable { let id: String let name: String let price: Double } // 타입 안전한 API 클라이언트 class APIClient { func request(_ endpoint: APIEndpoint) async throws -> T { let url = URL(string: "https://api.example.com\(endpoint.path)")! var request = URLRequest(url: url) request.httpMethod = endpoint.method.rawValue endpoint.headers?.forEach { key, value in request.setValue(value, forHTTPHeaderField: key) } let (data, _) = try await URLSession.shared.data(for: request) return try JSONDecoder().decode(T.self, from: data) } } // 사용법 let userEndpoint = APIEndpoint(path: "/users/123") let productEndpoint = APIEndpoint(path: "/products/456") let client = APIClient() Task { // 컴파일 타임에 응답 타입이 결정됨 let user: UserResponse = try await client.request(userEndpoint) let product: ProductResponse = try await client.request(productEndpoint) // 잘못된 타입 할당은 컴파일 에러 // let wrongUser: ProductResponse = try await client.request(userEndpoint) ❌ }
📱 SwiftUI와의 조합
import SwiftUI // 뷰 상태 관리 struct ViewState<State> { let isLoading: Bool let error: Error? fileprivate init(isLoading: Bool = false, error: Error? = nil) { self.isLoading = isLoading self.error = error } } enum Loading { } enum Loaded { } enum Error { } typealias LoadingState = ViewState<Loading> typealias LoadedState = ViewState<Loaded> typealias ErrorState = ViewState<Error> // SwiftUI View와 통합 struct ContentView: View { @State private var currentState: any ViewStateProtocol = LoadingState(isLoading: true) var body: some View { Group { switch currentState { case is LoadingState: ProgressView("로딩 중...") case is LoadedState: Text("데이터 로드 완료") case is ErrorState: Text("오류 발생") default: EmptyView() } } .onAppear { loadData() } } private func loadData() { // 상태 전이 로직 currentState = LoadingState(isLoading: true) DispatchQueue.main.asyncAfter(deadline: .now() + 2) { currentState = LoadedState(isLoading: false) } } } protocol ViewStateProtocol { } extension ViewState: ViewStateProtocol { }
디버깅과 테스팅
🔍 Phantom Types 디버깅 팁
// 디버깅을 위한 타입 정보 출력 extension PhantomWrapper { var debugDescription: String { return "PhantomWrapper<\(T.self)>(\(value))" } static var typeDescription: String { return "PhantomWrapper<\(T.self)>" } } // 런타임 타입 체크 (디버깅용) func debugTypeInfo<T>(_ phantom: PhantomWrapper<T>) { print("타입: \(type(of: phantom))") print("제네릭 타입: \(T.self)") print("값: \(phantom.value)") print("메모리 크기: \(MemoryLayout.size(ofValue: phantom))") } // 사용 let userID = UserID("user123") debugTypeInfo(userID) // 출력: // 타입: PhantomWrapper<User> // 제네릭 타입: User // 값: user123 // 메모리 크기: 16
🔍 테스트 작성
import XCTest class PhantomTypesTests: XCTestCase { func testIDTypeSafety() { let userID = UserID("user123") let productID = ProductID("prod456") // 타입이 다름을 확인 XCTAssertFalse(type(of: userID) == type(of: productID)) // 값은 접근 가능 XCTAssertEqual(userID.value, "user123") XCTAssertEqual(productID.value, "prod456") } func testDataStateTransitions() throws { let rawData = RawData(Data("test".utf8)) // 유효성 검사 성공 let validatedData = try validate(raw: rawData) XCTAssertEqual(validatedData.data, rawData.data) // 암호화 let encryptedData = encrypt(validated: validatedData) XCTAssertNotEqual(encryptedData.data, validatedData.data) } func testCompileTimeTypeSafety() { // 이 테스트는 컴파일이 되는지 확인하는 용도 let userID = UserID("test") let processedUser = processUser(id: userID) // 다음 라인이 주석 해제되면 컴파일 에러 발생 // let productID = ProductID("test") // let wrongProcessed = processUser(id: productID) // ❌ XCTAssertNotNil(processedUser) } private func processUser(id: UserID) -> String? { return "Processed: \(id.value)" } }
주의사항
⚠️ 흔한 실수들
// 🚫 실수 1: 런타임 타입 체크에 의존 func badExample<T>(_ phantom: PhantomWrapper<T>) { // Phantom Type은 런타임에 T 정보가 없을 수 있음 if T.self == User.self { // 위험한 패턴 // 타입 캐스팅 로직 } } // ✅ 올바른 방법: 프로토콜 기반 접근 protocol EntityMarker { static var entityName: String { get } } extension User: EntityMarker { static let entityName = "User" } extension Product: EntityMarker { static let entityName = "Product" } func goodExample<T: EntityMarker>(_ phantom: PhantomWrapper<T>) { print("Entity: \(T.entityName)") }
⚠️ 성능 고려사항
// 🚫 실수 2: 불필요한 중첩 // struct OverlyComplexType<A, B, C, D, E> { } // 컴파일 시간 증가 // ✅ 올바른 방법: 단순한 구조 struct SimplePhantom<T> { let value: String } // 필요시 타입 조합 typealias ComplexID = SimplePhantom<(User, Product)>
⚠️ API 설계 가이드라인
// ✅ 좋은 API 설계 protocol Identifiable { associatedtype EntityType var id: ID<EntityType> { get } } struct User: Identifiable { typealias EntityType = User let id: ID<User> let name: String } // ✅ 명확한 타입 별칭 typealias UserID = ID<User> typealias ProductID = ID<Product> // ❌ 피해야 할 패턴: 모호한 이름 // typealias SomeID = ID<SomeType> // typealias DataThing<T> = PhantomWrapper<T>
Conclusion
Swift Phantom Types는 컴파일 타임 타입 안전성을 극대화하는 강력한 도구입니다.
핵심 포인트를 정리해볼까요?
🎯 Zero-Cost 타입 안전성
- 런타임 오버헤드 없이 컴파일 타임 검증 강화
- 잘못된 타입 사용을 원천 차단
- 비즈니스 로직의 타입 레벨 모델링
🎯 실용적 적용 영역
- ID 타입 구분으로 Entity 안전성 확보
- 데이터 상태 관리로 처리 단계 보장
- API 타입 안전성으로 네트워킹 오류 방지
- 상태 머신으로 복잡한 플로우 제어
🎯 Swift 최신 기능과의 시너지
- Primary Associated Types로 더 명확한 API
- some/any 키워드로 타입 추상화 개선
- Generic Parameter Packs와의 조합 가능
🎯 성능과 개발 경험의 균형
- 컴파일 타임에 모든 검증 완료
- 런타임 성능 저하 없음
- 명확한 컴파일 에러 메시지로 개발 생산성 향상
가장 중요한 것은 과도한 타입 복잡성을 피하면서도 비즈니스 로직의 핵심 제약사항을 타입 시스템으로 표현하는 것입니다.
모든 곳에 Phantom Types를 적용할 필요는 없어요.
대신 런타임 에러 위험이 높거나, 타입 혼동이 빈번한 영역에 선택적으로 적용하는 것이 효과적입니다.
특히 Swift의 강력한 타입 시스템과 Phantom Types의 조합은 정말 강력합니다.
컴파일 타임에 더 많은 버그를 잡을수록, 런타임에서 더 안전하고 신뢰할 수 있는 앱을 만들 수 있거든요.
여러분의 Swift 코드도 Phantom Types로 한층 더 안전하고 표현력 있는 코드가 되길 바랍니다! 🚀✨
References
Episode #12: Tagged
We typically model our data with very general types, like strings and ints, but the values themselves are often far more specific, like emails and ids. We’ll explore how this can lead to subtle runtime bugs and how we can strengthen these types in an erg
www.pointfree.co
'Swift' 카테고리의 다른 글
Swift Collections 내부 구조 분석 (0) 2025.09.06 Make DSL with ResultBuilder (4) 2025.08.30 Swift 컴파일러의 타입 추론 파헤치기 (feat. 왜 이렇게 컴파일이 오래 걸릴까?) (4) 2025.07.27 Swift Homomorphic Encryption (1) 2025.07.05 Migrating the Password Monitoring service from Java (3) 2025.06.29