“클래스에 final을 붙이면 빨라진다”는 말을 들어봤을 겁니다. 반쯤 도시전설처럼 도는 이야기인데, 사실 뒤에는 정확한 메커니즘이 있습니다.
이전 글 Swift 심화 #4에서 이어지는 내용입니다.
메서드 호출이라는 당연해 보이는 동작은 실제로 세 가지 다른 방식으로 컴파일됩니다. 어느 방식이 되느냐가 성능과 최적화 가능성을 가르죠.
심화 시리즈, 이번 편은 디스패치(dispatch)입니다.
호출의 세 가지 얼굴 — 직행, 목록표, 메시지
object.doSomething()이라는 코드가 기계어로 번역되는 방식은 세 가지입니다.
정적 디스패치(static dispatch). 컴파일 시점에 어떤 함수인지 확정되어, 함수 주소로 직행하는 호출입니다.
가장 빠르고, 더 중요하게는 컴파일러가 인라이닝(호출 자체를 없애고 본문을 박아넣기)을 할 수 있어서 후속 최적화의 문이 열립니다. struct의 메서드, 전역 함수, final 메서드가 여기 속해요.
테이블 디스패치(table dispatch). 상속과 오버라이드가 가능한 클래스 메서드의 방식입니다.
변수의 컴파일 타임 타입이 Animal이어도 실제 인스턴스는 Dog일 수 있죠. 어떤 구현을 부를지는 실행 시점에 알 수 있습니다.
그래서 클래스마다 가상 함수 목록표(vtable)를 두고, 호출 시 “이 인스턴스의 표에서 몇 번째 함수”를 찾아 점프합니다. 다형성의 대가로 간접 참조 한 단계를 지불하는 거예요.
프로토콜의 경우 witness table이라는 사촌이 같은 역할을 합니다. some·any 편에서 본 그 표가 이겁니다.
메시지 디스패치(message dispatch). Objective-C 런타임의 방식으로, 호출이 objc_msgSend를 통한 이름 기반 조회로 처리됩니다.
셋 중 가장 느리지만, 런타임에 메서드를 바꿔치기하는 것까지 가능한 극한의 유연성이 대가로 주어져요. Swift에서는 @objc dynamic을 붙인 메서드가 이 세계로 갑니다.
objc_msgSend의 내부는 별도 글에서 다뤘으니 여기서는 자리만 잡아둘게요.
결국 유연성과 속도의 교환입니다. 유연할수록(런타임에 결정될수록) 느리고, 고정될수록(컴파일 타임에 결정될수록) 빠르며 최적화가 열립니다.
final과 private이 성능 키워드인 이유
이제 도시전설을 해부할 수 있습니다. 컴파일러가 테이블 디스패치를 정적으로 강등(devirtualization)할 수 있는 조건이 뭘까요.
“이 메서드는 오버라이드될 가능성이 없다”는 증명입니다.
final이 정확히 그 증명입니다. final class나 final func는 “서브클래스에서 재정의될 일 없음”의 선언이죠.
컴파일러는 vtable 조회를 걷어내고 직행 호출로 바꿀 수 있어요. 인라이닝의 문도 함께 열리고요.
private도 비슷한 효과를 냅니다. 파일 밖에서 보이지 않는 선언은 컴파일러가 그 파일 안의 사용처를 전부 볼 수 있으니, 오버라이드가 없다는 걸 스스로 증명할 수 있거든요.
전체 모듈 최적화(WMO, 요즘 릴리스 빌드 기본값)가 켜져 있으면 이 증명의 범위가 모듈 전체로 넓어집니다. internal 클래스도 상속이 없으면 자동으로 final 취급됩니다.
그래서 실무 지침은 이렇게 정리됩니다. 상속 계획이 없는 클래스에는 final을 붙인다.
성능 때문만이 아니라 설계 선언(이 타입은 상속 트리의 잎이다)으로서도 옳고, 컴파일러에게는 최적화 증명서가 됩니다.
다만 “final 붙였더니 앱이 빨라졌어요”급 기대는 금물입니다. 호출 오버헤드는 나노초 단위라, 핫루프 안이 아니면 체감이 없어요.
값진 건 인라이닝 이후에 연쇄되는 최적화들이고, 그건 컴파일러가 알아서 합니다. 우리 일은 증명을 막지 않는 것 정도입니다.
프로토콜 extension의 함정 — 요구사항이냐 아니냐
디스패치 지식이 실무에서 가장 크게 빛나는(정확히는, 몰라서 가장 크게 데이는) 지점이 프로토콜 기본 구현입니다. 퀴즈 하나 볼게요.
protocol Greeter {
func hello() // 요구사항으로 선언됨
}
extension Greeter {
func hello() { print("안녕하세요") }
func bye() { print("안녕히") } // 요구사항에 없음
}
struct Korean: Greeter {
func hello() { print("안녕") }
func bye() { print("잘 가") }
}
let k: any Greeter = Korean()
k.hello() // ?
k.bye() // ?
답은 “안녕”과 “안녕히”입니다. hello는 프로토콜 요구사항이라 witness table을 통해 동적으로 디스패치되고, Korean의 구현이 표에 등록되어 있어요.
반면 bye는 요구사항 목록에 없는 extension 전용 메서드입니다. 프로토콜 타입으로 부르면 정적으로 extension 구현이 직행 호출됩니다. Korean이 뭘 정의했든 상관없이요.
이 규칙을 모르면 “분명 구현했는데 내 코드가 안 불린다”는 미스터리 버그가 됩니다. 처방은 간단해요.
채택자가 갈아끼울 수 있어야 하는 메서드는 반드시 프로토콜 본문에 요구사항으로 선언한다. extension의 기본 구현은 그대로 두되, 선언이 본문에 있어야 표에 자리가 생깁니다.
POP(프로토콜 지향 프로그래밍) 편에서 프로토콜 extension을 상속 대체재로 소개했는데, 그 도구의 안전 수칙이 이것입니다.
계측으로 확인하기 — 감이 아니라 프로파일러
디스패치 이야기의 마무리는 늘 같은 경고여야 합니다. 이건 마이크로 최적화의 영역이고, 순서가 중요합니다.
먼저 Instruments의 Time Profiler로 실제 병목을 찾습니다. 대부분의 성능 문제는 디스패치가 아니라 알고리즘(O(n²) 루프), 불필요한 작업(매 프레임 재계산), I/O에서 나와요.
조기 최적화 경계는 크누스 글에서 다룬 그대로입니다.
프로파일에서 정말 핫루프 안의 동적 디스패치가 잡혔다면, 그때 처방 목록이 열립니다. 해당 타입 final화, any를 some·제네릭으로 교체(특수화 유도), 루프 밖으로 프로토콜 경계 끌어내기 같은 것들요.
거꾸로 말하면, 이 지식의 평소 용도는 최적화가 아니라 설계 이해입니다.
왜 struct가 기본값인지(정적 디스패치 친화), 왜 some이 any보다 권장되는지(특수화 가능), 왜 SwiftUI가 struct 뷰를 쓰는지.
언어의 큰 결정들이 전부 이 층에 뿌리를 두고 있어서, 디스패치를 알면 Swift의 설계가 하나의 그림으로 읽힙니다.
정리
- 메서드 호출은 정적(직행)·테이블(vtable/witness table)·메시지(objc_msgSend) 세 방식으로 컴파일되며, 유연성과 속도가 반비례합니다.
- final·private·WMO는 “오버라이드 없음”의 증명이 되어 동적 호출을 정적으로 강등시키고 인라이닝의 문을 엽니다. 상속 안 할 클래스에는 final이 기본기입니다.
- 프로토콜 extension 메서드는 요구사항 선언 여부에 따라 디스패치가 갈립니다. 갈아끼울 수 있어야 하는 메서드는 반드시 본문에 선언합니다.
- 순서는 프로파일러가 먼저입니다. 디스패치 지식의 평소 가치는 최적화 기교가 아니라 언어 설계를 읽는 눈입니다.
다음 편은 한 층 더 내려갑니다. struct의 크기는 어떻게 정해지는지, 프로퍼티 순서가 메모리를 바꾸는 이유, 그리고 any 상자의 실제 크기까지, 메모리 레이아웃을 다룹니다.
API와 버전 근거는 Swift Optimization Tips에서 2026-08-17에 확인했습니다.
이어서 읽기
Swift 심화 시리즈
- 이전 편: [Swift 심화 #4] 구조적 동시성, Task를 함부로 열면 안 되는 이유
- 이전 편: [Swift 심화 #3] Sendable과 Swift 6 동시성 에러 마이그레이션
- 이전 편: [Swift 심화 #2] Swift actor 완전 정리, 데이터 레이스 막는 법
출처 및 확인 기준
- Swift Optimization TipsSwift 프로젝트 · 공식 문서 · 확인 2026년 8월 17일근거: 정적·동적 디스패치, final·whole-module optimization과 성능 특성

![[Swift 심화 #5] Swift 디스패치 3형제, final이 성능 키워드인 이유 대표 이미지](/assets/images/posts/f974f02c-833c-4ab1-9350-a2e3543e8391/swift-method-dispatch-1.jpg)