Key-Value Observing(KVO) in Swift
안녕하세요 :) Zedd입니다.
오늘은 KVO에 대해서 공부!
# KVO
- Key-Value Observing의 약자
- 객체의 프로퍼티의 변경사항을 다른 객체에 알리기 위해 사용하는 코코아 프로그래밍 패턴
- Model과 View와 같이 논리적으로 분리된 파트간의 변경사항을 전달하는데 유용함
- NSObjec를 상속한 클래스에서만 KVO를 사용할 수 있음.
그럼 예제를 통해 보겠습니다.
Key-Value Coding(KVC) / KeyPath in Swift에서 사용한 예제를 가져와볼게요.
# Observing을 위한 Setup
class Address {
var town: String
init(town: String) {
self.town = town
}
}
Zedd : 나는 town이 변경하는 걸 다른 객체에게 알리고싶어!
라고 했을 때 해야하는 작업이 2가지 있습니다.
1. NSObjec 상속 ➞ NSObjec를 상속한 클래스에서만 KVO를 사용할 수 있기 때문 ➞ 상속을 해야하므로 class에서만 사용 가능
2. observe하려는 프로퍼티에 @objc attribute와 dynamic modifier를 추가해야합니다.
class Address: NSObject {
@objc dynamic var town: String
init(town: String) {
self.town = town
}
}
자 이렇게 해줬습니다.
# Observer 정의
내가 observe하려는 프로퍼티가 변경되는지 봐야할거아니에요?
그래서 observer가 필요합니다.
var address = Address(town: "어쩌구")
address.observe(\.town, options: [.old, .new]) { (object, change) in
print(change.oldValue, change.newValue)
}
저번시간에 배운 KeyPath를 사용하여 프로퍼티 KeyPath에 observer를 추가할 수 있습니다. (\.town)
그럼 address의 town값을 변경해보겠습니다.
var address = Address(town: "어쩌구")
address.observe(\.town, options: [.old, .new]) { (object, change) in
print(change.oldValue, change.newValue) // Optional("어쩌구") Optional("바보")
}
address.town = "바보"
1. town을 바보로 바꿈
2. 프로퍼티에 변경사항이 생김
3. observer의 change handler가 호출됨
4. handler내에서 oldValue와 newValue를 가져올 수 있음
그래서
Optional("어쩌구") Optional("바보")
가 출력되는 것을 볼 수 있어요.
address.observe(\.town, options: [.old, .new]) { (object, change) in
print(change.oldValue, change.newValue) // ✔️ Optional("어쩌구") Optional("바보")
}
지금 options에 이렇게 [.old, .new]가 추가되어있는 것을 볼 수 있는데요.
만약 변경사항이 필요하지 않으면 options를 안주면 됩니다.
var address = Address(town: "어쩌구")
address.observe(\.town) { (object, change) in
print(change.oldValue, change.newValue) // ✔️ nil nil
}
address.town = "바보"
그럼 이렇게 nil이 출력되게 됩니다.
options에는
- old
- new
- initial
- prior
이렇게 4가지의 값이 들어갈 수 있습니다.
old와 new는 봤으니 넘어가고
[initial]
initial은 초기화시에도 이 observer handler를 호출할거냐...라고 보면 될 것 같습니다.
var address = Address(town: "어쩌구")
address.observe(\.town, options: [.old, .new]) { (object, change) in
print(change.oldValue, change.newValue) // Optional("어쩌구") Optional("바보")
}
address.town = "바보"
우리가 초기값을 어쩌구로 줬는데, 이제 town을 바보로 변경했을 때 handler가 불리는데요,
나는 초기값에 대해서도 handler가 불리게 하고싶다!고 하면 initial을 추가해주면 됩니다.
var address = Address(town: "어쩌구")
address.observe(\.town, options: [.old, .new, .initial]) { (object, change) in
print(change.oldValue, change.newValue)
// nil Optional("어쩌구")
// Optional("어쩌구") Optional("바보")
}
address.town = "바보"
이렇게요! 어쩌구는 newValue로 들어갔네요.
[prior]
이전의 상태와 지금의 상태를 다 주는(?) 옵션이라고 볼 수 있습니다.
var address = Address(town: "어쩌구")
address.observe(\.town, options: [.old, .new, .prior]) { (object, change) in
print(change.oldValue, change.newValue)
// Optional("어쩌구") nil
// Optional("어쩌구") Optional("바보")
}
address.town = "바보"
자..initial이 없으니 따로 초기화할 때 handler가 불리지 않습니다.
바보로 바꿔준 순간 handler가 호출이 되는데요 위에서 봤을 때처럼
Optional("어쩌구") Optional("바보")
이렇게만 나오는게 아니라
// Optional("어쩌구") nil
// Optional("어쩌구") Optional("바보")
이렇게 2개가 호출되네요.
prior. 즉 "이전"이라는 말답게..이전의 상태도 같이 준다는 것을 알 수 있습니다.
initial이 된 순간 어쩌구는 oldValue로 취급되고 newValue는 없는 상태였죠.
프로터티가 변경된 순간 newValue는 바보가 됩니다.
이해하셨나요!?
handler의 파라미터로 change말고 제가 object라고 해준것도 있었는데요.
object에는 현재 address의 town값이 들어가게 됩니다.
그래서 현재 바보로 바뀌었으니 && prioir 옵션이 함께 있으니
이전값인 어쩌구와
현재값인 바보가 함께 출력되는 것을 볼 수 있습니다.
만약 이게 이전값인지..현재값인지 알고싶다면
change의 isPrior 프로퍼티를 통해 확인하면 됩니다.
true면 이전값, false면 현재값입니다.
KVO에 대한 설명은 끝났습니다.
KVO를 보시고 다음과 같은 궁금증이 생길 수 있습니다.
Q : oldValue, newValue..? 프로퍼티 옵저버(willSet, didSet)랑 비슷하군..
class Address: NSObject {
var town: String {
willSet { print(newValue) }
didSet { print(oldValue) }
}
init(town: String) {
self.town = town
}
}
A : 네 KVO와 비슷한건 맞는데, 위 코드 처럼 프로퍼티 옵저버는 타입 정의 내부에 위치해야 하는 반면,
KVO는 타입 정의 외부에서 obsever를 추가할 때 사용하는 거라고 보면 됩니다!
# KVO의 장점과 단점
[장점]
1. 두 객체간의 동기화를 달성가능. 위에서 언급했듯이 Model과 View와 같이 논리적으로 분리된 파트간의 변경사항을 전달 ➞ 동기화 가능
2. 객체의 구현을 변경하지 않고 내부 객체의 상태 변화에 대응할 수 있음. (SDK객체의 경우)
장점을 여기를 참고하는 중인데..꽤나 많은 곳에서 이 KVO의 장점에 대해 2번을 꼽더라구요.
예를 들어 라이브러리에 들어있는 프로퍼티의 변경사항을 알고싶다!
➞ 내가 내부 구현을 못바꾸니(프로퍼티 옵저버 이런걸 추가 못하니) KVO가 이럴때 좋음..이라고 하는데..
만약 해당 클래스가 NSObject를 상속받고 있지 않고 @objc dynamic도 붙어있지 않다면 못하는거 아닌가요..?
이게 장점이라고 말하긴 좀 그럴 것 같은데...암튼 제 생각입니당..ㅎㅎ
3. 관찰된 프로퍼티의 이전값(oldValue)와 최신값(newValue)를 제공
4. KeyPath를 사용하여 프로퍼티를 관찰하므로 nested 프로퍼티도 관찰 가능
class Address: NSObject {
@objc dynamic var town: String
init(town: String) {
self.town = town
}
}
class Person: NSObject {
@objc dynamic var address: Address
init(address: Address) {
self.address = address
}
}
이런식으로 해서 person인스턴스에서 \.address.town 이렇게 관찰도 가능하다는 뜻
5. 따로 옵저버를 해제해주지 않아도 됨. 시스템이 알아서 removeObserver해줌
[단점]
1. NSOject 상속해야됨 == Objective-C 런타임에 의존하게 됨.
2. 이건 제가 생각한 단점인데...
예를 들어
1. 내가 구현함 && struct임
이럴 때 프로퍼티 옵저버를 사용하면 Static dispatch를 사용하게 되어서 성능상의 이점을 가져갈 수 있는데,
2. 내가 구현함 && class임
클래스는 기본적으로 dynamic dispatch를 사용하잖아요?
근데 static dispatch를 사용할 수 있을때는 static dispatch를 사용하는데..
dynamic을 붙힘으로서 무조건 dynamic dispatch를 사용하게 되는거 아닐까요?!...
예를 들어 town이 final이라고 했을 때 (또는 컴파일러가 final로 유추할 수 있을때)
이때는 static dispatch를 할 수 있지만
dynamic때문에 dynamic dispatch를 하게 되는...
그래서 굳이 따지자면 성능측면에서 안좋다..??
이거 같은 경우에는 Swift) dynamic이란? / Realm의 dynamic var는? 글을 보고 추측해보았읍니다..
틀린점이 있다면 말씀해주세요~🇰🇷🇰🇷
참고
www.hackingwithswift.com/example-code/language/what-is-key-value-observing
developer.apple.com/documentation/swift/cocoa_design_patterns/using_key-value_observing_in_swift