이전에 KVC와 KeyPath에 대해 알아보았다. 바로 KVO를 위해서였다.
KVO가 무엇인지 KVC와 KeyPath를 왜 알아야 했는지 알아보자
다른 객체의 속성에 대한 변경 사항을 객체에 알리기 위한 코코아 프로그래밍 패턴
Model과 View처럼 논리적으로 분리된 부분 사이에서 변경사항을 전달하는데 유용 (특히 MVC 패턴에서 )
NSObject를 상속받은 클래스에서만 사용가능
객체의 속성 변화를 알리기(관찰되기) 위해서는 몇가지 조건이 있다.
코드 예시를 보면서 알아보자
class Pokemon {
var name: String = "Pikachu"
func evolution() {
name = "Raichu"
}
}
나의 피카츄가 진화하면 몬스터도감이 확인하고 알림을 주도록 만들어 보자
그러기 위해서는 NSObject를 상속받아야 하며 관찰될 프로퍼티는 @objc와 dynamic 을 추가 해주어야 한다.
KVO는 KVC의 메커니즘을 활용해 객체의 특정 속성의 변화를 감지한다.
class Pokemon {
@objc dynamic var name: String = "Pikachu"
func evolution() {
if name == "Pikachu" {
name = "Raichu"
}
}
}
이렇게 코드를 수정해주면 관찰될 준비는 끝이다.
관찰자 클래스의 인스턴스는 하나 이상의 프로퍼티에 대한 정보를 관리한다. 관찰자를 생성할 때, 관찰하려는 속성을 참조하는 키 경로(KeyPath)와 observe(_:options:changeHandler:) 메서드를 호출하여 관찰을 시작한다.
class Pokedex: NSObject {
@objc var myPokemon: Pokemon
var observation: NSKeyValueObservation?
init(pokemon: Pokemon) {
myPokemon = pokemon
super.init()
observation = observe(\.myPokemon.name, options: [.old, .new], changeHandler: { object, change in
print("포켓몬도감 알림: \(change.oldValue!)가 \(change.newValue!)로 진화하였습니다.")
})
}
}
.myPokeMon.name 경로를 통해 name을 참조하고 옵션으로 선택한 .old와 .new를 통해 변경전 값과 변경 후 값을 사용한다.
observe의 options 매개변수에는 .old, .new 말고도 여러가지 옵션이 있다. 만약 필요하지 않다면 생략 가능하다. 다음은 주요 옵션들이다.
.initial: 관찰 시작 시점에서도 즉시 알림을 받는다. 관찰을 시작하자 마자 해당 속성의 현재 값을 확인하려 할 때 유용
.old: 변경 전의 속성 값을 알림에 포함 시킨다. .oldValue 키를 통해 값을 얻는다.
.new: 변경 후의 속성 값을 알림에 포함시킨다. .newValue 키를 통해 새로운 값을 얻을 수 있다.
.prior: 속성 값이 변경되기 직전과 변경된 후에 두 번 알림을 받는다. 값이 변경되기 전과 후를 모두 관찰하고자 할 때 사용된다.
.initial 과 .prior 예시 코드
import Foundation
class MyClass: NSObject {
@objc dynamic var count = 0
}
class Observer: NSObject {
var observation: NSKeyValueObservation?
init(object: MyClass) {
super.init()
observation = object.observe(\.count, options: [.initial, .prior]) { (object, change) in
if change.isPrior {
print("About to change...")
} else {
print("Changed to \(object.count)")
}
}
}
}
let myObject = MyClass()
let observer = Observer(object: myObject)
print("Update 1")
myObject.count = 1
print("Update 2")
myObject.count = 2
// 결과
// About to change...
// Changed to 0
// Update 1
// About to change...
// Changed to 1
// Update 2
// About to change...
// Changed to 2
관찰대상을 관찰자의 초기자에 전달한다.
몬스터 도감에 내 포켓몬을 연결해준다고 볼 수 있다.
let myPokemon = Pokemon()
let pokedex = Pokedex(pokemon: myPokemoon)
myPokemon.evolution() // 포켓몬도감 알림: 피카츄가 라이츄로 진화하였습니다.
이제 나의 포켓몬을 진화시키면 피카츄가 라이츄로 진화한것을 포켓몬 도감이 알려준다!
객체의 속성이 변할 때 즉시 반응할 수 있어, 데이터의 동기화나 UI업데이트 같은 작업을 즉각적으로 처리 가능
직접 접근하기 어려운 외부 라이브러리 등의 변화를 감지하기 좋음 (NSObject를 상속하지 않고 있거나 @objc, dynamic 작성 안되어 있으면 소용없는디?)
보통 속성 변화에 따른 동작 구현은 많은 콜백 함수나 이벤트 리스너를 작성해야 하지만 KVO를 사용하면 이런 복잡성 없이 간단하게 변화 감지 가능
여러 디자인 패턴에서 모델과 뷰 사이의 통신을 자연스럽게 연결가능
NSObject를 상속해야 하며 그렇기 때문에 class에서만 사용 가능하고 런타임에 의존하게 된다.
러탄임에 프로퍼티 변경을 감지하기 위해 추카적인 작업을 수행하여 약간의 오버헤드가 발생할 수 있다. 만약 큰 데이터 세트 혹은 빈번하게 변하는 프로퍼티에 적용할 때는 고려해야 한다.
순환 참조 발생 가능성이 있으며, 이는 메모리 누수로 이어질 수 있다.
NSKeyValueObservation 객체가 메모리에서 완전히 해제되면 관찰이 중단된다. 하지만 다른곳에서 해당 인스턴스를 참조하고 있다면 관찰은 유지되어 예기치 못한 문제가 발생할 수 있다.
그렇기 때문에 순환참조가 발생하지 않게 weak를 적절히 사용해주는 등의 주의가 필요하며 만약 참조가 더이상 없더라도 deinit 메서드에 invalidtae()메서드를 명시적으로 호출하는 것이 명확한 코드의 표현을 위해 권장되는 방식이다.
deinit {
observation?.invalidate()
}
KVO는 객체의 속성이 변경되는 것에 효과적으로 반응할 수 있게 해준다. 이를 통해 모델과 뷰나 다른 로직적으로 분리된 부분들 사이의 통신을 간편하게 할 수 있다.
다만 NSObject를 상속 받아야 하는 점이 큰 단점이라고 생각한다.
주의해야할 점은 관찰이 더 이상 필요하지 않을 때 관찰을 종료해야 한다는 것이다.