티스토리 뷰

Swift

Key-Value Observing(KVO) in Swift

Zedd0202 2021. 3. 1. 18:22
반응형

 

안녕하세요 :) 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

반응형

'Swift' 카테고리의 다른 글

dynamicMemberLookup  (0) 2021.03.11
Property Wrapper  (2) 2021.03.07
Key-Value Coding(KVC) / KeyPath in Swift  (4) 2021.02.28
Unowned Optional References  (11) 2021.02.22
Swift ) ARC / Strong Reference Cycle 해결 방법(weak, unowned)  (4) 2021.02.18