POP · 디스패치 복습 2 (Q&A)
6/4 학습일지(모듈 단위 컴파일 / any inline boxing / @inlinable / 디스패치) 복습 문답을 이어가는 문서다. 주인님 이해 + 질문 + 답변을 보존하며 앞으로 6/4 복습은 여기에 누적한다. (6/3 복습은 POP·디스패치·제네릭 복습 (6/3))
값타입 · type erasure · loose coupling · 디스패치 채점
"값 타입(struct)에도 적용된다 — 복사 의미와 자연스럽게 결합"이 무슨 말?
클래스 상속과 달리 프로토콜은 struct(값 타입)도 채택 가능 → 추상화/다형성을 얻으면서도 값 타입의 복사 의미(value semantics)를 그대로 유지한다.
- 클래스 상속으로 다형성 → 반드시 참조 타입(공유·retain cycle·스레드 문제 동반)
- 프로토콜 → struct 채택 가능 → 복사되어 공유 상태 없음(예측 가능·스레드 안전)과 자연스럽게 결합
→ 추상화를 위해 참조 타입을 강요받지 않는다.
"type erasure는 existential 한계 우회 박스 패턴" — AnyView가 existential보다 효율적이란 뜻?
아니다 — 효율이 아니라 "가능하게 만드는" 문제다. AnyView 자체가 박싱(existential 계열)이고 효율로는 오히려 비용이다.
- 우회하는 한계 = associated type/Self 제약이 있는 프로토콜(PAT)은 그냥
any P로 못 쓰던 문제 (View도associatedtype Body보유) - type erasure = 그걸 구체 struct로 감싸 하나의 타입처럼 쓰게 함 (AnyView, AnySequence, AnyPublisher, AnyHashable)
- 효율 측면: 정적 타입 정보를 지워 오히려 느려짐(AnyView는 SwiftUI diff 비효율). "효율적이라서"가 아니라 "안 되던 걸 되게 하려고" 씀
내 정리 맞아? (some/any, AnyView)
| 정리 | 판정 |
|---|---|
| some, any 모두 프로토콜에 붙인다 | 🔶 대체로 맞음 — 둘 다 "타입 제약(주로 프로토콜)"에 붙임. some은 클래스/Any 제약도 가능하나 실무는 거의 프로토콜 |
| some: 호출자는 구체타입 모르고 컴파일러는 앎 → static | ✅ 정확 |
| any: 모두 모름, unboxing → dynamic, type erase로 여러 타입 묶음 | ✅ 맞음 (any P 자체가 내장 type erasure=existential, 동적은 PWT 통함) |
| AnyView도 타입 이레이즈용 struct? | ✅ 맞음 — View 채택 struct로 감싼 뷰의 구체 타입을 지움 |
프로토콜이 "느슨한 결합(loose coupling)"이라는 게, 클래스에 비해 느슨하다는 게 무슨 말?
구체 타입이 아니라 "계약(추상)"에 의존한다는 뜻. func save(_ r: Repository)는 어떤 구체 타입이 와도 계약만 지키면 됨 → 교체·테스트(mock)·확장 쉬움.
| 클래스 상속 (강한 결합) | 프로토콜 (느슨) | |
|---|---|---|
| 관계 | "is-a" 고정 계층, 단일 상속 | "can-do" 계약, 다중 채택 |
| 물려받는 것 | 부모의 상태+구현(fragile base class) | 구현/상태 강제 없음, 계약만 |
| 타입 | 참조 타입 강제 | 값 타입도 OK |
→ 클래스는 부모 구현 내부에 묶임(강한 결합), 프로토콜은 행동 계약에만 의존(느슨) → DI·테스트·교체 용이.
디스패치 채점 — k1: A(프로토콜), k2: 스트1(구체)일 때
| 호출 | 정답 | 비고 |
|---|---|---|
k1.a() | 동적 | requirement → witness table ✅ |
k1.b() | 정적 | b는 requirement 아님 → A.b 고정 ✅ |
k2.a() | 정적 | 구체 타입이라 직접 호출 (주인님 답 dynamic ❌ 교정) |
k2.b() | 정적 | 구체 타입 → 직접 호출 (주인님 답 동적 ❌ 교정) |
struct을 구체 타입으로 들고 있으면 무조건 정적 디스패치(struct엔 vtable 없음, 구체 타입이라 컴파일러가 함수 확정). witness table(동적)은 프로토콜 타입(existential)으로 볼 때만 등장. → k2는 a()·b() 둘 다 정적, 동적은 k1.a() 하나뿐.
any · Generic · Specialization · 모듈 경계 (6/15 복습)
any는 Existential Container + PWT 사용하니까 Dynamic Dispatch야?
맞다. any는 Dynamic Dispatch다.
호출 과정:
- Existential Container에서 PWT 포인터 읽기
- PWT에서 draw() 주소 조회
- Value Buffer에서 값 꺼내기 (Unboxing)
- 조회된 주소로 점프 (간접 호출)
→ 런타임에 PWT 조회 = Dynamic Dispatch
Generic은 Specialization과 별개로 항상 Static Dispatch 아냐?
아니다. Specialization 여부에 따라 달라진다.
| 상황 | 내부 동작 | Dispatch |
|---|---|---|
| Specialization ✅ | call Circle.draw (주소 하드코딩) | Static |
| Specialization ❌ | call pwt.draw(content) (PWT 조회) | Dynamic |
호출부에서 T=Circle인 건 알아도, 함수 내부에서 content.draw()는 PWT를 통해 호출할 수 있다 (Specialization 안 되면).
Specialization = 같은 모듈에서 함수 구현부와 호출이 있을 때 가능한 거 맞아?
맞다.
| 상황 | Specialization |
|---|---|
| 같은 모듈에서 구현 + 호출 | ✅ 가능 |
| 다른 모듈에서 호출 | ❌ 불가 |
다른 모듈 + @inlinable | ✅ 가능 |
Specialization 조건:
- 함수 본문이 보여야 함 (같은 모듈 or @inlinable)
- T가 뭔지 알아야 함 (호출부에서 구체 타입 전달)
→ 둘 다 충족 시 함수 복제 → Static Dispatch
import 모듈하면 함수 구현부 보이는 거 아냐?
아니다. import해도 함수 본문은 안 보인다.
모듈 A 컴파일러 입장:
- ✅ 아는 것:
draw<T: Drawable>(content: T)시그니처 - ❌ 모르는 것: draw 함수 안에 뭐가 있는지 (본문)
→ 복제할 코드가 없음 → Specialization 불가 → PWT 전달
@inlinable의 역할은?
함수 본문을 .swiftmodule에 포함시켜 다른 모듈에서도 Specialization 가능하게 함.
비유:
- 모듈 B = 비밀 레시피 보유 (함수 본문)
- 모듈 A = "Circle 재료로 만들어줘"
- 레시피 안 보여주면 → 모듈 B에게 부탁 (PWT 전달)
- @inlinable = 레시피 공개 → 모듈 A가 직접 복제 가능
선언 타입에 따른 Dispatch 정리
| 선언 타입 | 메서드 호출 | Dispatch |
|---|---|---|
any Drawable | A.k() | Dynamic (PWT 조회) |
Circle (구체 타입) | B.k() | Static (직접 호출) |
<T: Drawable> + Specialization ✅ | content.k() | Static |
<T: Drawable> + Specialization ❌ | content.k() | Dynamic |
메인 프로젝트에 여러 모듈이 있을 때, 컴파일된 모듈(.a, .framework)이 어떻게 포함돼?
컴파일 단계와 링킹 단계가 분리되어 있다.
| 모듈 타입 | 최종 앱에 포함 방식 |
|---|---|
| Static Library (.a) | 앱 바이너리에 복사됨 |
| Static Framework | 앱 바이너리에 복사됨 |
| Dynamic Framework | 앱 번들 내 별도 파일로 포함 |
| SPM Package (기본) | Static → 앱 바이너리에 복사됨 |
컴파일 시점에 다른 모듈의 .swiftmodule(시그니처)만 보고, .o(본문 바이너리)는 안 본다.
→ 복제할 소스 코드가 없음 → Specialization 불가 → PWT 전달
컴파일 다음에 링킹할 때 dyld가 동작해?
아니다. dyld는 런타임(앱 실행 시)에 동작한다.
| 단계 | 도구 | 역할 |
|---|---|---|
| 컴파일 | swiftc | .swift → .o (오브젝트 파일) |
| 링킹 (빌드 타임) | ld (Static Linker) | .o + .a → 실행 파일 |
| 로딩 (런타임) | dyld (Dynamic Linker) | Dynamic Library 로드 + 심볼 바인딩 |
• ld = 빌드 타임, Static Library 합침
• dyld = 런타임, Dynamic Library 로드
심볼 바인딩이 뭐야?
"함수 이름 → 실제 메모리 주소" 연결하는 것
왜 런타임에? Dynamic Library는 ASLR 보안으로 매번 다른 주소에 로드됨 → 빌드 때 주소 확정 불가
정적 라이브러리는 링킹 때 이미 메모리 주소를 알아?
링킹 때 "상대 주소(오프셋)"가 확정된다.
| Static Library | Dynamic Library | |
|---|---|---|
| 코드 위치 | 실행 파일 안에 복사됨 | 별도 파일로 존재 |
| 링킹 때 | 오프셋 확정 ✅ | 심볼 이름만 기록 |
| 런타임 때 | 바인딩 불필요 | dyld가 바인딩 필요 |
🔁 Generic Specialization 한 바퀴 (2026-06-20 복습)
모듈 경계 → PWT 비용 → address-only → 컴파일타임 vs 런타임 → 동적 디스패치까지, 헷갈리는 7가지를 한 번에.
① 모듈 경계 넘는 generic은 specialize 막힘 — 일반(non-generic) 함수도 마찬가지 아냐?
inlining 관점은 같고, specialization 관점은 다르다.
- Specialization: T별 전용 함수 생성 — generic 전용 개념. 일반 함수엔 "특수화할 T"가 없음
- Inlining: 본문 펼치기 — generic·일반 공통. cross-module이면 둘 다 막힘
| cross-module + 본문 없음 | 결과 |
|---|---|
| Generic 함수 | specialize ❌ + inline ❌ → PWT 경유 추상 호출 (손해 큼) |
| 일반 함수 | inline ❌만 → 직접 static call (정상, 손해 미미) |
일반 함수는 추상화 비용이 0이라 인라인만 못 할 뿐 정상 호출. generic이 cross-module에서 특히 손해.
② specialize 안 되면 PWT 넘기고 PWT로 조회 — 그 조회가 오래 걸린단 뜻?
조회 시간 자체보다 그 간접성의 연쇄가 진짜 비용.
- 인라인 불가 → 후속 최적화 전부 막힘 (상수전파·데드코드 제거 등) — 가장 큼
- 값을 address-only(간접)로 다루고 VWT로 copy/destroy — 추가 오버헤드
③ 메타데이터·PWT·값을 "값"으로 보내? "주소"로 보내? (address-only가 뭐야)
specialize 안 되면 전부 "주소(포인터)"로 보낸다.
| loadable (Int) | address-only (generic T) | |
|---|---|---|
| 값을 어디서 | 레지스터/스택 직접 | 메모리에 둔 채 주소로만 |
| 함수에 넘길 때 | 값 자체(by value) | 값의 주소 |
address-only = "레지스터에 직접 못 올리고 항상 메모리 주소로만 다뤄야 하는 값". 이유 = 컴파일러가 T의 구체 크기를 모름. 메타데이터·PWT는 타입당 하나 있는 정적 테이블이라 그 주소만 넘김.
④ 모듈 배치 3시나리오 — 언제 specialize 되나 (핵심: render 본문을 호출처가 보나)
| 시나리오 | render 위치 | @inlinable | 본문 보임? | 결과 |
|---|---|---|---|---|
| 1 | 다른 모듈 | ❌ | ❌ | specialize 불가 주소 3개·간접 |
| 2 | 같은 모듈 | — | ✅ | specialize 값 직접·인라인 |
| 3 | 다른 모듈 | ✅ | ✅ | specialize 값 직접·인라인 |
다른 모듈은 .swiftmodule(시그니처)만 보이고 .o(본문 바이너리)는 안 보임 → 복제할 소스 없음 → 불가. @inlinable이 본문을 swiftmodule에 공개해 해결. (단 PWT는 conformance 있는 모듈이 생성)
⑤ "제네릭은 호출 시 구체 타입을 안다"고 배웠는데? — 컴파일타임 vs 런타임
맞다. 단 "언제·어떻게" 아느냐가 다르다. 호출처는 항상 T를 안다. 차이는 render 본문이 그 정보를 받는 방식:
| 언제 T를 아나 | 어떻게 | 성능 | |
|---|---|---|---|
| Specialize | 컴파일 타임 | 본문에 박힘(치환) | 정적·인라인 |
| 추상 호출 | 런타임 | 메타데이터·PWT로 | 간접·인라인 X |
둘 다 결국 "구체 타입을 안다"가 맞다. "크기를 모른다"는 건 un-specialized 본문을 컴파일하는 시점 얘기 — 런타임엔 메타데이터 VWT로 크기·복사법까지 다 앎. 동작은 항상 올바름, 최적화만 못 함.
⑥ specialize는 컴파일 때 그 함수를 "실행"해보는 거야?
❌ 컴파일러는 함수를 실행하지 않는다. 컴파일=번역이지 실행이 아님.
- Specialize = 소스를 보고
T자리에Circle을 기계적으로 대입(치환) → 전용 기계어 생성. "코드 복제+치환"이지 실행 아님 - Un-specialize = 추상 기계어 하나 + 런타임 메타데이터로 처리
[T]에 "Circle"을 채워 전용 문서를 인쇄하는 것(specialize). 문서를 "실행"하는 게 아님. (제한적 constant folding 2+3→5은 별개 최적화)⑦ specialize 안 하면 동적 디스패치야?
✅ 프로토콜 요구사항 메서드 호출은 witness table(PWT) 동적 디스패치.
- generic 함수
render자체의 호출은 항상 정적. 동적인 건 본문 안의 프로토콜 요구사항 호출(shape.draw()) - 프로토콜 extension-only 메서드(요구사항 아님)는 specialize와 무관하게 정적(PWT에 없어 제약 타입 기준 결정) — extension dispatch 함정
➕ Generic 추가 — 메타데이터 · 인스턴스엔 타입 정보가 없다 (2026-06-20)
메타데이터(type metadata)가 뭐야? 넘기는 3개 중 하나라며?
specialize 안 된 generic 호출 시 값 포인터 · 메타데이터 포인터 · PWT 포인터 3개를 넘긴다(전부 주소). 메타데이터 = 런타임에 "이 타입이 어떤 타입인지" 담은 구조체(타입당 하나).
| 넘기는 것 | 무엇 | 왜 |
|---|---|---|
| 값 포인터 | 인스턴스(값)의 주소 | 실제 데이터 |
| 메타데이터 포인터 | 타입 정보(크기·정렬·VWT) | 값을 메모리에서 다루려고 |
| PWT 포인터 | 프로토콜 메서드 구현 주소록 | draw() 등 호출하려고 |
메타데이터에 든 것: 종류(struct/class/enum) · size/alignment/stride · VWT 포인터(복사·이동·해제) · (class면) superclass·vtable · 타입 이름.
| 무엇 | 단위 | |
|---|---|---|
| 타입 메타데이터 | 타입 전반 정보(크기·VWT 등) | 타입당 하나 (프로토콜 무관) |
| VWT | 값 생명주기(copy/move/destroy) | 메타데이터에 연결 |
| PWT | 프로토콜 요구사항 구현 주소록 | (타입,프로토콜) 쌍당 하나 |
인스턴스 주소를 알면 프로토콜도 알지 않아? 뭐하러 PWT를 보내?
struct 인스턴스엔 타입/프로토콜 정보가 없다(순수 데이터). 그래서 주소만으론 모른다.
- 프로토콜 conformance = 그 타입이 약속한 메서드들의 실제 구현 주소(=PWT 내용). 인스턴스 데이터엔 이게 없다 →
draw()부르려면 PWT 필요
| 인스턴스에 타입 정보가? | |
|---|---|
| struct | ❌ 없음(순수 데이터) → 메타데이터·PWT 둘 다 따로 |
| class | ✅ 맨 앞에 isa 포인터(타입 메타데이터) → 타입은 앎. 단 PWT는 별개 |
왜 인스턴스에 안 넣나: 값 타입은 순수 데이터여야 효율적(복사 쌈·크기 최소). 타입 정보는 인스턴스마다 같으니 중복 저장 말고 호출 시 한 번 인자로 전달. existential(any)은 반대로 container에 함께 담는다.
🧰 보조 노트 — 메모리 계층 · 컴파일/링킹 · 심볼 (2026-06-20)
레지스터란? 메모리 계층 순서는?
레지스터 = CPU 코어 안에 내장된 가장 작고 빠른 저장소(~1 사이클). ALU 연산은 레지스터끼리만.
| 계층 | 속도(대략) | 크기 | 위치 |
|---|---|---|---|
| 레지스터 | ~1 사이클 | 수백 바이트 | CPU 코어 내부 |
| 캐시 L1/L2/L3 | 몇~수십 사이클 | KB~수십 MB | CPU 내/근처(SRAM) |
| 주메모리(RAM) | ~100+ 사이클 | GB | 메인보드(DRAM) |
| 디스크(SSD/HDD) | µs~ms | GB~TB | 저장장치 |
- 변수·연산은 결국 레지스터에서 처리. 컴파일러가 자주 쓰는 값을 레지스터에 배치, 부족하면 스택(RAM)으로 spill
- RAM 읽을 때 캐시 거침 — 히트면 빠르고 미스면 RAM(느림). locality 원리
- generic 연결: Int(loadable)=레지스터 직접(빠름) / address-only(generic T)=메모리에 두고 주소로(느림) → specialize 이득
타입 체킹은 컴파일러, 함수 연결은 ld가 하나?
✅ 맞다. 단계를 나누면:
- ① 컴파일(swiftc): import 모듈의
.swiftmodule(시그니처)로 타입 체크..o생성 시 외부 함수 호출은 심볼 이름만 적고 주소는 비움(미해결) - ② 링킹(링커
ld): 모든.o+라이브러리 모아 미해결 심볼의 실제 주소를 채움(resolution+relocation)
| 라이브러리 종류 | 누가 함수를 연결 |
|---|---|
| Static (.a) | ld가 컴파일 타임에 코드 복사+주소 채움 |
| Dynamic (.dylib/framework) | ld는 기록만 → 실제 바인딩은 런타임 dyld |
ld가 끝까지, dynamic이면 ld 기록 + 런타임 dyld가 마무리.심볼(symbol)이란? 메서드 이름 같은 거야?
✅ 대체로 맞다. 심볼 = 링커가 "이름 있는 것"을 식별하는 고유 이름표. 함수 이름이 대표 예. 단:
- 함수만이 아니라 전역 변수·타입 메타데이터·witness table(PWT/VWT) 등도 심볼
- 사람이 쓴 이름 그대로가 아니라 mangled name(모듈·타입·시그니처 인코딩) — 오버로딩·제네릭·모듈 구분 위해 충돌 없는 고유 이름
링커는 정의된 심볼 ↔ 미해결 심볼을 같은 이름끼리 매칭해 주소 연결. 비유: 심볼=함수의 주민등록번호(이름은 겹쳐도 심볼은 유일).
심볼은 컴파일 때 생겨? .o에 적혀 있는 거야? (.swiftmodule과 차이)
✅ 컴파일 시 생성되어 .o의 심볼 테이블에 기록된다. 두 종류:
| 종류 | 뜻 | 내용 |
|---|---|---|
| 정의된 심볼(defined) | 이 .o가 제공 | 이름 + 파일 내 위치(offset) |
| 미해결 심볼(undefined) | 이 .o가 호출하지만 정의는 딴 데 | 이름만 (주소는 ld가 채움) |
| 파일 | 담는 것 | 누가 언제 |
|---|---|---|
.o | 기계어 + 심볼 테이블 + relocation | 링커 ld가 링킹 때 |
.swiftmodule | 타입·함수 시그니처(인터페이스) | swiftc가 타입체크 때 |
.o / 타입 인터페이스(타입체크용)=.swiftmodule. nm MainApp.o로 심볼 확인 가능.관련 문서
- POP · 디스패치 · 제네릭 복습 (6/3) — 1편(witness table·zero-cost·@inlinable·thunk 등)
- Protocol Oriented Programming · Swift Dispatch & Existential Container
- SwiftUI some View · any / some / Generic 쉽게