메서드 스위즐링 코드는 왜 하나같이 +load 안에 들어 있을까요? 비슷해 보이는 +initialize에 넣으면 안 되는 걸까요?
+load와 +initialize는 둘 다 “클래스가 준비될 때 한 번 불리는 메서드”처럼 보이지만 호출 시점부터 호출 방식, 상속 규칙까지 전부 다릅니다. Objective-C 면접 단골 주제이기도 하고, 잘못 이해하면 “왜 이 코드가 두 번 실행되지?” 하고 헤매기 쉬운 지점이라 한 번쯤 정리해 둘 만합니다.
한눈 비교표
+load |
+initialize |
|
|---|---|---|
| 호출 시점 | 클래스가 런타임에 로드될 때 (main 이전) | 클래스가 첫 메시지를 받기 직전 (lazy) |
| 호출 방식 | 함수 포인터 직접 호출 | objc_msgSend 경유 |
| 카테고리 | 클래스 것과 카테고리 것 모두 각각 호출 | 카테고리 구현이 클래스 구현을 덮어씀 |
| 상속 | 자기가 구현한 클래스만 호출됨 | 상속됨 — 서브클래스 때문에 여러 번 불릴 수 있음 |
| 안 쓰이면? | 그래도 호출됨 | 클래스를 안 쓰면 영영 호출 안 됨 |
표만 봐서는 감이 안 오는 부분을 하나씩 풀어보겠습니다.
+load: main보다 먼저, 무조건
+load는 해당 클래스(또는 카테고리)가 담긴 바이너리가 런타임에 로드되는 시점, 즉 main 함수가 실행되기도 전에 호출됩니다. 앱에서 그 클래스를 한 번도 안 써도 호출됩니다.
호출 방식이 특이합니다. objc_msgSend를 거치지 않고 함수 포인터로 직접 부릅니다. 그래서 일반적인 오버라이드 규칙이 적용되지 않습니다.
- 서브클래스가
+load를 구현 안 했다고 부모 것이 대신 불리지 않습니다 - 클래스의
+load와 카테고리의+load가 둘 다 각각 호출됩니다 — 덮어쓰기가 아닙니다 - 순서 보장: 부모 클래스의
+load가 자식보다 먼저, 클래스의+load가 카테고리보다 먼저
이 성질들이 스위즐링과 정확히 맞물립니다. 카테고리에서 +load를 구현해도 원본 클래스의 +load를 방해하지 않고 앱 수명에서 가장 이른 시점에 확실히 한 번 실행되니까요.
대신 대가가 있습니다. +load는 앱 시작 시간에 그대로 청구됩니다. 모든 클래스의 +load가 main 이전에 순차 실행되므로, 여기서 무거운 일을 하면 첫 화면이 늦게 뜹니다. 애플이 수년째 “가급적 +load를 피하라”고 안내하는 이유입니다. 실제로 +load 안에서는 self가 속한 이미지 밖의 다른 클래스가 아직 로드되지 않았을 수 있어 할 수 있는 일도 제한적입니다.
+initialize: 첫 메시지 직전에, 게으르게
+initialize는 정반대 전략입니다. 클래스가 첫 메시지를 받기 직전에 런타임이 호출해 줍니다. 그 클래스를 앱에서 한 번도 안 쓰면 영영 호출되지 않습니다. 시작 시간에 부담이 없는, 게으른(lazy) 초기화 지점입니다.
여기는 objc_msgSend를 경유하기 때문에 일반 메서드처럼 상속 규칙이 적용됩니다. 바로 이 지점에서 그 유명한 함정이 나옵니다.
@implementation Animal
+ (void)initialize {
NSLog(@"initialize: %@", self);
}
@end
@interface Dog : Animal
@end
@implementation Dog
@end
Dog에 첫 메시지를 보내면 로그가 이렇게 찍힙니다.
initialize: Animal
initialize: Dog
Animal의 +initialize가 두 번 실행됩니다. 한 번은 Animal 자신 몫, 한 번은 +initialize를 구현하지 않은 Dog가 부모 구현을 물려받아 실행한 몫입니다. 그래서 +initialize의 관례적 구현엔 클래스 체크가 들어갑니다.
+ (void)initialize {
if (self == [Animal class]) {
// 진짜 Animal 몫의 초기화만 여기서
}
}
참고로 +initialize는 런타임이 클래스 단위로 락을 잡고 불러주기 때문에 그 자체로 스레드 안전합니다. dispatch_once를 겹쳐 쓸 필요는 없습니다.
실무 선택 기준
판단 기준은 간단합니다.
- 스위즐링, 클래스 등록처럼 “무조건, 가장 먼저” 필요한 일 →
+load(단, 최소한으로) - 그 클래스를 쓸 때만 필요한 준비 작업 →
+initialize(self 체크 필수) - 대부분의 초기화 → 사실 둘 다 아니고,
dispatch_once싱글톤이나 lazy 프로퍼티로 충분합니다
Swift에는 이 고민 자체가 없습니다. Swift는 +load에 해당하는 것을 아예 제공하지 않습니다. 전역 실행 코드를 main 전에 끼워 넣는 공식 통로 자체가 없습니다. 대신 타입 프로퍼티(static let)가 언어 차원에서 lazy + 스레드 안전을 보장하므로 +initialize의 역할을 대체합니다. 앱 시작 성능 관점에서 Swift가 구조적으로 유리한 지점 중 하나입니다.
정리
+load는 main 이전, 무조건, 함수 포인터 직접 호출 — 클래스와 카테고리 것이 각각 다 불립니다+initialize는 첫 메시지 직전, lazy, msgSend 경유 — 안 쓰는 클래스에선 영영 안 불립니다+initialize는 상속 때문에 여러 번 실행될 수 있어if (self == [MyClass class])체크가 관례입니다- 스위즐링이
+load에 사는 이유: 가장 이른 시점 + 카테고리 독립 호출 + 확실한 1회 실행 +load남발은 앱 시작 시간을 직접 늦춥니다 — 무거운 초기화는 lazy로 미루는 게 정답입니다
런타임 시리즈의 다른 글(objc_msgSend, 메시지 포워딩, 스위즐링, KVO)과 겹쳐 보면, +load와 +initialize의 차이가 결국 “msgSend를 거치느냐”라는 하나의 축에서 갈라진다는 게 보입니다. 호출 경로를 이해하면 규칙을 외울 필요가 없어집니다.
