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 심화