[Cocoa] 코어데이터 스택을 수동으로 세팅하기

코어데이터 코어데이터 코어데이터. 쉽지도 않은 내용인데 이 블로그에서 최근에 코어데이터를 지긋지긋하게 많이도 다루는 것 같다. ㅠㅡㅠ 하지만 언젠가는 피가되고 살이될 코어데이터에 대한 내용이다.

이미 “간단한” 저장은 아주 손쉽게 Keyed Archiver를 사용하여 인코딩한 객체를 파일로 바로 저장하는 것은 살펴보았다. 하지만 만약, 저장한 주소록에 사람이 수백만명이라면 엄청나게 많은 데이터가 앱이 실행될 때 한번에 메모리로 로드되어 올라갈 것이다. (이것이 아카이빙으로 내용을 저장할 때의 한계이다. 많은 데이터는 결국 한 번에 로딩해서 안고 있어야 하는 부담이 있다.)

하지만 코어데이터는 굉장히 빠르게 영구저장소를 계속해서 액세스하고, 자동으로 차등저장 및 로딩을 지원하기 때문에 데이터세트가 어느 정도까지는 커져도 괜찮다. (적어도 나는 그렇게 알고 있다.)

iOS라면 UIManagedDocument를 사용하면 문서파일 자체를 코어데이터 영구저장소 파일(데이터베이스 파일)로 바로 사용할 수 있다. 이 내용은 이미 살펴본 바가 있는데, 문제는 NSManagedDocument 라는 것은 아직 공식적으로 존재하지 않는 클래스이다. (아 이런…) 결국 코코아 앱을 시작할 때 코어데이터를 적용해주지 않으면… 콸콸콸콸…

update: macOS에서는 NSPersistentDocument 라는 클래스가 있어서 코어데이터와 NSDocument를 긴밀하게 통합하여 사용할 수 있다.

프로젝트를 처음 생성하면서 코어데이터에 체크를 하지 않고 만들다가, 나중에 저장과 관련된 기능을 코어데이터로 바꾸기 원한다면, 프로젝트 내에 코어데이터 세팅을 직접 만들어야 한다. 물론 새로운 프로젝트를 생성해서 미리 만들어지는 코드를 가져와서 사용해도 되긴하는데, 공부의 목적으로 어떤 식으로 코어데이터 스택을 생성하고 세팅하는지 살펴보기로 하자. 코어데이터가 적용되지 않은 프로젝트에 추가해야 하는 것은 다음과 같다.

  1. 코어데이터 모델 파일 (.momd)
  2. NSMangedObject의 서브 클래스
  3. 코어데이터 스택

코어데이터 모델 파일

프로젝트에서 File > New.. 를 통해서 새 파일을 추가하고 코어데이터 모델 파일을 선택한다. 그러면 새로운 코어데이터 모델 파일이 프로젝트에 추가되고, 편집도 할 수 있다.

NSManagedObject의 서브 클래스

코어데이터 모델 편집기에서 엔티티를 작성했다면, 해당 엔티티를 클래스로 표현할 NSManagedObject 클래스를 추가해야 한다. 이 과정은 모델 편집기에서 엔티티를 선택하고 Editor > Create Subclass of NSManagedObjectModel… 을 선택하면 자동으로 생성해준다.

Xcode9에서는 클래스 파일을 명시적으로 만들지 않아도 컴파일 타임에 자동으로 생성할 수 있다. 수동으로 클래스를 만들어 사용할 경우에는 엔티티의 CodeGen 옵션을 None으로 만들어두자.

코어데이터 스택

코어데이트 스택을 구성하는 것은 결국 컨텍스트 객체를 얻고, 그 컨텍스트가 저장소와 연결될 수 있도록 셋업하는 것이다. 그러면 컨텍스트로부터 저장소까지의 연결고리를 다음과 같이 생각해보자.

  1. 컨텍스트(managed object context)는 저장을 위해 영구 저장소 코디네이터를 필요로 한다. 생성 후 persistentStoreCoordinator 속성에 코디네이터 객체를 지정해주어야 한다.
  2. 코디네이터를 만들기 위해서는 관리객체모델이 필요하다. 또한, 코디네이터를 생성한 후에는 코디네이터의 저장소 속성을 셋업해야 한다.
  3. 관리객체모델은 NSManagedObjectModel클래스의 인스턴스로 생성한다. 이 객체를 생성하기 위해서는 처음에 만들었던 코어데이터 모델 파일(momd)이 필요하다.

그렇다면 의존하고 있는 관계를 역으로 세워서 다음과 같은 순서로 코어데이터 스택을 준비한다.

  1. 관리 객체 모델 인스턴스를 생성한다.
  2. 영구저장소 코디네이터 객체를 1을 이용하여 생성한다.
  3. 영구저장소 코디네이터에 저장소 파일을 설정한다.
  4. 컨텍스트 객체를 생성한다.
  5. 컨텍스트 객체에 영구저장소 코디네이터를 연결한다.

예를 들어 학급의 학생을 관리하는 앱을 만드는 중이라고 하자. 그래서 Students 라는 이름으로 코어데이터 파일을 생성하고 (생성했을 때의 확장자는 .xcdatamodel 이다. 이 파일이 “컴파일”되어서 .momd 파일이 된다.) 엔티티를 구성한다. 그리고 편의상, 앱 델리게이트에서 컨텍스트를 제공하는 것으로 하겠다. 또, 여기서의 코드는 모두 Objective-C이다.

참고로, 이 글이 쓰여진 한참~이후에는 NSPersistentStoreContainer 라는 것이 등장하여 스택 구성을 완전 간편하게 만들어버렸다. 이 부분에 대해서는 별도의 포스팅으로 다시 안내하도록 하겠다.

 

먼저 앱 델리게이트의 인터페이스에서 컨텍스트를 참조할 수 있도록 접근자를 추가하자.

/// in AppDelegate.h

@interface AppDelegate: NSObject<NSApplicationDelegate>
...
@property (readonly, nonatomic) NSManagedObjectContext* context;
@end

그리고 구현부에서는 클래스 내부에서만 액세스할 수 있는 코디네이터 및 모델 객체에 대한 접근자 선언을 추가할 것이다.

/// in AppDelegate.m

@interface AppDelegate ()
@property (readonly, nonatomic) NSManagedObjectModel* managedObjectModel;
@property (readonly, nonatomic) NSPersistentStoreCoordinator* coordinator;
@end

@implementation AppDelegate
@synthesize context=_context, managedObjectModel=_managedObjectModel, coordinator=_managedObjectModel;

...
@end

모든 접근자는 “느긋하게” 초기화되는 형태로 구현하며, 따로 initialize나 다른 이니셜라이저를 오버라이딩하지 않을 것이다.

모델 셋업

다음은 모델 셋업 코드이다. 모델은 initWithContentsOfURL: 을 사용해서 momd 파일을 읽어서 그 내용으로 초기화한다.

- (NSManagedObjectModle *)managedObjectModel
{
  if(!_managedObjectModel) {  // 1
    const NSString* modelName = @"Students";
    NSURL* url = [[NSBundle mainBundle] urlForResource:modelName withExtension:@"momd"]; // 2
    _managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:url];
  }
  return _managedObjectModel;
}
  1. 모델 인스턴스를 위한 내부 변수는 _managedObjectModel 이며 이 값은 기본적으로 nil로 초기화되어 생성된다. 따라서 이 접근자는 일종의 computed property로 최초 액세스시에 초기화하고, 그 이후에는 초기화된 값을 그대로 사용한다.
  2. “momd”에 ‘.‘을 붙이지 않는다.

모델은 이런식으로 초기화될 것이니 염려하지 말고 영구 저장소 코디네이터를 셋업하자.

영구 저장소 코디네이터 (Persistent Store Coordinator)

코디네이터는 모델에 전적으로 의존하기 때문에 initWithManagedObjectModel:을 통해서 초기화된다. 저장소 파일은 생성한 이후에 추가한다. 그 이유는 코디네이터 1개가 여러 개의 저장소를 한꺼번에 관리할 수 있기 때문이다.

저장소는 파일을 생성하는 것이 아니라 (생성해 두어도 상관은 없으나…) 파일의 위치를 주는 것이 중요하다. 파일의 위치는 라이브러리 내부나 사용자문서 파일등을 사용하면 되는데, 여기서는 라이브러리 디렉토리를 사용하도록 하자.

/// 
- (NSPersistentStoreCoordinator *)coordinator
{
  if(_coordinator) {
    _coordinator = [[NSPersistentStoreCoordinator alloc] 
                      initWithManagedObjectModel:[self managedObjectModel]];
    NSError *error = nil;
    [_coordinator addPersistentStoreWithType:NSSQLiteStoreType
                  configuration:nil
                  URL:[self storeURL]
                  options:nil
                  error:&error];
    if(error){
      // 에러 발생!
    }
}

라이브러리내 위치는 파일매니저를 통해서 얻을 수 있다.  프로퍼티로 만들어도 되지만, 어차피 딱 한 번만 불릴 것이라, 그냥 메소드형식으로만 작성한다. (내부 인터페이스에도 추가해줘야 한다. Swift를 같이 하면서 종종 이걸 까먹게 되더라.)

- (NSURL *)storeURL
{
  NSFileManager* filemanaer = [NSFileManager defaultManager];
  NSURL* dirURL = [[filemanager URLsForDirectory: NSLibraryDirectory
                               inDomains:NSUserDomainMask] firstObject];
  return [dirURL URLByAppendingPathComponent:@"store.sqlite"];
}

관리 객체 컨텍스트

자 이제 거의 다 왔다. 다음은 컨텍스트를 셋업하면 된다. concurrency type은 컨텍스트가 메인 큐를 사용할 것인지 별도의 백그라운드 큐를 사용할 것인지를 정하는데, 그냥 메인큐로 쓰기로 했다. 이후에는 만들어둔 코디네이터를 할당해주면 된다.

///
- (NSManagedObjectContext*) context
{
  if(!_context) {
    _context = [[NSManagedObjectContext alloc] 
                 initWithConcurrencyType: NSMainQueueConcurrencyType];
    [_context setPresistentStoreCoordinator:[self coordinator]];
  }
  return _context;
}

샘플 프로젝트

프로젝트 소스 받기

링크에 첨부한 샘플 프로젝트는 Xcode가 만들어주는 코어데이터 설정 없이 위 과정을 거쳐서 손수 코어데이터 스택을 셋업하여 코코아 바인딩으로 구현한 앱이다.