2026-06-11 (수)
✅ 학습 완료
- Swift Dispatch 메커니즘과 Existential Container 완전 정리
- Existential Container 구조 완벽 이해 (40 bytes = 24 Value Buffer + 8 VWT + 8 PWT)
- VWT (Value Witness Table) 역할: 값 관리 (allocate, copy, destroy)
- PWT (Protocol Witness Table) 역할: Protocol 메서드 호출 (draw, erase)
- Boxing 과정 상세 (Step 1~4: Container 할당 → 값 복사 → VWT 설정 → PWT 설정)
- Unboxing 2가지 케이스: 메서드 호출 / 타입 캐스팅
- "구체 타입 → Existential Container 변환" = Boxing 의미 이해
- Container 저장 위치 (스택 vs 힙)
- Inline vs Non-Inline 저장 방식
- CPU 캐시 효율성과 캐시 미스 (10-30배 속도 차이)
- Array의 특수한 경우 (구조체는 inline, 요소는 힙)
- 🆕 Vtable 상세 설명 (Class 메서드 주소 테이블, Dynamic Dispatch 메커니즘)
- 🆕 Final class의 중요성 (상속 불가 = Static Dispatch 가능)
- 🆕 Private 메서드 최적화 (컴파일러가 Static Dispatch로 최적화)
- 🆕 Dog 타입 선언해도 final 아니면 Dynamic Dispatch
- 🆕 Inline 저장 ≠ Static Dispatch (별개 개념)
- 🆕 Inline의 정확한 정의 (연속 메모리 vs 메모리 점프)
- 🔬 심화 개념 8개 추가 (메모리 구조, 호출 메커니즘)
- 📄 상세 문서 보기 (FAQ 14개 + 심화 개념 8개)
- 문서 작성 가이드라인 표준화
- 템플릿 구조 필수 준수 (Hero, Why, Scope, As-is/To-be, QA Checklist)
- 코드 포맷팅 규칙 (.codebox 사용, 들여쓰기 필수)
- 다이어그램 작성 규칙 (ASCII 금지, HTML 구조화 필수)
- 개발자 실행 중심 작성 (의사결정 트리, 실전 체크리스트)
- 📄 README.md에 상세 가이드 추가
- Sendable Conformance 규칙 완전 정리 (Apple 공식 문서 정독)
- 📖 읽은 출처: Apple Developer — Sendable protocol
- value semantic type의 정의와 Sendable과의 연결 (복사되어 공유 상태 없음)
- Sendable로 표시 가능한 4가지: 값 타입 / mutable 없는 참조 타입 / 내부 접근 관리 참조 타입 / @Sendable 함수·클로저
- Sendable Class 3조건: final · let+Sendable 타입 · 부모 없음 or NSObject
@unchecked Sendable = 컴파일러 검사 끄기 (안전은 개발자 책임, lock/queue 직접)
- 암묵적(implicit) conformance 조건: frozen 이거나, public 아니고 @usableFromInline 아닐 때 (모듈 경계/라이브러리 진화 이유)
@MainActor class가 자동 Sendable인 이유 (메인 액터가 모든 접근을 직렬화)
@Sendable 클로저: by-value 캡처 + 캡처값 Sendable, var는 let으로 복사
- 📄 상세 문서 보기 (Q&A 14개 + 정리표 + 체크리스트)
💡 중요한 포인트
Existential Container 비유:
- 택배 상자 = Existential Container (고정 크기 40 bytes)
- 상자 공간 = Value Buffer (24 bytes)
- "취급 주의" 스티커 = VWT (어떻게 복사/삭제하나?)
- "사용 설명서" 스티커 = PWT (draw() 버튼은 어디?)
작은 물건 (≤ 24 bytes)은 상자에 직접 넣고, 큰 물건 (> 24 bytes)은 창고(힙)에 보관하고 주소표만 넣는다.
📕 오답노트
class에 let만 있으면 Sendable 아닌가? (왜 final이 필요?)
- let만으론 부족: ① let의 타입도 Sendable이어야 하고 ② class는
final이어야 한다
- final이 필요한 이유: class는 상속되므로, 하위 클래스가
var를 추가하고 부모 타입으로 위장해 넘기면 mutable 상태가 도메인을 넘는다
- → final이 상속을 막아 "이 타입의 모든 객체는 불변"을 컴파일러가 확정. struct/enum은 상속이 없어 이 문제 자체가 없다
- non-final class + Sendable = 컴파일 에러 (조심하라는 경고가 아님).
@unchecked Sendable로만 우회
struct도 내부 var면 Sendable 안 되는 거 아냐?
- 아니다! struct/enum은
var가 있어도 Sendable이 된다 (값 타입은 전달 시 복사되어 각자 독립)
- 진짜 조건은 "멤버·associated value의 타입이 Sendable인가" 뿐
let 강제는 class에만 해당 — class는 참조 공유라 불변화가 필요
클로저가 값 바꿀까 봐 @Sendable을 붙이는 건가?
- 방향이 거꾸로: @Sendable은 막는 장치가 아니라 "이 클로저는 도메인 경계를 넘는다"는 표식
- 표식이 붙으면 컴파일러가 위험한 캡처(외부 var 변경, non-Sendable 캡처)를 검사해 막아줌
- 보통
Task.detached 같은 API가 @Sendable을 요구해 자동 적용됨
- class의 내부 var는
let box로 잡아도 참조 복사라 같은 객체 공유 → 못 넘김 (var 값만 let으로 복사하거나 타입을 Sendable로)
Boxing은 런타임에만 발생하는가?
- 컴파일 타임: 컴파일러가 "여기에 Boxing 필요"라고 판단하고 코드 삽입
- 런타임: 실제로 Container에 값을 복사하는 실행
- → 컴파일러가 미리 알고 준비, 실제 실행은 런타임
"구체 타입 → Existential Container 변환"이 무슨 뜻?
- 구체 타입: Circle, Point처럼 실제 타입 이름이 있는 것
- 변환 (= Boxing): Circle (8 bytes) → Container (40 bytes)로 형태 변화
- 타입도
Circle → any Drawable로 바뀜
- → 책상 위의 책을 택배 상자에 넣는 것과 같음
Unboxing은 타입 캐스팅할 때만?
- 아니다! 메서드 호출할 때도 Unboxing 발생
- Case 1:
drawable.draw() → PWT 조회 → 값 꺼내기 → 메서드 실행
- Case 2:
as? Circle → 런타임 타입 체킹 → 값 추출
- → 둘 다 Unboxing, 둘 다 런타임 오버헤드
Existential Container는 항상 힙에?
- 아니다! 기본적으로 스택에 저장됨
- 일반 변수:
let drawable: any Drawable = ... → 스택 ✅
- 클래스 프로퍼티:
class VM { var drawable } → 힙 (클래스가 힙에 있으므로)
- escaping 클로저 캡처 → 힙으로 이동
- → Container 위치 ≠ Value Buffer 값 위치 (별개 개념)
Inline = 함수 인라이닝?
- 아니다! 여기서 Inline은 "직접 저장"이라는 뜻
- Inline 저장: 값 그 자체를 Value Buffer에 직접 넣음 (≤ 24 bytes)
- Non-Inline: 포인터만 저장, 실제 값은 힙에 (> 24 bytes)
- → 함수 인라이닝(코드 삽입)과는 다른 개념
- → 편지봉투에 메모지 직접 넣기 vs 창고 주소표만 넣기
캐시 미스가 뭐길래 10-30배 느려?
- CPU 캐시: 메모리보다 훨씬 빠른 임시 저장소
- 캐시 히트: 데이터가 캐시에 있음 (~1-10 사이클) ✅
- 캐시 미스: 캐시에 없어서 메인 메모리까지 가야 함 (~100-300 사이클) ❌
- → Inline 저장: 연속 메모리 → 한 번에 캐시 로드 → 캐시 히트
- → Non-Inline: 메모리 점프 (스택 → 힙) → 캐시 미스 가능성
Array는 24 bytes 이하니까 inline 저장되면 빠르다?
- 절반만 맞다! Array 구조체는 inline 저장되지만, 요소는 항상 힙에 있음
- Array 구조: buffer pointer (힙 가리킴) + count + capacity ≈ 24 bytes
- 구조체는 Value Buffer에 inline 저장 가능
- 하지만 실제 배열 요소 [1.0, 2.0, 3.0]은 항상 힙에 저장됨
- → 결국 포인터를 따라가야 하므로 Non-Inline처럼 동작
🆕 Vtable이 뭔가?
- Virtual Table = Class의 메서드 주소들을 담은 테이블
- 각 Class마다 자신의 vtable을 가짐 (Animal's vtable, Dog's vtable)
- 런타임에 "어떤 메서드를 호출할지" 결정하는 Dynamic Dispatch 메커니즘
- 호출 과정: 인스턴스의 vtable pointer → vtable에서 메서드 주소 찾기 → 점프
- → 다형성(Polymorphism)을 구현하기 위한 핵심 구조
🆕 let animal: Dog = Dog() 하면 Static Dispatch?
- 아니다! Dog 타입으로 선언해도,
final 아니면 Dynamic Dispatch
- 이유: Dog가 상속 가능 → 서브클래스가 메서드를 오버라이드할 수 있음
- 컴파일러는 안전을 위해 vtable 사용
- Static Dispatch를 원하면:
final class Dog 선언 필수 ✅
- →
final = 상속 불가 = 오버라이드 불가 = 컴파일 타임 확정
🆕 Private 메서드는 항상 Dynamic Dispatch?
- 아니다! 컴파일러가 Static Dispatch로 최적화 가능
private = 이 클래스 안에서만 접근 가능
- 서브클래스에서 오버라이드 불가 → vtable 불필요
- 컴파일러가 "항상 이 구현만 호출된다"고 확신 가능
- → WMO(Whole Module Optimization) 활성화 시 더 적극적 최적화
🆕 Inline 저장되면 Static Dispatch?
- 아니다! Inline 저장 ≠ Static Dispatch (완전히 별개 개념)
- Inline 저장: 메모리 할당 방식 → 캐시 효율성 (메모리 성능)
- Static Dispatch: 메서드 호출 방식 → 컴파일 타임 결정 (호출 성능)
any Protocol은 값이 inline 저장되어도, 메서드 호출은 Dynamic Dispatch
- →
let drawable: any Drawable = Point() → Point는 inline 저장 (메모리 빠름), 하지만 drawable.draw()는 PWT 조회 (메서드 느림)
🆕 Inline의 정확한 정의 (메모리 점프)
- Inline 저장: Value Buffer에 데이터를 직접 저장 (연속된 메모리 주소) ✅
- Container 읽을 때 메모리 점프 없이 바로 데이터 접근 가능
- Non-Inline 저장: Value Buffer에는 힙 포인터만 저장 ❌
- 실제 데이터는 힙의 다른 주소 → 메모리 점프 발생 (Container → 힙)
- → 메모리 점프 = 캐시 미스 가능성 → 10-30배 느림
🔬 "인라인"은 두 가지 완전히 다른 의미!
- A. Inline 저장 (메모리): 연속 메모리에 데이터 직접 저장 → 캐시 효율성
- B. 함수 인라이닝 (최적화): 함수 호출 제거하고 코드 직접 삽입 → 호출 오버헤드 제거
- "Dynamic Dispatch는 인라이닝 불가" ← 여기서 인라이닝 = 함수 인라이닝!
- → 두 개념은 완전히 별개! 문맥에 따라 의미가 다름
🔬 vtable은 왜 데이터 세그먼트에?
- vtable = 함수 주소 배열 (예: [0x3000, 0x2000])
- vtable 자체는 "데이터"이지 "코드"가 아님
- 코드 세그먼트 = 실행 가능한 기계어 코드 (Dog.sound() 코드)
- 데이터 세그먼트 = 전역 변수, 상수, vtable (주소 배열)
- → vtable은 주소 값들을 담은 배열이므로 데이터 세그먼트에 저장
🔬 Struct는 메서드 주소를 가지지 않는다!
- Struct 인스턴스 = 데이터만 (메서드 주소 없음)
dog.bark() → 컴파일러가 call 0x3000으로 변환 (주소 하드코딩)
- 0x3000 = 코드 세그먼트 주소 (Dog.bark() 함수 코드)
- Class는 vtable pointer를 가지지만, Struct는 아무 포인터도 없음
- → Struct 메서드 = 실제로는 전역 함수처럼 동작
🔬 "포함하는 객체에 직접 저장"의 의미
- Struct 프로퍼티: 포함하는 객체 안에 직접 저장 (포인터 없음, 연속 메모리)
- 예:
class VM { var point: Point } → Point가 VM 안에 직접 포함
- Class 프로퍼티: 포인터로 참조 (별도 힙 할당, 메모리 점프)
- 예:
class VM { var dog: Dog } → Dog는 별도 힙에 할당, 포인터만 저장
- → Struct = 값 복사 (연속 메모리), Class = 참조 복사 (포인터)
🔄 다음에 할 일
- 카카오페이 블로그 "문제 해결을 위해 우리가 한 일" 섹션 마저 읽기
- Swift Concurrency race condition 실습 프로젝트 분석