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로 돌아와야 한다. |