김경록의 앱 개발 여정

[Swift] 타입 추상화(associated, Opaque, Existential, Generic) 본문

TIL

[Swift] 타입 추상화(associated, Opaque, Existential, Generic)

Kim Roks 2025. 7. 16. 21:14

Swift는 타입 안정성이 아주 강력한 언어입니다.
특히 타입 추상화(type abstraction)와 관련된 기능들이 다양해서,

제대로 활용하면 코드의 유연성과 재사용성이 크게 높아지죠.

오늘은 그중에서도 뭔가 비슷한 것 같으면서도 분명히 다른 네 가지 기능을 정리해보려고 합니다:

  • 연관 타입 (associatedtype)
  • 제네릭 (Generic)
  • 불명확 타입 (Opaque Type)
  • 실존 타입 (Existential Type)

어디에 어떻게 써야 할지, 어떤 차이가 있는지 살펴보고 그래서 '이게 왜 존재해야하는가'에 대해 개인적인 생각을 담아봤습니다.

💡 한글 용어는 야곰님의 『스위프트 프로그래밍 4판』을 기준으로 사용했습니다.


1. 🧩 연관 타입 (associatedtype) — 프로토콜 내부에서의 타입 추상화

📌 개념

associatedtype은 프로토콜 내부에서 타입을 추상화하는 기능이에요.
즉, 프로토콜을 따르는 타입이 어떤 구체 타입을 쓸지는 채택한 쪽에서 나중에 정하게 됩니다.

protocol Container { 
  associatedtype Item
  func append(_ item: Item)
  func get(index: Int) -> Item 
}

🎯 목적

  • 프로토콜이 행동만 정의하고, 구체적인 타입은 구현체가 지정
  • 다양한 타입에 대해 유연한 추상화 가능
  • Swift의 Protocol-Oriented Programming 스타일에 잘 맞음

⚠️ 특징

  • associatedtype이 있는 프로토콜은 그 자체로 타입으로 사용할 수 없습니다.
     
  • 대신 제네릭이나 불명확 타입(some)과 함께 사용해야 해요.
let c: Container // ❌ 에러

2. 🧬 제네릭 (Generic) — 타입과 함수에서의 추상화

📌 개념

제네릭은 타입을 외부에서 주입받는 형태의 추상화입니다.
함수, 구조체, 클래스 등에서 사용할 수 있습니다. 이건 많이들 접하셨을거 같아요

 
struct Stack<T> {
  var items = [T]()
  mutating func push(_ item: T) {
    items.append(item)
  } 
  mutating func pop() -> T {
    return items.removeLast() 
  } 
}
 
🎯 목적
  • 다양한 타입에 대해 재사용할 수 있는 유연한 코드
  • 성능 최적화에 유리 (정적 디스패치)
  • 타입 안정성 유지

3. 🔒 불명확 타입 (Opaque Type, some) — 타입을 감추되 고정

📌 개념

some 키워드는 내부 구현 타입은 숨기고, 외부엔 프로토콜만 노출하는 방식이에요.
SwiftUI에서 많이 보셨을 겁니다.

 
 
func makeView() -> some View { 
  Text("Hello") 
}
  • 이 함수는 Text를 반환하지만, 외부에서는 View라는 프로토콜만 보입니다.

🎯 목적

  • associatedtype이 있는 프로토콜을 반환할 때 쓸 수 있는 방법
  • 타입을 감추면서도 정적 디스패치를 유지 (성능 좋음)
  • 복잡한 내부 구현을 숨기고 싶을 때 유용 (예: SwiftUI의 View 반환)

4. 🧳 실존 타입 (Existential Type, any) — 다형성을 위한 추상화

📌 개념

실존 타입이란 프로토콜을 타입처럼 사용하는 것입니다.
Swift 5.7부터 any Protocol 문법이 도입되었고, 6부터는 반드시 명시적으로 써야 합니다.

 

protocol Drawable { func draw() }

final class Circle: Drawable { 
  func draw() {
    print("그림 그려용") 
  }
} 

let shape: any Drawable = Circle() shape.draw() // ✅ 동적 디스패치

 

any Drawable은 여러 타입을 하나의 변수로 묶기 위한 런타임 컨테이너입니다.


❗ 비교: 실존 타입 없이 쓴 경우

let shape = Circle() shape.draw() // ✅ 정적 디스패치
  • 이건 shape의 타입이 Circle로 정적으로 확정되기 때문에, 디스패치도 정적으로 이뤄집니다.
  • 성능 최적화가 더 잘 됩니다.

🎯 목적

  • 다형성을 극대화할 수 있음
  • 여러 타입을 하나의 타입처럼 다루고 싶을 때
  • 런타임에서 타입이 결정되는 유연한 코드 작성

⚠️ 특징

  • 동적 디스패치를 사용 → 성능 손해 가능
  • 타입 인라인 최적화가 어려움
  • Swift 6부터는 any 키워드를 반드시 써야 함
  • 실존 타입은 변수 활용에 유연성을 줄 수 있지만, 성능의 아쉬움이 있을 수 있으니 사용시 고려를 해봐야합니다

🧭 네 가지 방식 비교 정리

 

항목 associatedtype 제네릭 opaque Existential
사용 위치 프로토콜 내부 타입/함수 선언 반환 타입 변수/매개변수 등
타입 결정 시점 채택 시 컴파일 시 컴파일 시 런타임
다형성 지원 ✅ 가능 ❌ 제한적 ❌ 제한적 ✅ 완전 지원
디스패치 방식 정적 + 간접 ✅ 정적 ✅ 정적 ❌ 동적 (느림)
타입 감춤
배열 등에 담기 ✅ 가능
 

🧭 언제 어떤 걸 써야 할까?

 

상황 적합한 방식
다양한 타입을 하나의 추상화로 다루고 싶다 associatedtype + Opaque
재사용 가능한 자료구조, 알고리즘이 필요하다 제네릭
프로토콜 반환이 필요한데 associatedtype이 있어서 제한된다 Opaque
여러 타입을 하나의 배열 등에 담고 싶다 Existential
런타임에 타입이 결정돼야 하고 유연성이 필요하다 Existential
성능이 중요하고 타입도 정적이어야 한다 제네릭 또는 Opaque
 

✅ 마치며, 결국, 이 모든 기능의 목적은?

이 모든 기능은 결국 “좋은 소프트웨어 아키텍처”를 만들기 위해 존재한다.

라고 생각합니다.

이런 추상화 기능들은 각각의 방식으로 우리가 더 유연하고, 안전하며, 유지보수가 쉬운 코드를 작성할 수 있게 도와줍니다.

🎯 추상화를 통해 우리가 얻게 되는 것들

  • 유연한 설계: 타입을 고정하지 않고 바꿔 쓸 수 있는 구조
  • 구현과 인터페이스의 분리: 내부 구현을 몰라도 외부에서 쓸 수 있는 캡슐화
  • 타입 안전성: 컴파일 타임에 오류를 잡아낼 수 있는 강력함
  • 재사용성: 반복되는 패턴 없이 다양한 상황에서 활용 가능
  • 결합도 감소: 모듈 간 의존을 줄이고 테스트/확장이 쉬운 구조
  • 테스트 용이성: 추상화 덕분에 mocking, DI가 쉬워짐
  • 성능과 구조 간의 균형 조율: 런타임 유연성과 정적 최적화 사이에서 선택 가능

🧱 아키텍처 관점에서 본 각 기능의 역할

기능 아키텍처적 가치
associatedtype 구현체에 의존하지 않고 유연한 프로토콜 설계 가능
제네릭 다양한 타입에 대해 재사용 가능, 타입 안정성 유지
오파큐 타입 (some) 구현을 감추면서도 성능을 확보 (SwiftUI에서 핵심적으로 사용됨)
실존 타입 (any) 다형성과 런타임 유연성 확보, UI 컴포넌트/플러그인/전략 패턴 등에 유용
 

예전엔 “왜 이렇게 복잡하게 만들었지?” 싶었던 기능들도,
이제는 코드의 구조적인 개선과 실전 유지보수의 편안함을 위해 마련된 장치라는 걸 느끼게 됩니다.

아직 실질적으로 구조화해서 본격적으로 사용할 기회는 많지 않았지만,
앞으로 다가올 상황을 위해 이 개념들을 이해할 필요가 있다고 느꼈습니다.