코어데이터에서 커스텀 타입 속성을 사용하기

코어데이터 내의 엔티티의 속성은 문자열, 숫자값, 날짜, 바이너리 데이터등의 기본적인 타입을 지정할 수 있다. 하지만 어떤 경우에는 이런 타입 이외의 커스텀 타입을 사용해야 하는 경우가 있을 수 있다. 예를 들어 NSColor라든지, 혹은 CGRect, CGPoint, CGSize와 같은 C 구조체로 된 정보 또, 아예 직접 작성한 커스텀 타입인 경우도 있을 수 있다. 이러한 커스텀 타입을 엔티티의 속성으로 사용하는 방법에 대해서 알아보자.

코어데이터 모델 편집기에서 엔티티 내 속성(attribute)의 타입을 선택하기 위한 팝업 메뉴 중에는 Transformable 이라는 타입이 존재한다. 이 타입은 저장소에 저장될 때는 바이너리 데이터로 저장되지만 컨텍스트로 올라갈 때는 다른 형식으로 변환되어 올라가도록 동작하게 된다.

속성의 타입을 Transformable로 선택한 후 오른쪽 데이터 모델 인스펙터를 보면 그림과 같이 Value Transformer를 설정하거나 Custom Class를 설정하는 부분이 있다. 그림의 예는 NSColor를 코어데이터에 저장하기 위한 설정을 보여주고 있다.

커스텀 클래스에 NSColor를 기입하고, Value Transformer는 비워둔다. 이 상태로 해당 엔티티에 대한 클래스를 생성하면, NSColor 타입의 프로퍼티가 생성되어 있는 것을 확인할 수 있다. (만약 커스텀 클래스 란을 비워두면 타입이 id로 정의된다.)

이때, NSColorNSCoding을 따르는 클래스이다. 따라서 별도의 트랜스포머 없이 데이터와 컬러 객체간의 변경이 가능하다. 즉, 커스텀 클래스를 만들어서 이 타입을 엔티티 속성에 사용한다고 하면, 해당 클래스가 NSCoding을 준수하도록만 해주고 모델 편집기에서는 Transformable로 타입을 설정해주면 된다.

NSCoding을 따르지 않는 클래스를 사용하는 방법으로는 트랜스포머를 사용하는 방법이 있다. 예를 들면 NSImage 같은 것을 코어데이터에 어떻게 저장할 것인가 하는 부분이다. (코코아 바인딩을 사용한다면 이미지뷰나 이미지웰의 data 항목을 사용하면 굳이 NSImage 인스턴스가 아닌 NSData만으로도 구현은 가능하다.)

NSImageNSCoding을 따르지 않으므로 트랜스포머를 만들어야 한다. 이 때 주의할점은 앱 -> 코어데이터로의 방향이 정방향이라는 점이며, 따라서 transformedValueClass는 항상 NSData여야 한다는 점이다.

/// PhotoTransformer for NSImage into Core Data

@interface PhotoTransformer: NSValueTransformer
@end

@implementation PhotoTransformer

+ (Class)transformedValueClass {
  return [NSData class];
}

+ (BOOL)allowsReverseTransformation
{
  return YES;
}

- (id)transformedValue:(id)value
{
  // NSImage -> NSData 변환
  NSImage* image = (NSImage*)value;
  return [image TIFFRepresentation];
}

- (id)reverseTransformedValue:(id)value
{
  NSData* data = (NSData*)value;
  NSImage* image = [[NSImage alloc] initWithData:data];
  return image;
}

// 트랜스포머를 등록하기. 이 부분은 여기보다는 앱 델리게이트에서 해주는 것이 좋다.
+ (void)initialize
{ 
  PhotoTransformer *pt = [[PhotoTransformer alloc] init];
  [NSValueTransformer setValueTransformer:pt
                      forName:@"PhotoTransformer"];
}
@end

Swift 버전

일전에 값 트랜스포머 관련해서 포스팅을 한 번 했을 때에도 언급했던 것 같은데, Swift를 써서 값 트랜스포머를 쓸 때에는 몇 가지 다른 점이 있다.

  1. Swift에서 ValueTransformer 클래스는 NSObject의 서브 클래스가 아니다. 따라서 initialize를 오버라이딩하는 부분을 만들 수 없다. 따라서 이 부분은 앱 델리게이트의 메소드를 오버라이딩해서 호출되도록 해야 한다.
  2. ValueTransformer의 이름은 단순 문자열 타입이 아니라 NSValueTransformerName 이라는 별도 타입으로 정의된다.  보통 서브클래스를 만든 후에 ValueTranformer를 확장하여 해당이름을 클래스 속성으로 추가한다.

동일한 클래스를 Swift로 작성하면 다음과 같다.

@objc(PhotoTransformer)
class PhotoTransformer: ValueTransformer {
  override class func transformedValueClass() -> AnyClass { return NSData.self }
  override class func allowsReverseTransformation() -> Bool { return true }

  override func transformedValue(_ value: Any?) -> Any? {
    // 
    if let image = value as? NSImage {
      return image.tiffRepresentation
    }
    return nil;
  }
  
  override func reverseTransformedValue(_ value: Any?) -> Any? {
    if let data = value as? Data {
      return NSImage(data: data)
    }
    return nil
  }
}

// 이름을 추가 등록
extension ValueTransformer {
  static let photoTransformerName = NSValueTransformerName(rawValue: "PhotoTransformer")
}

샘플 프로젝트

간단한 실증용 프로젝트를 만들어보자. (Objective-C로 진행했다.) 프로젝트를 하나 생성한다. 시작 시 코어데이터에 체크하여 코어데이터 스택이 미리 준비되도록 한다.

가장 먼저할 일은 코어데이터 모델 파일을 열어서 엔티티를 추가하는 것이다. 다음과 같이 간단하게 3개의 속성만을 정의한다. photo는 사진 속성으로 Transformable 타입으로 선택한다.

photo 속성을 선택하고 데이터 모델 인스펙터를 통해서 몇 가지 세부 사항을 정의한다.  커스텀 클래스는 NSImage로 기입하고, Value Transformer Name에는 “Photo2DataTransformer”라고 쓴다. 값 트랜스포머를 만든 후에 반드시 이 이름으로 등록해야한다

데이터 모델에 대한 편집은 이것으로 끝났다. 다음은 Photo2DataTransformer를 작성할 시간이다. 새 파일을 추가하고 Cocoa Class를 선택한다. NSValueTransformer의 서브 클래스를 만든다고 설정하면 헤더에 Foundation.h를 임포트하게 되는데, 이 상황에서는 NSImage가 노출되지 않으니, 이 부분을 <Cocoa/Cocoa.h>로만 변경해주면 된다. 이후 소스는 위에서 설명한 내용과 동일하다.

다음은 등록을 위해서 앱 델리게이트를 편집할 차례이다. 앱 델리게이트에서는 두 가지 작업을 처리해야 한다.

  1. UI는 코코아 바인딩으로 설정할 것이므로 NSManagedObjectContext에 접근할 수 있는 접근자를 준비한다.
  2. 앞서 작성한 Photo2DataTransformer를 등록한다.

 

/// in AppDelegate.m

#import "Photo2DataTransformer.h"  // 1

@interface AppDelegate ()
...
- (NSManagedObjectContext *)context; // 2
@end

@implementation AppDelgate
...

+ (void)initialize //3
{
  [super initialize];
  Photo2DataTransformer *pt = [[Photo2DataTransformer alloc] init];
  [NSValueTransformer setValueTransformer:pt
                      forName: @"Photo2DataTransformer"];
}

- (NSManagedObjectContext*)context // 4
{
  return self.persistentContainer.viewContext;
}
...
  1. Photo2DataTransformer.h 헤더 반입
  2. 컨텍스트 접근자 선언
  3. 값 트랜스포머를 등록한다.
  4. 컨텍스트 접근자 구현

모든 코드 작업은 끝났고, 이제 UI를 만들 차례이다. 만들어질 UI의 모양은 다음과 같다.

  • 테이블 뷰는 뷰 기반 테이블 뷰로 1개 칼럼을 가진다.
  • 테이블 뷰 셀 뷰 내에는 1개의 이미지 뷰와 2 개의 텍스트레이블을 위치시켰다.
  • Add 버튼을 하나 추가한다.
  • Box를 하나 추가하고 그 속에, 두 개의 텍스트레이블+필드 쌍과 이미지웰(ImageWell)을 추가했다.
  • 참고로 ImageWell은 선택해서 Editable 속성에 체크해주어야 한다.

그리고 ArrayController 하나를 추가한다. 이름을 EmplyeesController라고 짓고, 다음과 같이 엔티티 모드로 하고 엔티티 이름을 준다. 그리고 앱이 시작했을 때 저장된 내용을 읽어와 보여주도록하려면 Prepares Content에도 체크한다.

다음은 테이블 뷰를 선택해서 바인딩 Table Content를 배열컨트롤러의 컨트롤러 키 “arrangedObject”에 바인딩한다.  그리고 테이블 뷰의 선택한 행과 배열컨트롤러의 선택된 값을 동기화하기 위해서 테이블 뷰의 바인딩 Selection Indexes를 배열 컨트롤러의 컨트롤러 키 “selectionIndexes”와 바인딩한다.

  • 테이블 뷰 바인딩 설정
    • 바인딩: Table Content
      • 대상 : EmployeesController
      • 컨트롤러 키 : arrangedObjects
    • 바인딩: Selection Indexes
      • 대상: EmplyeesController
      • 컨트롤러 키 : selectionIndexes

다음은 테이블 뷰 셀 내부의 뷰들에 대해 바인딩한다. 테이블 뷰 셀 내부의 컨트롤들은 모두 테이블 뷰 셀과 바인딩하면서 셀의 “objectValue”의 하위 키패스와 바인딩하면 된다.

  • 이미지 뷰 (테이블 뷰 셀 내) 바인딩
    • 바인딩: Value (주의: Data가 아님)
      • 대상 : Table Cell View
      • 모델 키 : objectValue.photo
  • 텍스트 필드 두 개
    • 바인딩 : value
      • 대상 : Table Cell View
      • 모델 키 : objectValue.firstName
        (아래쪽 텍스트 필드는 모델 키만 objectValue.lastName을 쓰고 나머지는 동일하다.)

버튼은 바인딩이 아니라 액션 메시지를 연결한다. EmployeesController의 add: 와 연결해준다.

다음 아래쪽 박스 내부의 UI는 테이블 뷰에서 선택한 데이터의 세부 내용이다. 두 개의 텍스트 필드와 이미지 뷰를 배열 컨트롤러의 “selection”과 연결해주면 된다

  • 이미지 뷰 (Image Well)
    • 바인딩: Value
      • 대상 : EmployeesController
      • 컨트롤러 키 : selection
      • 모델 키 : photo
  • 텍스트 필드 2 개
    • 바인딩 : value
      • 대상 : EmployeesController
      • 컨트롤러 키 : selection
      • 모델 키 : firstName, lastName

끝으로 메뉴의 Save… 항목을 앱 델리게이트의 saveAction: 과 연결해주면 완성이다.

아래 표는 바인딩 전체 정보에 대한 요약이다.

[ninja_tables id=”9074″]

샘플 프로젝트 다운로드 : https://app.box.com/s/r5arwd1g4uhq1wd3z9p5elexli7gpun1

정리

코어데이터에서 기본적으로 지원되지 않는 타입을 사용하는 경우 다음의 방법을 쓰면 된다.

  1. 커스텀 클래스가 NSCoding을 지원하면 해당 속성을 Transformable로 설정하면 된다.
  2. 커스텀 클래스가 NSCoding을 지원하지 못하는 경우, Transformable 타입으로 설정하고 값 트랜스포머를 만들어서 설정해준다.

 

참고자료

코코아 바인딩의 기초

MVC 패턴에서 프로그래머가 가장 많이 작성하는 코드는 크게 두 가지인데, 하나는 모델 데이터에서 발생한 변경을 뷰에 반영하는 것이고, 다른 하나는 뷰에서 사용자의 조작에 의해서 변경된 값을 모델 데이터에 반영하는 것이다. 사실 이것이 MVC에서 컨트롤러가 수행하는 일이다. GUI프로그램을 작성할 때 가장 많이 하게되는 이러한 작업을 조금 더 간단하게 (가급적이면 코드를 작성하지 않고) 구현하는 방법이 있으면 제법 편하지 않을까? 이럴 때 사용하는 코코아 바인딩은 Cocoa에서 macOS에서 데이터 모델과 뷰 사이의 양방향 연결을 만드는 일종의 ‘마법’이다. 예를 들어 아래 그림과 같은 간단한 앱을 생각해보자.

코코아 바인딩 샘플 앱

이 앱은 매우 간단한 구조를 가지고 있다. 뷰에는 숫자를 표시하는 레이블하나와 슬라이더가 하나있다. 이 앱이 하는 일은 사용자가 슬라이더를 좌우로 옮길 때마다, 슬라이더의 값이 레이블에 숫자로 반영되는 것이다. 이를 위해서는 뷰 컨트롤러에 대해서 다음의 프로퍼티와 액션이 정의되고, 그에 대한 코드가 구현되면 된다.

  1. 뷰 컨트롤러는 숫자값 하나를 저장할 NSNumber타입의 프로퍼티를 하나 가지고 있다. (꼭 NSNumber가 아니어도 된다. 그냥 float이나 double 타입의 값이기만 해도 된다.)
  2. 뷰 컨트롤러는 텍스트레이블에 대한 아웃렛 연결을 가지게 된다. (이 아웃렛을 통해서 레이블을 참조하고 그 값을 변경할 수 있다.)
  3. 슬라이더는 사용자가 옮길 때마다 뷰 컨트롤러에게 액션 메시지를 보내게 된다. 이 메소드에서는 슬라이더의 값을 받아서 레이블의 값을 세팅하는 코드를 작성해주어야 할 것이다.

코코아 바인딩은 특정한 프로퍼티에 대해서 UI 컨트롤과의 양방향 연결을 구성하는 테크닉이다. 기본적으로 뷰 내의 컨트롤과 뷰 컨트롤러 사이의 연결을 만들 수 있지만, 반드시 뷰와 뷰 컨트롤러 사이의 관계일 필요는 없다. 샘플 프로젝트는 추가적인 클래스 없이 앱 델리게이트를 이용하기로 한다. 기본 macOS용 앱을 위한 프로젝트를 시작하고, 앱 델리게이트 파일을 다음과 같이 하나의 프로퍼티를 추가한다. (그외에 다른 아웃렛이나 액션을 선언하지 않아도 된다.)

Put Together

MainMenu.xib 파일을 열어서 메인 윈도우에 레이블 하나와 슬라이더 하나를 아래와 같이 배치한다. 참고로 슬라이더를 추가한 후에는 속성 인스펙터에서 Continuous에 체크한다. 슬라이더는 기본적으로 움직였다가 마우스를 릴리즈하는 시점에 타깃에게 액션 메시지를 보내는데, Continuous가 체크되면 슬라이더 노브를 움직이는 동안에 계속적으로 메시지가 전달된다. (즉 움직이는 중에 숫자값이 계속 바뀌는 것을 볼 수 있다.)

그리고 숫자값 프로퍼티 v에 대해 초기값을 설정해보자. 왼쪽 객체 목록에서 앱 델리게이트를 선택하고, 속성 인스펙터에서 User Defined Runtime Attributes 아래에 있는 + 버튼을 클릭한다. 키 패스에는 v를 입력하고 타입은 Number, 값은 50으로 입력한다.

참고로, nib 파일은 뷰, 컨트롤, 컨트롤러등의 여러 객체들이 어떠한 프로퍼티값을 가지고 있는 상태 그대로를 직렬화한 파일이다. 따라서 인터페이스 빌더에서 정의하는 크기와 위치(곧, 뷰의 frame 속성이다.)라던지 색상 및 컨트롤의 유형 등의 정보는 객체의 인스턴스를 만들어서 코드상에서 설정해야 하는 번거로움을 덜어주는 역할이라고 보면 된다.

이번에는 바인딩을 연결하자. 먼저 텍스트 레이블을 선택한다. 참고로 이 레이블에 대해서는 앱 델리게이트에서 어떤 아웃렛 연결이나 액션메시지 연결은 없었다는 점을 상기하자. 우측 인스펙터의 7번째 탭이 바인딩 탭이다. Value 라고 되어 있는 부분을 보면 몇 가지 값 속성이 있는데, 그 중 value 를 찾아서 세부 항목을 열어본다. 그러면 팝업박스를 통해서 어떤 객체에 연결할 것인지를 고를 수 있다.1 여기서 앱 델리게이트를 선택한다.

다음은 모델키패스(model keypath)를 입력해야 한다. 연결하고자 하는 숫자값은 앱델리게이트 내의 v 라는 이름을 가진 NSNumber이다. 이 키패스는 바인딩되는 객체를 기준으로, self.v 라고 입력한다. (self.을 생략하고 v만 입력해도 된다.)

다음은 슬라이더를 선택해서 똑같이, Bind to 는 Delegate로, 키 패스는 self.v 로 입력해준다.

앱 델리게이트의 프로퍼티와 텍스트레이블, 슬라이더를 바인딩으로 연결 완료했다. 재차 강조하지만 여기에는 프로퍼티 정의 외에는 어떠한 코드도 작성되지 않았다. 하지만 이것으로 앱은 완성이다.

이제 앱을 빌드하고 실행해보자. 화면에 나타나는 앱 윈도에서 슬라이더를 좌우로 움직여보자. 슬라이더를 움직이면 레이블의 숫자값이 쫘르르르 변하는 것을 볼 수 있다.

어떤 원리로 동작하나

코코아 바인딩은 과연 어떤 마법이길래, 이러한 뷰와 데이터 모델 간의 양방향 바인딩이 코드 한 줄 없이 자동으로 이루어질 수 있을까? 이는 기본적인 코코아의 기본 패턴/기능 몇 가지를 멋지게 조합한 결과물이다.

  • 컨트롤의 특정한 프로퍼티와 컨트롤러 내의 모델 프로퍼티는 그 이름을 기준으로 연결된다.
  • 컨트롤이 조작되면 바인딩이 연결된 객체와 키패스를 이용해서 해당 값을 업데이트한다.
  • 반대로 컨트롤러 내의 어떤 값이 변경되면 그 값과 연결된 컨트롤들이 통지를 받고 값을 업데이트한다.

먼저 샘플 프로젝트에서 사용된 예를 살펴보자. 여기에는 NSLabelNSSlider 가 각각 하나씩 사용되었는데, 이들은 모두 NSControl의 자식 클래스들이다. 이쯤에서 NSControl에 대해서 간략히 살펴보자

NSControl

NSControl은 화면상에 표시되는 여러 컨트롤 디바이스를 표현하는 클래스이다. 표면적으로는 셀 이라는 것을 이용해서 시각적으로 표현되는데, 여기서는 그게 중요한 게 아니고 컨트롤은 어떤 값을 조정/조작하는데 사용되는 클래스라는 점이다. 실질적으로는 아무런 값을 가지지 않는 컨트롤(버튼 등)도 있고, 텍스트를 표현하는 레이블이나, 텍스트를 편집할 수도 있는 텍스트 필드, 슬라이더 등등 화면상에 표현되는 표준 UI 컴포넌트들은 모두 NSControl이라고 볼 수 있다.

NSControl은 어떤 값을 조작하는 UI 단위이다. 이 값은 기본적으로 objectValue 라고 하는 id 타입 객체이며, 하나의 컨트롤은 그 포맷에 따라 doubleValue, intValue, stringValue  등의 여러 타입의 프로퍼티를 가지고 있다. 하지만 이들은 모두 컨트롤 내부에서 추상클래스와 포매터를 기반으로 하나의 값의 다른 측면을 바라보는 것이다.  따라서 여러 프로퍼티에 혼란스러워하지 않고 컨트롤러 1개는 값 1개를 조작한다고 생각하면 된다.

NSControl은 코코아의 디자인패턴 중 target-action이 적용되는 전형적인 클래스이다. 타깃-액션은 컨트롤에 어떤 타깃이 주어지면, 사용자가 자신을 조작할 때 미리 정해진 타깃으로 액션 메시지를 전송하게 된다. 우리가 보통 인터페이스 빌더에서 컨트롤러 객체로 액션 메시지를 연결하는 행위는 해당 컨트롤이 트리거링되는 조작을 받았을 때, 어떤 컨트롤러에게 어떤 메시지를 보내야 하는지를 알려주는 것이라 해석할 수 있다.

키밸류 코딩

컨트롤이 조작되었을 때, 컨트롤을 타깃에게 어떠한 액션 메시지를 보내게 된다. 이 때 보통은 타깃이 뷰 컨트롤러이고, 코드상에서는 NSViewController 등의 클래스를 서브 클래싱하면서 IBAction 메소드를 정의하여 인터페이스 빌더에서 컨트롤과 뷰 컨트롤러간에 action 연결을 만들게 된다.

바인딩에서 액션은 임의의 커스텀 메소드를 사용하지 않았다. 대신에 이 액션은 하고자 하는 일이 명확하다. 바인딩하는 대상의 특정한 프로퍼티 키패스를 컨트롤러의 값으로 세팅하는 것이다. 우리는 샘플 프로젝트에서 AppDelegatev 라는 이름의 프로퍼티를 만들었다. AppDelegateNSObject의 서브 클래스이므로, 키밸류 코딩 컨벤션을 따른다.(이는 NSKeyValueCoding이라는 비정규 프로토콜에서 정의된다.) 따라서 바인딩되는 객체와 키 패스를 알고 있다면, 슬라이더는 그 자신이 조작되는 시점에 타깃에게 "self.v"라는 키패스의 값을 세팅하라는 메시지를 보낼 것이다.

[self.target setValue:self.objectValue forKeyPath:@"self.v"];
 ^^^^^^^^^^^ 1                                    ^^^^^^^^^^ 2
  1. 타깃은 바인딩될 객체로 설정된다.
  2. self.v는 모델 키패스로 설정됐다.

이를 통해서 슬라이더를 움직이면 AppDelegate의 프로퍼티 v는 슬라이더의 값이 된다. 그러면 이 값이 변경되었을 때, 레이블은 어떻게 업데이트 되는 것일까?

키밸류 옵저빙

프로퍼티의 구조를 이해하고 있다면, 어떤 프로퍼티 값이 변경되었을 때 특정한 동작을 취하게 하는 것을 구현하는 방법에 대해서 감을 잡을 것이다. 보통은 setter 메소드를 정의하는 것이다. 샘플 프로젝트의 v 는 copy, nonatomic 시멘틱으로 정의되어 있다. 따라서 컴파일러는 다음과 같이 메소드를 합성해 낼 것이다.

- (void)setV:(NSNumber *)v
{
  NSNumber* newV = [v copy];
  __v = newV;
}

만약, 이 시점에 레이블의 텍스트를 변경하고 싶다면 다음과 같이 코드를 한 줄 추가해주면 된다.

- (void)setV:(NSNumber *)v
{
  NSNumber* newV = [v copy];
  __v = newV;
  [self.vLabel setObjectValue: self.v];
}

문제는 이렇게하려면 앱 델리게이트가 해당 레이블에 대한 참조를 가져야한다는 것이다. 그런데 우리는 샘플 프로젝트에서 어떠한 아웃렛도 선언하거나 연결한 적이 없다. 그러면 어떻게 레이블은 그 값을 자동으로 변경할 수 있을까? 그 비밀은 키-밸류 옵저빙이다. 키 밸류 옵저빙은 어떤 객체가 다른 객체의 키패스에 대한 값이 변경될 때, 그것에 대한 통지를 받게 되는 매커니즘이다. 이와 관련된 내부적인 처리는 모두 런타임에서 수행되며, 우리는 그저 그 키패스에 해당하는 프로퍼티가 키밸류 코딩 규칙을 따르도록 정의하면 되는 것이다.2

바인딩을 세팅하게 되면, 앞서 살펴본바와 같이 타깃-액션 메시지를 보내는 기능을 동적으로 생성하는 동시에, UI컨트롤은 모델 키패스에 대하여 자신을 옵저버로 등록한다. 그리고 해당 프로퍼티가 변경되면, 그 프로퍼티의 값을 자신의 objectValue로 설정한다.

대략 다음과 같은식의 코드들이 자동으로 붙게 된다. 여기서 대상 객체를 target으로 했지만, 실제로 하나의 컨트롤은 여러 개의 모델과 바인딩될 수 있으므로 별도의 객체 식별자를 사용할 것으로 보인다.

...
// 아마도 awakeFromNib 쯤에...
[self.taget addObserver:self
            forKeyPath:@"self.v"
            options:NSKeyValueObservingNew
            context:NULL];
...

// 그리고 바인딩된 모델 키패스의 변경을 감지하도록
- (void)observeValueForKeyPath:(NSString*)keypath 
        ofObject:(id)object 
        change:(NSDictionary<NSKeyValueChangeKey, id>*)change
        context:(void*)context
{
   if([keypath isEqualToString:@"self.v") {
     // 변경된 값을 자신의 값으로 치환한다.
     (id)newVaue = [change objectForKey:NSKeyValueChangeNewKey];
     self.objectValue = newValue;
   } else {
     [super observeValueForKeyPath:keypath
            ofObject:object
            change:change
            context:context];
  }
}


// 해제되기 직전에는 옵저버 등록을 해제해야 함
- (void)dealloc
{
  [self.target removeObserverForKeyPath:@"self.v"];
}

슬라이더와 레이블 모두가 이런 식으로 특정한 모델 데이터에 바인딩 되어 있다. 샘플 프로젝트에서는 슬라이더를 통해서만 모델 값을 변경할 수 있었지만, + , – 버튼을 이용해서 값을 조정한다던가, 레이블 대신에 텍스트 필드를 사용해서 값을 입력받을 수 있게한다면 이 모든 관계들이 실시간으로 영향을 받으면서 변경되는 것이다.

Swift와 코코아 바인딩

키밸류코딩과 키밸류옵저빙은 모두 Objective-C 런타임에 전적으로 의존하는 매커니즘이다. 그러면 Swift로 작성하는 코코아 앱에서는 코코아바인딩을 사용할 수 없을까? 당연히 사용할 수 있다. 대신에 몇가지 조건이 있다.

  1. 모델 키패스를 포함하는 객체의 클래스는 NSObject의 서브 클래스여야 하며
  2. Objective-C런타임에서 식별할 수 있게 @objc 접두어를 써서 정의해야 한다.
  3. 모델 키패스가 되려는 프로퍼티 역시 @objc 접두어를 써야 한다.
  4. 동시에 접근자 메소드를 동적으로 생성해내기 위해서 dynamic 키워드도 써야 한다.

따라서 샘플 프로젝트의 앱 델리게이트는 Swift로 작성됐다면 다음과 같은 모양이어야 할 것이다.

@objc
class AppDelegate: NSObject, NSApplicationDelegate
{
  @IBOutlet weak var window: NSWindow!
  @objc dynamic var v: float = 50.0;
}

그외에…

  1. 코코아 바인딩은 인터페이스 빌더에서 설정하는 것이 간편하다. 하지만 실제로 이 기능은 NSKeyValueBindingCreation이라는 비정규 프로토콜에 정의된 메소드를 호출하는 것으로 축약된다.
  2. 샘플 앱 프로젝트는 여기에서 다운받을 수 있다.(Swift 버전은 여기)

참고자료

 


  1. 특정한 값을 바인딩하기 위한 선택사항으로는 동일 nib 파일 내에 있어야 한다는 제약은 있다. 이는 스토리보드 상에서도 같이 적용될 것이다. 
  2. 키밸류 코딩 규칙을 따르도록 하는 것은 전혀 어렵지 않다. @property를 써서 정의하면 된다. 키밸류 코딩 규칙은 getter, setter 메소드와 인스턴스 변수이름 사이의 관계로만 정의되어 있다. 

코코아바인딩 :: 컨트롤러의 컨텐츠 제공방법

컨트롤러 컨텐츠를 제공하기

컨트롤러들은 (당연히도) 관리할 컨텐츠가 필요하며, 컨텐츠를 지정하는 방법에는 여러가지가 있다. 바인딩을 생성하는 코드를 통해서도 가능하며, IB를 통해서도 설정할 수 있다.

컨트롤러 컨텐츠 설정하기

NSObjectController와 그 서브클래스들은 -initWithContent:를 통해서 초기화되며, 만약 컨텐츠 바인딩을 의도한다면 nil을 넘겨도 된다. 혹은 명시적으로 setContent:를 통해서 지정해줄 수도 있다. 보통은 컨트롤러의 컨텐츠 바인딩을 통해서 연결하는 것이 일반적이다. 코코아바인딩 :: 컨트롤러의 컨텐츠 제공방법 더보기

키밸류 옵저빙이란

키밸류 옵저빙

키밸류코딩(KVC)에 이어서 키밸류 옵저빙에 대해 이야기해보자. 키밸류 코딩에 관한 포스팅에서 키밸류 코딩은 키밸류 옵저빙의 근간이 되는, 어떤 객체의 프로퍼티를 키 이름으로 런타임에 동적으로 탐색하여 액세스할 수 있게하는 기술이라고 하였다. 키밸류 옵저빙 역시 프로퍼티 액세스와 관련한 Objective-C 런타임이 제공하는 동적 기능의 일종으로, 특정한 키에 대한 객체의 프로퍼티 값이 변경될 때, 해당 변경에 대한 알림이 다른 객체로 통지되는 것을 말한다.

예를 들어 foo 라는 객체 인스턴스에 a 라는 프로퍼티가 있고, bar 라는 객체가 이 프로퍼티에 대한 옵저버로 등록이 되어 있다면, foo의 내부 혹은 외부에서 a라는 값을 업데이트하게 될 때, foo 내부의 코드에서 bar라는 객체에 명시적으로 어떤 메시지를 보내지 않더라도, bar 객체는 미리 정해진 어떤 메시지를 받게 되어 그 변경에 따른 어떤 작업을 처리할 수 있게 된다는 것이다.

한편으로 생각하면 매우 간단하고 단순한 기술처럼 보인다.

  1. KVC 호환으로 구현된 A라는 객체가 있고,
  2. B라는 객체는 A라는 객체의 특정한 키에 대해서 자신을 옵저버로 등록한다.
  3. KVO호환 방식으로 객체 A에 해당 키에 대한 값 변경이 발생한다.
  4. 객체 A는 자신을 지켜보는 옵저버들에게 값 변경에 대한 통지를 보낸다.
  5. 옵저빙하고 있는 객체의 키 값 변경에 대한 통지를 B가 받게 된다.

여기에는 별다른 마술이 없는 것처럼 보인다. A라는 객체에 대해서 명시적으로 객체 B가 “내가 이런이런 키에 대한 옵저버가 되겠다”고 말했고, 객체 A는 그에 대해서 변경이 발생하면 자신에게 옵저버가 되겠다고 말한 객체들에 대해서 정해진 메시지를 보낸다. 그러면 먼저 이 간단해 보이는 기술을 어떻게 쓸 수 있는지 살펴보자.

옵저버로 등록하기

옵저버를 등록하는 것은 타깃 객체에 -addObserver:forKeyPath:options:context: 메시지를 보내어 등록한다. 등록을 마치면 타깃 객체에서 해당 메시지에 명시한 키패스의 값에 변화가 발생하면 이 내용이 옵저버에게 통지된다.

옵션

키패스에 변경이 발생하여 이를 옵저버에게 알려줄 때, 타깃 객체는 옵저버에게 변경 사항의 세부 정보가 담긴 사전(dictionary) 객체를 전달한다. 이 때 이 사전 객체가 어떤 값을 담고 있을 것인가에 대해서는 옵저버로 등록할 때 전달한 옵션값에 의해 결정된다. 옵션값은 NSKeyValueObservingOptions라는 열거값에 정의된 상수들로 다음과 같은 것들이 있다.

  • NSKeyValueObservingOptionNew – 변경사전에 새 값에 대한 값을 포함한다.
  • NSKeyValueObservingOptionOld – 변경사전에 기존 값을 포함시킨다.
  • NSKeyValueObservingOptionInitial – 등록이 처리되는 시점에 옵저버에게 메시지를 보낸다.
  • NSKeyValueObservingOptionPrior – 변경 전/후에 각각 메시지가 보내지도록 한다.

컨텍스트

컨텍스트 정보는 통지를 구분하기 위한 방법으로 사용되는데, 통상 NULL을 넘겨준다. 그러면 변경 통지 메시지에서 NULL이 넘어오게 되며 이 때 변경 통지의 구분은 타깃의 키패스에 의존하게 된다. 이 구현도 나쁘지는 않지만, 우리가 알지 못하는 수퍼 클래스에 의해서 옵저빙되는 키패스를 놓칠 우려가 있기 때문에 이를 사용한다.

보통은 정적 포인터 변수(자기 자신을 가리키는)를 하나 만들어서 사용한다.

변경 통지 받기

타깃 객체는 특정한 프로퍼티가 변경되면 그 키패스에 대한 옵저버들에게 변경 통지를 알린다. 변경 통지를 보내는 방법은 옵저버에게 -observeValueForKeyPath:ofObject:change:context: 를 호출하는 것이다. 따라서 KVO 통지를 받고 싶은 클래스에서는 이 메소드를 오버라이드 해야 한다. 이 때 이 메소드의 각 파라미터는 다음과 같다.

  • keyPath: 옵저빙하는 키패스
  • ofObject: 통지를 보낸 객체
  • change: NSKeyValueChangeKey에 정의된 변경 사항에 대한 키들을 사용하여 변경에 대한 세부 내용을 담은 사전
  • context: 옵저버가 등록시 사용한 컨텍스트

특히 change는 어떤 형태의 변경이 일어났는지를 알려주는 정보가 담긴 사전이다. 여기에는 기본적으로 몇가지 키가 존재한다.

  1. NSKeyValueChangeNewKey – 변경 후의 값
  2. NSKeyValueChangeOldKey – 변경 전의 값
  3. NSKeyValueChangeNotificationPriorKey
  4. NSKeyValueChangeKindKey – 변경의 종류
  5. NSKeyValueChangeIndexesKey – to-many 관계에 대한 변경인 경우, 변경이 발생한 위치의 인덱스

이 중에서 NSKeyValueChangeKindKey는 어떤 종류의 변경인지를 알려준다. 일반적으로 저장 프로퍼티의 값이 다른 값으로 교체되는 것을 생각할 수 있지만, 객체의 프로퍼티는 단순한 변수라는 개념 이상의 to-one / to-many relationship이기도 하기 때문이다. 이 키가 사전에 존재한다면, 사전의 그 값은 다음 중 하나가 될 것이다. (NSUInteger기반으로 enum 값들로 만들어져 있다.)

  • NSKeyValueChangeSetting – 프로퍼티 값이 다른 값으로 set 되었다.
  • NSKeyValueChangeInsertion – to-many 관계에서 아이템이 추가되었다.
  • NSKeyValueChangeRemoval – to-many 관계에서 아이템이 제거되었다.
  • NSKeyValueChangeReplacement – to-many 관계에서 아이템이 변경되었다.

만약 커스텀 클래스의 배열에서 N 번째 원소의 특정한 프로퍼티 값이 변경되는 것은 KVO에 호환일까? 그렇지 않다. 그것은 원소 객체 그 자체의 내부 상태 변경이므로 해당 관계에 대한 KVO의 관심사항이 아니다. to-many 관계(배열이나 Set인 프로퍼티)의 변경은 다음 중 하나의 경우만 호환된다.

  1. 배열 자체가 다른 배열로 교체되었다.
  2. 배열에 원소가 추가되었다.
  3. 배열에서 원소가 삭제되었다.
  4. N번째 원소가 다른 객체로 교체되었다.

이미 추가되어 있는 원소의 다른 프로퍼티 변경에 대한 추적은 기본 KVO로 호환되지 않는다. 이는 타깃 객체 자체가 다시 각 배열 원소에 대한 옵저버가 되어야 한다. (이와 관련된 내용은 집합 메소드와 관련하여 별도의 포스팅에서 다시 한 번 다뤄보도록 하겠다.)

예제

간단한 예를 들어보자. 이전에 KVC 관련한 소개글에서 사용했던 Foo 클래스를 떠올려보자.

@interface Foo: NSObject
@property (copy, nonatomic) NSString* moo;
@end

@implementation Foo
@end

옵저버

다음은 옵저버가 될 클래스이다. 통지를 받는 부분만 아래와 같이 작성해본다.

@interface Bar: NSObject
@end

/// 이 일련의 예제는 모두 같은 파일 1개에서 
/// 사용된다고 가정한다.
/// 따라서 static 변수를 다음과 같이 초기화한다.
static void* FooBarObservingContext = &FooBarObservingContext;
@implementation Bar
- (void)observeValueForKeyPath:(NSString *)keyPath 
        ofObject:(id)object 
        change:(NSDictionary<NSKeyValueChangeKey,id> *)change 
        context:(void *)context
{
   if(context == FooBarObservingContext ) {
       /// 변경전 값과 변경 후 값을 로그에 찍어본다.
       NSLog(@"the foo's bar has changed from:%@ to:%@",
             [change objectForKey:NSKeyValueChangeOldKey],
             [change objectForKey:NSKeyValueChangeNewKey]
       );
   } else {
     /// 정해진 컨텍스트외에는 수퍼클래스로 돌린다.
     [super observeValueForKeyPath:keyPath
            ofObject:object
            change:change
            context:context];
   }
}
@end

KVC 호환으로 작성된 객체는 특별한 경우를 제외하고는 자동으로 KVO 호환이 된다. 여기서 특별한 경우라는 것은, 인스턴스 변수를 직접 액세스하는 것이다. 따라서 setValue:forKey: 를 통한 변경은 물론이고,  setter 접근자를 사용해서 키 값을 업데이트 하는 경우에도 통지가 간다. 그러니까 분명 Objective-C 런타임은 실제로 “어떤 작업을 setter 실행의 앞 뒤에 하고 있는 것”처럼 보인다. 이는 암시적으로 KVC/KVO에서 호출되는 접근자/변경자 메소드들은 실제로 코드상에 명시된 함수가 아니라 그 앞뒤로 어떤 일을 하는 실제로는 decorated된 다른 함수를 호출하고 있는 것이라 볼 수 있다. 즉 KVC/KVO에 의해서 접근자 메소드는 런타임에 실제로는 다른 함수로 바꿔치기 되어 호출되는 것이다.1

특정 키에 옵저버 설치하기

실제로 객체 인스턴스를 만들어서 옵저버를 추가해서 변경을 확인해보자. main 함수를 아래와 같이 추가로 작성하고 컴파일해보도록 하자. (지금까지 작성된 Foo, Bar 클래스 및 아래의 main 함수는 모두 main.m 이라는 하나의 파일에 작성된다. 전체 코드를 담은 Gist를 다음 페이지에 게재해두겠다.) 참고로, 옵저버의 사용이 끝나는 지점에서는 반드시 옵저버를 제거해주어야 한다.

int main(int argc, const char ** argv){
  @autoreleasepool {
    Foo* foo = [[Foo alloc] init];
    Bar* bar = [[Bar alloc] init];
    foo.moo = "one";
 
    [foo addObserver:bar forKeyPath:@"moo"
         options:(NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew)
         context:theContext];

    foo.moo = "two"

    [foo removeObserver:bar forKeyPath:@"bar"];
  }
  return 0;
}
    

위 코드에서는 옵저버의 추가와 제거를 객체 외부의 컨텍스트에서 수행했지만, 실제로 많은 경우에는 옵저버를 생성하는 시점에 옵저빙 타깃에 대한 옵저버로 추가하면서 해제될 때 옵저버를 제거하는 처리를 한다.

의존하는 키

객체의 프로퍼티 중에서는 스토리지 변수를 가지지 않는 읽기 전용의 프로퍼티들이 있다. 보통 이러한 프로퍼티는 2개 이상의 인스턴스 변수나 다른 프로퍼티로부터 계산된 값을 반영하는 것이다. 소금물에 대한 정보를 나타내는 클래스를 다음과 같이 디자인한다고 생각해보자.

@interface Solution: NSObject
@property (readwrite, nonatomic) float salt;
@property (readwrite, nonatomic) float water;
@property (readonly, nonatomic) float weight;
@property (readonly, nonatomic) float concentration;
@end

이 중에서 소금물의 무게와 농도는 소금의 무게, 물의 무게로부터 계산가능한 값이므로 readonly로 선언했다. (혹은 프로퍼티로 선언하지 않고 메소드로 선언해도 무방할 듯 하지만…) 이들 계산 프로퍼티(computed property)들의 구현은 다음과 같이 간단하다.

@implementation Solution

- (float)weight
{  return _salt + _water; }

- (float)concentration
{
  if(self.weight > 0) {
    return _salt / self.weight;
  }
  return 0;
}
@end

그런데 이러한 계산 프로퍼티들에 대해서도 KVO 적용이 가능할까? 앞선 예에서는 setter에 의해서 값이 변경될 때 KVO 통지가 발생하는 것을 보았는데, 이들 계산 프로퍼티는 setter라는 것이 없다. 계산 프로퍼티는 특성 상 다른 프로퍼티 키에 의존하게 된다. 예를 들어 weight 프로퍼티는 saltwater의 합이므로 두 프로퍼티에 의존하고 있다고 할 수 있다. 즉 두 키 중 하나의 값이 바뀌면 그에 따라서 weight의 값도 바뀌는 것이다.  concentration 역시 weight, salt 키에 의존하고 있다. 물론 농도 키를 옵저빙할 필요가 있을 때 salt, water의 키를 옵저빙하는 방법도 있겠지만, 여기서 만든 Solution 클래스를 내가 아닌 다른 누군가가 사용하려 한다면 그 원리를 모를 수도 있는 것이다. 계산 프로퍼티를 옵저빙할 수 있게 하려면 해당 키패스가 어떤 키에 의존하고 있음을 런타임에게 알려주어야 한다.

NSObject의 클래스 메소드인 +keyPathsForValuesAffectingValueForKey: 는 해당 클래스 내에서 특정 프로퍼티 키에 영향을 줄 수 있는 키패스의 집합을 리턴하는데, 이 메소드를 오버라이드해서 계산 프로퍼티가 의존하는 키들을 명시해줄 수 있다. 이 메소드를 구현해두면 salt나 water 값을 변경했을 때, concentration 키가 변경되었다는 통지를 보낼 수 있는 것이다.

+ (NSSet<NSString*> *)keyPathsForValuesAffectingValueForKeyPath:(NSString *)keyPath
{
  // 특정한 키패스에 대해서 이미 부모클래스에서 설정한 의존성이 있을 수 있으므로
  // 부모 클래스의 의존성을 먼저 구한 후, 추가적으로 의존성을 더해준다.
  NSSet<String*>* defaults = [super keyPathsForValuesAffectingValueForKeyPath:keyPath];
  if([keyPath isEqualToString:@"concentration"]) {
    defaults = [defaults setByAddingObjectsFromArray:@[@"water", @"salt"]];
  }
  return defaults;
}

참고로 정의된 키패스에 대해서 +keyPathsForValueAffecting<KeyPath>와 같은 식으로 개별 키 이름을 통한 메소드를 사용해서 제어할 수도 있다.

자동 통지 VS 수동 통지

기본적으로 특정한 프로퍼티가 KVC 호환 방식으로 정의되어 있다면, 프로퍼티에 대한 변경 통지는 KVO 호환 방식의 변경(setter를 이용하거나, setValue:forKey:를 사용)을 적용했을 때 자동으로 옵저버들에게 발송된다. 사실 이 방식은 수동으로 컨트롤할 수 있다. 통지를 보내는 방식을 수동으로 관리하는 경우는 1)빈번하게 변경되는 값에 대해서 꼭 필요한 상황에 한해서만 통지를 보내고 싶을 때, 2) 일련의 값들의 변경 통지를 그룹으로 묶어서 한 번에 처리하고 싶을 때 등의 상황에서 사용한다.

특정 키 패스에 대한 자동 통지 여부는 NSObject의 +automaticallyNotifiesObersversForKey:에 의해서 개별적으로 설정할 수 있다. (역시 개별 키에 대해서 +automaticallyNotifiesObersversFor<Key>의 형태로 구현해도 된다. 예를 들어 위의 예에서 water에 대한 자동 통지 여부는 다음과 같이 꺼 버릴 수 있다.

+ (BOOL)automaticallyNotifiesObserversForWater
{ return NO; }

이렇게하면 setWater: 를 통한 값 변경은 자동으로 통지되지 않으며, 이에 의존하고 있는 concentration 역시 자동으로 업데이트되지 않는다. 수동으로 통지를 보내려면 -willChangeValueForKey:, -didChangeValueForKey: 를 해당 값을 변경하는 앞/뒤로 붙여주어야 한다. 소금물에서 물을 빼는 경우는 없고, 물을 더하는 경우는 있을 수 있으므로 다음과 같이 addWater: 라는 메소드를 추가하고 구현해보자.

- (void)addWater:(float)w
{
  [self willChangeValueForKey:@"water"];
  _water += w;
  [self didChangeValueForKey:@"water"];
}

이 경우 자동으로 변경통지를 보내지 않는 water이지만, addWater:를 호출하여 변경한 경우에는 변경 통지를 받을 수 있게 한다. 이러한 수동 통지는 흔히 여러 키에 대한 변경의 통지를 하나로 묶을 때 사용된다.  이 때는 마치 괄호를 이중으로 쓰듯이 willChange... ~ didChange...를 짝으로 묶어서 감싸면 된다.

- (void)setBalance:(double)theBalance
{
  if(theBalance != _balance) {
    [self willChangeValueForKey:@"balance"];
    [self willChangeValueForKey:@"itemChanged"];
    _balance = theBalance;
    _itemChanged += 1;
    [self didChangeValueForKey:@"itemChanged"];
    [self didChangeValueForKey:@"balance"];
  }
}

 

to-many 관계의 각 원소를 추적하기

특정한 키에 대한 의존성을 설정할 수 있다 하더라도, 그것이 만능은 아니다. 예를 들어서 to-many 관계는 의존 키패스를 지원하지 않는다. 예를 들어서 Department라는 부서를 표현하는 클래스가 있고, 여기에 해당 부서 직원들의 전체 급여합을 계산하는 totalSalary라는 키가 있다. 각 직원은 Employee라는 클래스로 표현되고, 여기에는 개개인의 급여를 나타내는 salary라는 키가 있다고 하자.

totalSalary는 해당 부서 임직원의 salary 키의 합계이므로 “employees.@sum.salary” 라는 키패스로 표현할 수 있고, 이 키는 “employees.salary”에 의존한다고 할 수 있다. 하지만 employees가 to-many 관계인 이유로 이 의존성은 올바르게 동작하지 않는다. 대신에 totalSalary는 저장 프로퍼티로서 다음과 같이 구현되어야 한다. (이 코드는 애플 공식 문서의 내용을 일부 수정한 것이다.) 특히 직원을 추가/제거하는 시점이 중요한데, 직원을 추가하면 새로 추가된 직원의 “salary” 키에 대해 감시해야 하고, 직원이 제거될 때에는 옵저버를 해제해야 한다. 그리고 직원의 수 역시 전체 급여액에 영향을 미치므로 추가/제거 시점에 변경해야 한다.

/// setter에서 다른 값이 들어왔을 때만 통지를 보내도록 한다.
- (void)setTotalSalary:(NSNumber *)newTotalSalary
{
  if(self.totalSalary != newTotalSalary) {
    [self willChangeValueForKey:@"totalSalary"];
    _totalSalary = [newTotalSalary copy];
    [self didChangeValueForKey:@"totalSalary"];
  }
}

/// 총 급여값을 업데이트하는 메소드
- (void)updateTotalSalary
{
  [self setTotalSalary:[self valueForKeyPath:@"employees.@sum.salary"]];
}

/// 소속 직원의 급여 변경에 대한 KVO 통지를 받았을 때 처리
/// 총급여를 재계산한다. (그리고 변경됐으면 자동으로 통지...)
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object 
        change:(NSDictionary<NSKeyValueChangeKey, id> *)change
        context:(void*)context
{
  if (context == totalSalaryContext) {
    [self updateTotalSalary];
  }
  else {
    [super observeValueForKeyPaht:keyPath ofObject:object change:change context:context];
  }
}

/// 소속 직원들의 급여 변경에 대해서는 직원을 추가할 때 옵저버로 등록해야 한다.
/// 이 변경은 KVC 집합 변경자를 작성하여 적용한다.
/// 코드 상으로는 배열 프록시를 사용해서 직원을 추가/제거해야 하며,
/// 가장 간단하게는 NSArrayController를 사용하는 것이 좋다.
- (void)insertObject:(Employee*)employee inEmployeesAtIndex:(NSUInteger)index
{
  [employee addObserver:self forKeyPath:@"salary" 
            options:NSKeyValueObservingOptionNew 
            context:totalSalaryContext];
  [_employees addObject:employee atIndex:index];
  [self updateTotalSalary];
}

/// 소속 직원이 빠져나갈 때에는 옵저버를 해제해주어야 한다.
- (void)removeObjectFromEmployeesAtIndex:(NSUInteger)index
{
  Employee* employeeToLeave = [_employee objectAtIndex:index];
  [employeeToLeave removeObserver:self forKeyPath:@"salary"];
  [_employee removeObjectAtIndex:index];
  [self updateTotalSalary];
}

만약 코어데이터를 사용하고 있다면, 삽입이나 삭제에 있어서 직접적인 개입이 어렵다. 이 경우에는 코어데이터 컨텍스트의 변경이 발생했을 때 노티피케이션 센터를 통해서 통지를 받아, 각 값을 업데이트 하도록 처리하는 방법을 생각해볼 수 있다.

정리

이상으로 키밸류 옵저빙이 무엇이며 어떤식으로 동작하는지, 또 어떻게 적용하고 제어할 수 있는지에 대해 살펴보았다. 대부분의 경우 NSObject가 필요한 처리를 다 해주고 있으므로 계산 프로퍼티에 대한 의존키 설정 등으로 충분히 처리할 수 있지만, 마지막에서 살펴본바와 같이 to-many 관계에 있어서는 세세한 추적과 처리가 필요하다. 따라서 KVO를 잘 이해하고 활용하기 위해서는 KVO와 관련된 메소드 뿐만 아니라 집합 접근자/변경자와 같은 KVC 관련된 내용에 대해서 많은 연습과 연구가 필요하다.

참고자료