← 학습일지로 돌아가기
Swift Protocol 심화

Associated Type과 PAT 완벽 가이드

Associated Type이 뭔지, PAT(Protocol with Associated Type)가 왜 특별한지, Swift 5.7 이후 어떻게 바뀌었는지 구체적인 예시와 함께 설명한다.

Associated Type PAT any / some

1. Associated Type이란?

프로토콜 안에서 "나중에 정해질 타입"을 선언하는 것

//────────────────────────────────────────────────────────────── // 프로토콜 정의: Item이 뭔지는 "아직 모름" //────────────────────────────────────────────────────────────── protocol Container { //1. Associated Type 선언 (타입 플레이스홀더) associatedtype Item //2. Item을 사용하는 메서드들 func add(_ item: Item) func get() -> Item }

채택할 때 구체 타입이 정해짐:

//────────────────────────────────────────────────────────────── // 구현체 1: Int를 담는 컨테이너 //────────────────────────────────────────────────────────────── struct IntBox: Container { //1. Item = Int로 확정 (빈칸 채우기) typealias Item = Int //2. 내부 저장소 private var value: Int = 0 //3. 프로토콜 메서드 구현 func add(_ item: Int) { value = item } func get() -> Int { return value } } //────────────────────────────────────────────────────────────── // 구현체 2: String을 담는 컨테이너 //────────────────────────────────────────────────────────────── struct StringBox: Container { //1. Item = String으로 확정 typealias Item = String //2. 내부 저장소 private var value: String = "" //3. 프로토콜 메서드 구현 func add(_ item: String) { value = item } func get() -> String { return value } }
비유: Associated Type = 빈칸 문제

프로토콜: "___을 넣고 빼는 기능을 만들어"
IntBox: "Int를 넣고 빼겠습니다" (빈칸 = Int)
StringBox: "String을 넣고 빼겠습니다" (빈칸 = String)

2. PAT (Protocol with Associated Type)란?

PAT = Associated Type을 가진 프로토콜

일반 프로토콜
protocol Drawable { func draw() } // 모든 타입이 명확함 // → 변수 타입으로 바로 사용 가능
PAT (Associated Type 있음)
protocol Container { associatedtype Item // ← PAT! func get() -> Item } // Item이 뭔지 모름 // → 타입으로 바로 못 씀 (5.6 이하)

Self를 사용하는 프로토콜도 PAT처럼 동작

//────────────────────────────────────────────────────────────── // Self = 채택한 타입 자신을 의미 //────────────────────────────────────────────────────────────── protocol Copyable { //1. Self = 나중에 확정될 타입 (Associated Type과 유사) func copy() -> Self } struct Document: Copyable { var title: String //2. Self = Document로 확정 func copy() -> Document { return Document(title: title + " (복사본)") } } // 사용 예시 let original = Document(title: "계약서") let copied = original.copy() // → Document 타입

3. 왜 PAT는 일반 타입으로 못 썼나? (Swift 5.6 이하)

Before: 에러 발생
// Swift 5.6 이하 let box: Container = IntBox() // ❌ Error: Protocol 'Container' can // only be used as a generic constraint // 왜? Item이 뭔지 모르니까! // box.get() → 반환 타입이 뭐야?
해결: Generic 사용
// Generic constraint로 사용 ✅ func process<T: Container>(box: T) { let item = box.get() // item 타입 = T.Item (추론됨) } process(box: IntBox()) // T.Item = Int process(box: StringBox()) // T.Item = String

왜 에러가 날까? - 상세 설명

//────────────────────────────────────────────────────────────── // 문제 상황: 컴파일러 입장에서 생각해보기 //────────────────────────────────────────────────────────────── let box: Container = ??? //1. box.get() 호출 시 let result = box.get() //2. 컴파일러: "result 타입이 뭐야?" // - IntBox면 → Int // - StringBox면 → String // - DataBox면 → Data // → 컴파일 타임에 알 수 없음! //3. 문제점 // - 메모리 크기 모름 (Int=8B, String=16B...) // - 다음 연산 불가능: result + 1 // result가 Int인지 모름 result.count // result가 String인지 모름
핵심 문제: Associated Type이 확정되지 않으면 타입 시스템이 작동할 수 없다.
→ Swift 5.6 이하에서는 Generic constraint로만 사용 가능했다.

4. Swift 5.7+ 에서 바뀐 점

any 키워드로 PAT도 타입으로 사용 가능!

//────────────────────────────────────────────────────────────── // Swift 5.7+ : any 키워드 사용 //────────────────────────────────────────────────────────────── //1. any 키워드로 PAT를 변수 타입으로 사용 let box: any Container = IntBox() // ✅ 됨! //2. 배열에 서로 다른 Item 타입의 Container들을 담을 수 있음 let boxes: [any Container] = [ IntBox(), // Item = Int StringBox(), // Item = String ] //3. 반복문으로 처리 가능 for container in boxes { let item = container.get() // item 타입 = Any print(item) }
어떻게 가능해졌나?

any Container = Existential Container 사용
→ 런타임에 실제 타입 정보를 박스(40바이트)에 담아서 처리
→ 컴파일 타임에 타입 몰라도 OK

→ Existential Container 상세 설명 보기

5. any PAT의 제한 사항

제한 1: 반환값 타입이 Any가 됨

//────────────────────────────────────────────────────────────── // 반환 타입 정보 손실 //────────────────────────────────────────────────────────────── let box: any Container = IntBox() //1. get() 호출 let item = box.get() // → item 타입 = Any (Int가 아님!) //2. 바로 사용 불가 item + 1 // ❌ Error: Any에 + 연산자 없음 //3. 캐스팅 필요 if let intValue = item as? Int { print(intValue + 1) // ✅ 11 }

제한 2: Associated Type을 파라미터로 받는 메서드 호출 불가

//────────────────────────────────────────────────────────────── // 파라미터에 Item 타입을 사용하는 메서드 //────────────────────────────────────────────────────────────── protocol Container { associatedtype Item func add(_ item: Item) // ← Item을 파라미터로 받음 func get() -> Item } let box: any Container = IntBox() //1. 반환은 됨 (Any로) let result = box.get() // ✅ Any 타입으로 반환 //2. 파라미터 전달은 안 됨 box.add(10) // ❌ Error! // 왜? // 컴파일러: "Item이 Int인지 String인지 모르는데 // Int(10)를 넣으라고? 안전하지 않아!"
규칙 정리:

Item반환 위치 → Any로 반환 (OK)
Item파라미터 위치 → 호출 불가 (Error)

제한 3: Self 반환 시 구체 타입 잃어버림

//────────────────────────────────────────────────────────────── // Self 반환 타입의 메서드 //────────────────────────────────────────────────────────────── protocol Copyable { func copy() -> Self } let doc: any Copyable = Document(title: "원본") //1. copy() 호출 let copied = doc.copy() // → copied 타입 = any Copyable (Document가 아님!) //2. Document 전용 프로퍼티 접근 불가 copied.title // ❌ 'any Copyable'에 title 없음 //3. 캐스팅 필요 if let document = copied as? Document { print(document.title) // ✅ "원본 (복사본)" }

6. 일반 프로토콜 vs PAT + any 비교

기능 일반 프로토콜
any Drawable
PAT + any
any Container
변수 타입으로 사용 ✅ 가능 ✅ 가능 (Swift 5.7+)
배열에 담기 ✅ 가능 ✅ 가능
모든 메서드 호출 ✅ 가능 ⚠️ 일부 제한
반환 타입 프로토콜에 정의된 타입 Any (타입 정보 손실)
Associated Type 파라미터 해당 없음 ❌ 호출 불가
Self 파라미터/반환 해당 없음 ⚠️ 타입 정보 손실
결론: Swift 5.7+에서 PAT도 any로 타입처럼 쓸 수 있지만, 완전히 같지는 않다.
타입 정보 손실과 일부 메서드 호출 제한이 있다.

7. 실전 예시: 언제 뭘 쓸까?

Case 1: 타입 정보가 중요할 때 → Generic

//────────────────────────────────────────────────────────────── // 시나리오: 캐시에서 데이터를 꺼내서 처리해야 함 // 요구사항: 정확한 타입으로 반환받아야 함 //────────────────────────────────────────────────────────────── protocol Cache { associatedtype Value func get(key: String) -> Value? } //1. Generic 함수: Value 타입이 보존됨 func fetchFromCache<C: Cache>(cache: C, key: String) -> C.Value? { return cache.get(key: key) } //2. 사용 struct UserCache: Cache { typealias Value = User func get(key: String) -> User? { ... } } let user = fetchFromCache(cache: UserCache(), key: "user_123") // user 타입 = User? ✅ (타입 정보 보존)

Case 2: 여러 타입을 섞어서 저장할 때 → any

//────────────────────────────────────────────────────────────── // 시나리오: 여러 종류의 Repository를 관리하는 Manager // 요구사항: 다양한 Entity 타입의 Repository를 배열로 관리 //────────────────────────────────────────────────────────────── protocol Repository { associatedtype Entity func findAll() -> [Entity] } //1. 구현체들 struct UserRepository: Repository { typealias Entity = User func findAll() -> [User] { ... } } struct ProductRepository: Repository { typealias Entity = Product func findAll() -> [Product] { ... } } //2. any로 배열에 담기 var repositories: [any Repository] = [ UserRepository(), ProductRepository(), ] //3. 순회하며 처리 (타입 정보는 손실됨) for repo in repositories { let entities = repo.findAll() // [Any] 타입 print("Count: \(entities.count)") }

Case 3: 반환 타입을 숨기고 싶을 때 → some

//────────────────────────────────────────────────────────────── // 시나리오: Factory가 어떤 구현체를 만드는지 숨기고 싶음 // 요구사항: 호출자는 프로토콜만 알면 됨 //────────────────────────────────────────────────────────────── //1. some 반환: 컴파일러는 알고, 호출자는 모름 func makeDefaultCache() -> some Cache { return InMemoryCache() // 내부 구현 } //2. 사용: 타입 정보 보존! let cache = makeDefaultCache() let value = cache.get(key: "test") // value 타입 = InMemoryCache.Value (정확한 타입) //3. 장점 // - 호출자는 Cache 프로토콜만 알면 됨 // - 내부 구현 변경해도 호출 코드 수정 불필요 // - 타입 정보는 컴파일러가 보존 → 성능 최적화 가능
선택 가이드:

Generic → 타입 정보가 중요할 때, 성능이 중요할 때
any → 여러 타입을 섞어서 저장할 때, 런타임 다형성이 필요할 때
some → 구현을 숨기면서 타입 정보는 보존하고 싶을 때

8. 한 줄 정리

Associated Type = 프로토콜 안의 "나중에 정해질 타입" 플레이스홀더

PAT = Associated Type을 가진 프로토콜 (Protocol with Associated Type)

Swift 5.6 이하: PAT는 타입으로 사용 불가 → Generic으로만 사용

Swift 5.7+: any로 타입처럼 사용 가능 → 단, 타입 정보 손실 + 일부 메서드 제한

9. Q&A

Q: Generic에서 where 없이도 T.Item 접근 가능?

A: 가능. T: Container만으로 T.Item 접근됨. where는 추가 제약 필요할 때만.

// where 불필요 func process<T: Container>(box: T) { let item = box.get() // T.Item 타입 } // where 필요 - 추가 제약 func process<T: Container>(box: T) where T.Item == Int { } func process<T: Container>(box: T) where T.Item: Equatable { }

Q: func copy() -> Self vs -> Document 둘 다 가능?

A: struct는 둘 다 OK. 상속 없어서 Self가 항상 자기 자신.

struct Document: Copyable { func copy() -> Document { ... } // ✅ func copy() -> Self { ... } // ✅ 둘 다 가능 }

Q: Generic 타입 파라미터를 Associated Type으로 사용 가능?

A: 가능. 단, T: Int는 불가 (Int는 프로토콜 아님).

// ❌ 불가능 - Int는 프로토콜이 아님 struct Circle<T: Int> // ✅ 가능 struct Circle<T: Numeric>: Drawable { typealias Item = T // Generic → Associated Type ✅ }

Q: Generic 함수 내에서 item + 1 가능?

A: 불가. 함수 본문은 "모든 가능한 T"에 대해 유효해야 함. where 필요.

func process<T: Container>(box: T) { let item = box.get() // T.Item (뭔지 모름) item + 1 // ❌ T.Item이 String이면? } // where 있으면 OK func process<T: Container>(box: T) where T.Item: Numeric { let item = box.get() item + 1 // ✅ }

Q: any Copyable로 선언하면 구체 타입 프로퍼티 접근 가능?

A: 불가. 프로토콜에 정의된 것만 접근 가능. 캐스팅 필요.

let doc: any Copyable = Document(title: "원본") doc.title // ❌ 'any Copyable'에 title 없음 doc.copy() // ✅ 프로토콜 메서드라 OK // 캐스팅 필요 if let d = doc as? Document { d.title // ✅ }

관련 문서