2. any vs some 한 번에 정리
| 비교 | any P | some P |
|---|---|---|
| 구체 타입 숨김 | 가능 | 가능 |
| 반환마다 타입 달라져도 됨 | 가능 | 불가 (항상 한 타입) |
| 주 용도 | 혼합 저장/동적 선택 | 반환 API 캡슐화 |
| 성능 관점 | 런타임 간접 호출 가능성 | 최적화 유리한 편 |
어려운 용어를 먼저 아주 짧게 정의하고, 바로 코드 예시로 연결해서 이해하도록 구성한 문서다.
| 용어 | 쉬운 뜻 |
|---|---|
| Protocol | 기능 약속서. "이 함수는 있어야 한다"를 정한다. |
| Concrete Type | 실제 타입 이름이 있는 것. 예: URLImageLoader |
| Generic (T: P) | 타입을 나중에 받는 템플릿 함수/타입. |
| any P | P를 만족하는 "아무 타입"을 담는 상자(existential). |
| some P | 호출자에겐 숨기지만 내부적으로는 한 가지 고정 타입. |
| Dispatch | 함수 호출 대상을 결정하는 방식. |
| Witness Table | 프로토콜 요구사항을 실제 함수에 연결해주는 표(런타임 참조). |
| Specialization | 컴파일러가 Generic 코드를 구체 타입 버전으로 최적화하는 것. |
| 키워드 | 무엇을 가리키나 | 주요 사용 위치 | 핵심 포인트 |
|---|---|---|---|
| self | 현재 인스턴스(값/객체) 또는 타입 문맥의 현재 타입 값 | 메서드 본문, 타입 메서드 | 소문자 self는 \"지금 대상\" 자체를 가리킨다. |
| Self | 현재 구체 타입(타입 자리의 키워드) | 프로토콜 요구사항, 메서드 반환 타입 | func copy() -> Self는 \"같은 구체 타입 인스턴스\" 반환을 뜻한다. |
| any P | P를 채택한 임의 타입 값을 담는 existential | 혼합 저장, 런타임 선택, DI 슬롯 | 다양한 타입을 한 변수/배열에 담기 좋다. |
| some P | 호출자에게 숨긴 단일 구체 타입(opaque) | 팩토리 반환 타입, API 캡슐화 | 타입 숨김 + 선언 단위로 실제 타입 하나 고정. |
protocol Copyable {
func copy() -> Self
}
final class Document: Copyable {
var content: String
required init(content: String) { self.content = content }
func copy() -> Self {
// self: 현재 인스턴스
// Self: 반환해야 하는 현재 구체 타입
type(of: self).init(content: content)
}
}
protocol Drawable { func draw() }
struct Circle: Drawable { func draw() {} }
struct Square: Drawable { func draw() {} }
let mixed: [any Drawable] = [Circle(), Square()] // any: 혼합 저장
func makeDefaultShape() -> some Drawable { // some: 단일 구현 숨김
Circle()
}| 비교 | any P | some P |
|---|---|---|
| 구체 타입 숨김 | 가능 | 가능 |
| 반환마다 타입 달라져도 됨 | 가능 | 불가 (항상 한 타입) |
| 주 용도 | 혼합 저장/동적 선택 | 반환 API 캡슐화 |
| 성능 관점 | 런타임 간접 호출 가능성 | 최적화 유리한 편 |
protocol ImageLoading {
func load(_ url: URL) async throws -> Data
}
struct URLImageLoader: ImageLoading {
func load(_ url: URL) async throws -> Data { Data() }
}
struct CacheImageLoader: ImageLoading {
func load(_ url: URL) async throws -> Data { Data() }
}
// 1) any: 서로 다른 타입을 한 컨테이너에 담기
let mixed: [any ImageLoading] = [URLImageLoader(), CacheImageLoader()]
// 2) some: 호출자에겐 숨기되, 실제 반환 타입은 하나
func makeStableLoader() -> some ImageLoading {
URLImageLoader()
}
// 3) any 반환: 호출마다 다른 타입 반환 가능
func makeDynamicLoader(useCache: Bool) -> any ImageLoading {
if useCache { return CacheImageLoader() }
return URLImageLoader()
}
// 4) Generic 사용: 함수 레벨에서 구체 타입 최적화 여지
func render<L: ImageLoading>(loader: L) async throws {
_ = try await loader.load(URL(string: "https://example.com")!)
}
// 5) any 파라미터도 가능
func renderAny(loader: any ImageLoading) async throws {
_ = try await loader.load(URL(string: "https://example.com")!)
}// A. 숨기지 않은 API (구체 타입 노출)
public struct URLAuthProvider {
public func token() -> String { "t" }
}
public enum AuthFactory {
public static func make() -> URLAuthProvider {
URLAuthProvider()
}
}// B. 숨긴 API (계약만 노출)
public protocol AuthProviding {
func token() -> String
}
struct URLAuthProvider: AuthProviding {
func token() -> String { "t" }
}
public enum AuthFactory {
public static func make() -> some AuthProviding {
URLAuthProvider()
}
}| 질문 | 핵심 답 |
|---|---|
| 테스트 더블이 뭐야? | 테스트에서 실제 객체 대신 넣는 가짜 객체 전체(mock, stub, fake)를 말한다. |
| 상속 더블과 protocol 더블 차이? | 상속 더블은 구현체 변경(초기화/상태)에 흔들리기 쉽고, protocol 더블은 계약 변경 때 주로 영향 받는다. |
| 상속 더블도 override로 피하면 되지? | 메서드 본문은 우회 가능해도 부모 init 경로/필수 파라미터/검증 로직 영향은 남는다. |
| any와 some 차이? | 둘 다 타입 숨김은 가능. any는 런타임에 타입이 달라질 수 있고, some은 선언 단위로 실제 타입이 하나로 고정된다. |
| 왜 some으로 구체 타입을 숨겨? | 외부 결합을 줄여 내부 구현 교체 시 파급을 줄이기 위해서다. |
| render(loader: any ImageLoading) 가능? | 가능하다. 다만 성능 민감 경로에서는 Generic/ some이 최적화에 유리한 경우가 많다. |
| AnyEventEmitter<E>에서 E 생략 가능? | 고정 이벤트 타입 래퍼로는 가능하지만, 여러 이벤트 타입 재사용을 원하면 제네릭 E가 필요하다. |
| associatedtype 쓰면 typealias 필수? | 필수 아님. 구현 시그니처로 추론되면 생략 가능하고, 추론이 모호하면 typealias를 명시한다. |
| 질문 | 핵심 답 |
|---|---|
| @discardableResult 왜 쓰나? | 반환값(UUID 토큰)을 안 받는 호출부에서도 경고를 막기 위한 장치다. 모든 호출부가 토큰을 저장하면 없어도 된다. |
| 메인에서 lock 쓰면 멈추나? | 락 구간이 길면 멈출 수 있다. 다만 짧은 딕셔너리 접근 보호 용도면 보통 체감이 작다. |
| running[UUID: Task] 왜 필요? | 진행 중 작업을 추적해 셀 재사용 시 이전 요청을 정확히 취소하기 위해 필요하다. |
| Task {} vs Task.detached {} | Task는 현재 actor 문맥을 상속하고, detached는 상속하지 않는다. |
| Task.detached는 항상 백그라운드? | 스레드 고정 개념이 아니다. main actor 보장이 없고 executor가 스케줄링한다. UI는 명시적으로 MainActor로 돌아와야 한다. |
Swift 5.7부터 프로토콜 선언에 <Item> 문법이 추가되었다. 이건 "제네릭"이 아니라 Associated Type을 외부에 노출하는 문법이다.
// Swift 5.7+ Primary Associated Type
protocol Container<Item> {
associatedtype Item
var first: Item { get }
func append(_ item: Item)
}
// 사용
let box: any Container<Int> // Item이 Int인 Container (existential)
let box2: some Container<Int> // Item이 Int인 Container (opaque)| 문법 | 이름 | 의미 |
|---|---|---|
| Array<Int> | Generic Type | Array가 제네릭, <Int>는 타입 파라미터 |
| any Container<Int> | Existential + PAT | <Int>는 Associated Type 지정 |
| some Container<Int> | Opaque + PAT | 컴파일러가 구체 타입 알지만 숨김 |
| <T: Container> | Generic Constraint | T가 제네릭 파라미터 |
| Swift 버전 | let x: Container |
|---|---|
| 5.6 이하 | OK (암묵적 existential) |
| 5.7 | 경고 (any 명시 권장) |
| 6.0+ | 에러 (any 필수) |
func useGeneric<T: Container>(c: T) {
// ✅ 아는 것 - Container 인터페이스 전부
let item = c.first // OK - T.Item 타입으로 반환
c.append(item) // OK - T.Item을 받는 메서드
// ❌ 모르는 것 - Item의 구체 타입
let n: Int = c.first // 컴파일 에러 - Item이 Int인지 모름
}| 아는 것 | 모르는 것 |
|---|---|
| c가 Container 프로토콜 준수 | T의 구체 타입 (Array? Set?) |
| 프로토콜에 정의된 프로퍼티/메서드 | Item의 구체 타입 (Int? String?) |
| T.Item이라는 타입 존재 |
// where 있음 - Item이 Int임을 알고 사용 가능
func useGeneric<T: Container>(c: T) where T.Item == Int {
let item: Int = c.first // OK, Int로 사용
}
// where 없음 - Item이 뭔지 모름
func useGeneric<T: Container>(c: T) {
let item: T.Item = c.first // OK, 추상적으로만 사용
// let n: Int = c.first // ❌ 컴파일 에러
}where 절은 선택 사항이다. 없으면 함수 내에서 T.Item을 추상 타입으로만 다룬다.
| 문법 | Dispatch | 이유 |
|---|---|---|
| any Container<Int> | Dynamic | Existential - 런타임에 실제 타입 확인 |
| some Container<Int> | Static | Opaque - 컴파일러가 구체 타입 알고 있음 |
| <T: Container> | Static | Generic - 호출 시점에 T 확정, specialization 가능 |