← 학습일지(6/4)로 돌아가기
복습 · 2026-06-04 오답노트 심화

POP · 디스패치 복습 2 (Q&A)

6/4 학습일지(모듈 단위 컴파일 / any inline boxing / @inlinable / 디스패치) 복습 문답을 이어가는 문서다. 주인님 이해 + 질문 + 답변을 보존하며 앞으로 6/4 복습은 여기에 누적한다. (6/3 복습은 POP·디스패치·제네릭 복습 (6/3))

복습 Q&A Dispatch / Type Erasure 자주 나옴

값타입 · 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(구체)일 때

protocol A { func a() } extension A { func a() {}; func b() {} } // b는 requirement 아님 struct 스트1: A { func a() {}; func b() {} } let k1: A = 스트1() // existential (프로토콜 타입) let k2: 스트1 = 스트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다.

let drawable: any Drawable = Circle() drawable.draw() // Dynamic Dispatch

호출 과정:

  1. Existential Container에서 PWT 포인터 읽기
  2. PWT에서 draw() 주소 조회
  3. Value Buffer에서 값 꺼내기 (Unboxing)
  4. 조회된 주소로 점프 (간접 호출)

런타임에 PWT 조회 = Dynamic Dispatch

Generic은 Specialization과 별개로 항상 Static Dispatch 아냐?

아니다. Specialization 여부에 따라 달라진다.

상황내부 동작Dispatch
Specialization ✅call Circle.draw (주소 하드코딩)Static
Specialization ❌call pwt.draw(content) (PWT 조회)Dynamic
"컴파일 타임에 T=Circle 안다" ≠ "Static Dispatch"

호출부에서 T=Circle인 건 알아도, 함수 내부에서 content.draw()는 PWT를 통해 호출할 수 있다 (Specialization 안 되면).

Specialization = 같은 모듈에서 함수 구현부와 호출이 있을 때 가능한 거 맞아?

맞다.

상황Specialization
같은 모듈에서 구현 + 호출✅ 가능
다른 모듈에서 호출❌ 불가
다른 모듈 + @inlinable✅ 가능

Specialization 조건:

  1. 함수 본문이 보여야 함 (같은 모듈 or @inlinable)
  2. T가 뭔지 알아야 함 (호출부에서 구체 타입 전달)

→ 둘 다 충족 시 함수 복제 → Static Dispatch

import 모듈하면 함수 구현부 보이는 거 아냐?

아니다. import해도 함수 본문은 안 보인다.

// 모듈 B 컴파일 시 생성되는 것: 1. .swiftmodule 파일 → 인터페이스 (선언부만) public func draw<T: Drawable>(content: T) // 시그니처만 2. 바이너리 (.o, .framework) → 실제 구현 (이미 기계어) // 함수 본문은 이미 컴파일됨

모듈 A 컴파일러 입장:

  • ✅ 아는 것: draw<T: Drawable>(content: T) 시그니처
  • ❌ 모르는 것: draw 함수 안에 뭐가 있는지 (본문)

→ 복제할 코드가 없음 → Specialization 불가 → PWT 전달

@inlinable의 역할은?

함수 본문을 .swiftmodule에 포함시켜 다른 모듈에서도 Specialization 가능하게 함.

// 모듈 B @inlinable public func draw<T: Drawable>(content: T) { content.draw() // ← 이 본문이 .swiftmodule에 포함됨 } // 모듈 A import ModuleB draw(Circle()) // ✅ Specialization 가능!

비유:

  • 모듈 B = 비밀 레시피 보유 (함수 본문)
  • 모듈 A = "Circle 재료로 만들어줘"
  • 레시피 안 보여주면 → 모듈 B에게 부탁 (PWT 전달)
  • @inlinable = 레시피 공개 → 모듈 A가 직접 복제 가능

선언 타입에 따른 Dispatch 정리

선언 타입메서드 호출Dispatch
any DrawableA.k()Dynamic (PWT 조회)
Circle (구체 타입)B.k()Static (직접 호출)
<T: Drawable> + Specialization ✅content.k()Static
<T: Drawable> + Specialization ❌content.k()Dynamic
핵심: Dispatch는 "선언된 변수 타입"과 "Specialization 여부"가 결정한다.

메인 프로젝트에 여러 모듈이 있을 때, 컴파일된 모듈(.a, .framework)이 어떻게 포함돼?

컴파일 단계와 링킹 단계가 분리되어 있다.

// 1. 각 모듈 컴파일 (독립적) ModuleA → ModuleA.swiftmodule (시그니처) + ModuleA.o (바이너리) ModuleB → ModuleB.swiftmodule + ModuleB.o // 2. 메인 프로젝트 컴파일 import ModuleA // .swiftmodule만 읽음 (타입 체크용) import ModuleB → MainApp.o 생성 // 3. 링킹 단계 MainApp.o + ModuleA.o + ModuleB.o + 시스템 라이브러리 → 하나의 실행 파일 (MyApp)
모듈 타입최종 앱에 포함 방식
Static Library (.a)앱 바이너리에 복사
Static Framework앱 바이너리에 복사
Dynamic Framework앱 번들 내 별도 파일로 포함
SPM Package (기본)Static → 앱 바이너리에 복사됨
Specialization 불가 이유 연결:

컴파일 시점에 다른 모듈의 .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 로드

심볼 바인딩이 뭐야?

"함수 이름 → 실제 메모리 주소" 연결하는 것

// 내 앱 코드 import UIKit let view = UIView() // UIView는 UIKit에 있음 // 빌드 타임 (ld) call UIView.init 주소: ???????? // 심볼만 있고 주소는 비어있음 // 런타임 (dyld 심볼 바인딩) 1. UIKit.framework 로드 2. UIView.init 주소 찾음 → 0x7fff1234 3. ???????? → 0x7fff1234 ✅ 채워짐

왜 런타임에? Dynamic Library는 ASLR 보안으로 매번 다른 주소에 로드됨 → 빌드 때 주소 확정 불가

정적 라이브러리는 링킹 때 이미 메모리 주소를 알아?

링킹 때 "상대 주소(오프셋)"가 확정된다.

Static LibraryDynamic Library
코드 위치실행 파일 안에 복사됨별도 파일로 존재
링킹 때오프셋 확정 ✅심볼 이름만 기록
런타임 때바인딩 불필요dyld가 바인딩 필요
// Static Library - 링킹 때 합쳐짐 ┌─────────────────────────┐ │ 실행 파일 │ │ 내 코드: 오프셋 0x0000 │ │ ModuleA: 오프셋 0x1000 │ ← 복사되어 합쳐짐 │ │ │ call ModuleA.foo │ │ → 0x1000으로 점프 ← 링킹 때 확정 └─────────────────────────┘ → 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 — 추가 오버헤드
specialize되면: 직접 호출 + 인라인 + 값 직접 처리로 이 전부 사라짐.

③ 메타데이터·PWT·값을 "값"으로 보내? "주소"로 보내? (address-only가 뭐야)

specialize 안 되면 전부 "주소(포인터)"로 보낸다.

render(&값, // ① 값의 주소 (address-only) Circle_meta, // ② 타입 메타데이터 주소 Circle_PWT) // ③ PWT 주소
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 specialize O → draw() = 정적 (직접+인라인) generic specialize X → draw() = 동적 (PWT 조회) existential any P → draw() = 동적 (PWT, 항상) 구체 타입 let c: Circle → draw() = 정적
두 예외:
  • 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프로토콜 요구사항 구현 주소록(타입,프로토콜) 쌍당 하나
비유: 메타데이터=타입의 설계도("나는 struct, 24바이트, 복사는 VWT 참고") / VWT=취급설명서(복사·폐기) / PWT=메서드 구현 주소록.

인스턴스 주소를 알면 프로토콜도 알지 않아? 뭐하러 PWT를 보내?

struct 인스턴스엔 타입/프로토콜 정보가 없다(순수 데이터). 그래서 주소만으론 모른다.

struct Circle: Drawable { var radius: Double } // 8바이트 let c = Circle(radius: 5) // 메모리엔 radius 값(8바이트)만! "나 Circle이야" 표식 없음
  • 프로토콜 conformance = 그 타입이 약속한 메서드들의 실제 구현 주소(=PWT 내용). 인스턴스 데이터엔 이게 없다 → draw() 부르려면 PWT 필요
인스턴스에 타입 정보가?
struct❌ 없음(순수 데이터) → 메타데이터·PWT 둘 다 따로
class✅ 맨 앞에 isa 포인터(타입 메타데이터) → 타입은 앎. 단 PWT는 별개

왜 인스턴스에 안 넣나: 값 타입은 순수 데이터여야 효율적(복사 쌈·크기 최소). 타입 정보는 인스턴스마다 같으니 중복 저장 말고 호출 시 한 번 인자로 전달. existential(any)은 반대로 container에 함께 담는다.

비유: struct 인스턴스 = 이름표 없는 상자(내용물만). "이건 Circle이고 Drawable로 쓰려면 이 주소록(PWT) 봐"라고 따로 알려줘야 함.

🧰 보조 노트 — 메모리 계층 · 컴파일/링킹 · 심볼 (2026-06-20)

레지스터란? 메모리 계층 순서는?

레지스터 = CPU 코어 안에 내장된 가장 작고 빠른 저장소(~1 사이클). ALU 연산은 레지스터끼리만.

계층속도(대략)크기위치
레지스터~1 사이클수백 바이트CPU 코어 내부
캐시 L1/L2/L3몇~수십 사이클KB~수십 MBCPU 내/근처(SRAM)
주메모리(RAM)~100+ 사이클GB메인보드(DRAM)
디스크(SSD/HDD)µs~msGB~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
static이면 ld가 끝까지, dynamic이면 ld 기록 + 런타임 dyld가 마무리.

심볼(symbol)이란? 메서드 이름 같은 거야?

✅ 대체로 맞다. 심볼 = 링커가 "이름 있는 것"을 식별하는 고유 이름표. 함수 이름이 대표 예. 단:

  • 함수만이 아니라 전역 변수·타입 메타데이터·witness table(PWT/VWT) 등도 심볼
  • 사람이 쓴 이름 그대로가 아니라 mangled name(모듈·타입·시그니처 인코딩) — 오버로딩·제네릭·모듈 구분 위해 충돌 없는 고유 이름
func render<T: Drawable>(_:) in GraphicsKit ↓ name mangling $s11GraphicsKit6render...AA8DrawableRzlF // 실제 심볼 (swift demangle로 복원)

링커는 정의된 심볼 ↔ 미해결 심볼을 같은 이름끼리 매칭해 주소 연결. 비유: 심볼=함수의 주민등록번호(이름은 겹쳐도 심볼은 유일).

심볼은 컴파일 때 생겨? .o에 적혀 있는 거야? (.swiftmodule과 차이)

컴파일 시 생성되어 .o의 심볼 테이블에 기록된다. 두 종류:

종류내용
정의된 심볼(defined)이 .o가 제공이름 + 파일 내 위치(offset)
미해결 심볼(undefined)이 .o가 호출하지만 정의는 딴 데이름만 (주소는 ld가 채움)
MainApp.o 심볼 테이블 (개념) 정의됨 : $s7MainApp6CircleV... // Circle, 여기 있음 미해결 : $s11GraphicsKit6render... // render 호출, 주소 모름 → ld가 채움
파일담는 것누가 언제
.o기계어 + 심볼 테이블 + relocation링커 ld가 링킹 때
.swiftmodule타입·함수 시그니처(인터페이스)swiftc가 타입체크 때
구분: 심볼(링킹용)=.o / 타입 인터페이스(타입체크용)=.swiftmodule. nm MainApp.o로 심볼 확인 가능.

관련 문서