← 학습일지로 돌아가기
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)
}
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 // ✅
}