KVO(Key-Value Observing)는 쓰는 법만 보면 단순합니다. addObserver로 등록하면 프로퍼티가 바뀔 때마다 알림이 옵니다.
관련 글 Objective-C 메서드 스위즐링(Method Swizzling) 완벽 정리에서 배경 개념과 이어지는 적용 사례를 함께 확인할 수 있습니다.
그런데 이상하지 않나요? 프로퍼티 setter에 알림 코드를 넣은 적이 없는데, 런타임은 값이 바뀐 걸 어떻게 알까요?
답은 과감합니다. 옵저버를 붙이는 순간, 런타임이 그 객체의 클래스를 몰래 바꿔치기합니다.
이 기법의 이름이 isa-swizzling입니다. 지난 글의 메서드 스위즐링이 연결표의 “줄”을 바꾸는 일이었다면, 이번엔 객체가 소속된 “클래스” 자체를 갈아끼우는 이야기입니다.
addObserver 순간에 벌어지는 일
Person 객체에 옵저버를 붙이면 런타임은 다음을 수행합니다.
NSKVONotifying_Person이라는 서브클래스를 동적으로 생성합니다- 관찰 대상 프로퍼티의 setter를 오버라이드합니다 — 변경 전후에 알림 코드를 끼워 넣은 버전으로
- 그 객체의 isa 포인터를 새 서브클래스로 교체합니다
이후 person.name = @"Kim"을 실행하면 호출 흐름이 이렇게 됩니다.
// NSKVONotifying_Person이 오버라이드한 setter의 의사 코드
- (void)setName:(NSString *)name {
[self willChangeValueForKey:@"name"];
[super setName:name]; // 원래 setter 실행
[self didChangeValueForKey:@"name"]; // 여기서 옵저버에게 통지
}
객체의 메모리 내용은 그대로인데 소속 클래스만 바뀌었으니, 기존 코드는 아무것도 눈치채지 못합니다. 마지막 옵저버가 제거되면 isa는 원래 클래스로 돌아갑니다.
클래스가 거짓말을 한다: class vs object_getClass
여기서 재미있는 디테일이 나옵니다. 옵저버가 붙은 객체에게 클래스를 물어보면 답이 두 가지로 갈립니다.
[person class]; // Person — 거짓말
object_getClass(person); // NSKVONotifying_Person — 진실
런타임은 동적 서브클래스에서 -class 메서드까지 오버라이드해서 원래 클래스를 답하도록 만들어 둡니다. 개발자가 “내 객체 클래스가 왜 이상한 이름이지?“라고 혼란에 빠지지 않게 하려는 배려이자, 구현 세부를 숨기려는 설계입니다.
object_getClass()는 isa 포인터를 직접 읽기 때문에 진짜 클래스가 보입니다. 디버깅 중 두 값이 다르다면, 그 객체는 지금 누군가에게 관찰당하고 있다는 뜻입니다.
setter를 거치지 않으면 알림도 없다
isa-swizzling의 원리를 알면 KVO의 유명한 제약이 당연해집니다.
person.name = @"Kim"; // 알림 발생 — 오버라이드된 setter 경유
person->_name = @"Kim"; // 알림 없음 — ivar 직접 수정, setter를 안 거침
KVO 알림의 진원지는 오버라이드된 setter입니다. ivar를 직접 수정하면 그 경로를 우회하니 아무 일도 일어나지 않습니다. Objective-C에서 프로퍼티 접근에 점 문법(내부적으로 setter 호출)을 권장하는 실질적인 이유 중 하나입니다.
setter 없이 값을 바꿔야 하는 상황이라면 수동 알림으로 감싸면 됩니다.
[self willChangeValueForKey:@"name"];
_name = @"Kim";
[self didChangeValueForKey:@"name"];
반대로 특정 프로퍼티의 자동 알림을 끄고 싶다면 automaticallyNotifiesObserversForKey:에서 NO를 리턴하면 됩니다.
실무에서 조심할 것들
관찰 수명 관리는 여전히 중요합니다. 블록 기반 API가 반환한 NSKeyValueObservation 토큰을 관찰이 필요한 동안 보관해야 합니다(Apple KVO 문서).
관찰을 끝낼 때 토큰을 무효화하거나 해제하면 등록도 정리됩니다. 문자열 기반 API를 유지한다면 addObserver와 removeObserver의 수명을 직접 맞춰야 합니다(Apple KVO 문서).
KVO와 메서드 스위즐링을 같은 클래스에 섞으면 순서 문제가 생깁니다. 스위즐링 후 KVO가 붙으면 동적 서브클래스가 스위즐링된 setter를 감싸지만 반대 순서면 서로의 가정이 어긋날 수 있습니다.
런타임을 만지는 기법 두 개가 한 객체에서 만나면 디버깅 난도가 곱해집니다.
Swift에서는 @objc dynamic이 필수입니다.
class Person: NSObject {
@objc dynamic var name: String = ""
}
let observation = person.observe(\.name, options: [.new]) { _, change in
print(change.newValue ?? "")
}
isa-swizzling이 작동하려면 setter 호출이 Objective-C 메시지 경로로 흘러야 하기 때문입니다.
Swift 4부터는 블록 기반 observe(_:options:changeHandler:) API를 씁니다. 반환된 NSKeyValueObservation 토큰이 해제될 때 등록 해제까지 자동으로 처리해 줍니다. Using Key-Value Observing in Swift
덕분에 문자열 keyPath 시절의 사고 대부분이 이 구조 하나로 막힙니다.
정리
- KVO는 옵저버 등록 순간 NSKVONotifying_ 서브클래스를 동적 생성해 isa를 바꿔치기합니다
- 알림의 진원지는 오버라이드된 setter — willChange/didChange가 끼워진 버전입니다
-class는 원래 클래스를 답하도록 조작돼 있고, 진짜는object_getClass()로 보입니다- ivar 직접 수정은 setter를 우회하므로 알림이 나가지 않습니다 — 필요하면 willChange/didChange로 수동 통지합니다
- Swift에서는
@objc dynamic+ 블록 기반 observe API가 표준이고, 토큰 해제가 곧 등록 해제입니다
메서드 스위즐링과 isa-swizzling까지 보면 Objective-C 런타임의 그림이 잡힙니다. 메서드 연결표도, 객체의 소속 클래스도 전부 런타임에 바꿀 수 있는 데이터라는 것 — 이 유연함이 Cocoa 프레임워크 마법 대부분의 원천입니다.
API와 버전 근거는 Using Key-Value Observing in Swift에서 2026-08-17에 확인했습니다.
이어서 읽기
- Objective-C 메서드 스위즐링(Method Swizzling) 완벽 정리
- +load vs +initialize: 호출 시점과 상속 함정
- Objective-C 카테고리 vs Swift extension 차이 (개념·주의점 총정리)
출처 및 확인 기준
- Using Key-Value Observing in SwiftApple · 공식 문서 · 확인 2026년 8월 17일근거: Swift KVO 등록, 변경 알림과 observation token 생명주기

