Swift Type System

any / some / Generic / Dispatch를 쉽게 이해하기

어려운 용어를 먼저 아주 짧게 정의하고, 바로 코드 예시로 연결해서 이해하도록 구성한 문서다.

1. 이 문서는 성능 미세 튜닝 문서가 아니라, 개념을 헷갈리지 않게 정리하는 문서다.
2. 핵심은 "언제 any를 쓰고, 언제 some/Generic을 쓰는가"다.

1. 용어 사전 (진짜 쉽게)

용어쉬운 뜻
Protocol기능 약속서. "이 함수는 있어야 한다"를 정한다.
Concrete Type실제 타입 이름이 있는 것. 예: URLImageLoader
Generic (T: P)타입을 나중에 받는 템플릿 함수/타입.
any PP를 만족하는 "아무 타입"을 담는 상자(existential).
some P호출자에겐 숨기지만 내부적으로는 한 가지 고정 타입.
Dispatch함수 호출 대상을 결정하는 방식.
Witness Table프로토콜 요구사항을 실제 함수에 연결해주는 표(런타임 참조).
Specialization컴파일러가 Generic 코드를 구체 타입 버전으로 최적화하는 것.

1-1. self / Self / any / some 한 페이지 압축 비교

키워드무엇을 가리키나주요 사용 위치핵심 포인트
self현재 인스턴스(값/객체) 또는 타입 문맥의 현재 타입 값메서드 본문, 타입 메서드소문자 self는 \"지금 대상\" 자체를 가리킨다.
Self현재 구체 타입(타입 자리의 키워드)프로토콜 요구사항, 메서드 반환 타입func copy() -> Self는 \"같은 구체 타입 인스턴스\" 반환을 뜻한다.
any PP를 채택한 임의 타입 값을 담는 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()
}
1. self는 \"값/객체\", Self는 \"타입\"이다.
2. any는 다형성 저장/전달, some은 반환 API 캡슐화에 주로 쓴다.

2. any vs some 한 번에 정리

비교any Psome P
구체 타입 숨김가능가능
반환마다 타입 달라져도 됨가능불가 (항상 한 타입)
주 용도혼합 저장/동적 선택반환 API 캡슐화
성능 관점런타임 간접 호출 가능성최적화 유리한 편

3. Dispatch를 쉬운 비유로

  1. 정적 dispatch: 컴파일 때 이미 호출할 함수가 정해진다.
  2. 동적 dispatch: 실행 중에 "어떤 구현을 부를지" 찾아간다.
  3. any는 상자 안 실제 타입을 런타임에 확인하고 연결할 때가 많다.
  4. Generic/some은 컴파일러가 구체 타입으로 붙잡아 최적화하기 쉽다.
1. 둘 다 항상 느리다/빠르다가 아니다.
2. 보통 앱 병목은 네트워크, 디스크, 렌더링이고 dispatch 차이는 핫루프에서만 체감된다.

4. 코드 예시: any / some / Generic

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")!)
}

5. 질문 핵심: 왜 구체 타입을 숨기나

1. 맞다. 숨기려는 대상은 "구체 타입"이다.

2. 숨기는 이유는 변경 파급을 줄이기 위해서다.

// A. 숨기지 않은 API (구체 타입 노출)
public struct URLAuthProvider {
    public func token() -> String { "t" }
}

public enum AuthFactory {
    public static func make() -> URLAuthProvider {
        URLAuthProvider()
    }
}
위 구조에서 나중에 URLAuthProviderKeychainAuthProvider로 바꾸면, 반환 타입이 바뀌어서 외부 호출 코드/API 계약이 깨질 수 있다.
// 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()
    }
}
호출자는 AuthProviding 계약만 알면 된다. 내부 구현을 바꿔도 외부 파급을 줄이기 쉽다.

6. 언제 무엇을 쓰면 되나 (실무 규칙)

  1. 혼합 컬렉션, 런타임 분기 선택이 필요하면 any.
  2. 외부 반환 API에서 단일 구현을 숨기고 싶으면 some.
  3. 핫패스 CPU 루프면 Generic/some 우선 검토, 그 외는 가독성과 유지보수 우선.
  4. 팀이 헷갈리면 "저장은 any, 팩토리 반환은 some" 규칙부터 시작한다.

7. 대화 Q&A 요약

질문핵심 답
테스트 더블이 뭐야?테스트에서 실제 객체 대신 넣는 가짜 객체 전체(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를 명시한다.
1. 이 표에서 헷갈리는 항목은 본문 2, 4, 5 절 코드 예시를 같이 보면 바로 연결된다.
2. 팀 룰을 하나로 정할 때는 \"반환 some / 저장 any\"를 기본값으로 두고 예외만 문서화하면 된다.

7-1. 대화 Q&A 요약 (Task / lock / actor)

질문핵심 답
@discardableResult 왜 쓰나?반환값(UUID 토큰)을 안 받는 호출부에서도 경고를 막기 위한 장치다. 모든 호출부가 토큰을 저장하면 없어도 된다.
메인에서 lock 쓰면 멈추나?락 구간이 길면 멈출 수 있다. 다만 짧은 딕셔너리 접근 보호 용도면 보통 체감이 작다.
running[UUID: Task] 왜 필요?진행 중 작업을 추적해 셀 재사용 시 이전 요청을 정확히 취소하기 위해 필요하다.
Task {} vs Task.detached {}Task는 현재 actor 문맥을 상속하고, detached는 상속하지 않는다.
Task.detached는 항상 백그라운드?스레드 고정 개념이 아니다. main actor 보장이 없고 executor가 스케줄링한다. UI는 명시적으로 MainActor로 돌아와야 한다.

관련 문서: pt, px, scale 개념 정리, SwiftUI some View 심화

8. Primary Associated Type & Generic 함수가 아는 것 (2026-07-06)

8-1. Primary Associated Type (Swift 5.7+)

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 TypeArray가 제네릭, <Int>는 타입 파라미터
any Container<Int>Existential + PAT<Int>는 Associated Type 지정
some Container<Int>Opaque + PAT컴파일러가 구체 타입 알지만 숨김
<T: Container>Generic ConstraintT가 제네릭 파라미터
Container<Int><Int>는 타입 파라미터가 아니라 Associated Type 지정이다. 생김새가 제네릭과 같아서 헷갈리지만, "제네릭"이라 부르면 틀린 용어다.

⚠️ 주의: SomeType<T> 문법만 보면 Generic struct인지 Protocol+PAT인지 구분 불가능하다. Swift 6.0부터는 프로토콜에 any가 필수라 구분 가능하지만, 5.7 이하에서는 선언부를 봐야 안다.

8-2. any 생략 가능? (Swift 버전별)

Swift 버전let x: Container
5.6 이하OK (암묵적 existential)
5.7경고 (any 명시 권장)
6.0+에러 (any 필수)

8-3. Generic 함수가 아는 것 / 모르는 것

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인지 모름
}
아는 것모르는 것
cContainer 프로토콜 준수T의 구체 타입 (Array? Set?)
프로토콜에 정의된 프로퍼티/메서드Item의 구체 타입 (Int? String?)
T.Item이라는 타입 존재
"내부를 모른다"가 아니다. 프로토콜 인터페이스는 다 알지만, Associated Type의 구체 타입만 모른다가 정확한 표현이다.

8-4. where 절 유무 차이

// 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을 추상 타입으로만 다룬다.

8-5. Dispatch 정리

문법Dispatch이유
any Container<Int>DynamicExistential - 런타임에 실제 타입 확인
some Container<Int>StaticOpaque - 컴파일러가 구체 타입 알고 있음
<T: Container>StaticGeneric - 호출 시점에 T 확정, specialization 가능