Core Data 개론

Apple이 UI와 독립적으로 Model layer에 툴로 코어 데이터를 제공하고 있다는 것은 iOS 혹은 MacOSX 어플리케이션 개발자라면 다 알 것이다. 근데 많은 사람들이 코어 데이터를 O/RM (Object-relational mapping)으로 혹은 SQL Wrapper 정도로 오해하기도 한다. 물론 SQL를 기본으로 사용하고 있기는 하지만 더 높은 차원의 추상화되어 있는 개념으로 생각해야한다. 만약 O/RM이나 SQL Wrapper를 기대하고 코어 데이터를 생각한다면 다른 것을 사용해야 할 것이다. 코어 데이터를 코코아 어플리케이션 정보 저장에 사용되는 코코아용 데이터 베이스 API로 생각하는 것도 오해 중 하나라고 할 수 있다. 디스크에 있는 저장소뿐만 아니라 흔히 데이터 오브젝트라고 불리는 메모리상의 모든 객체를 포함한다. 자바나 C#, 혹은 다른 객체 지향 언어로 작업하면 데이터 객체를 만드는 데 많은 시간이 소요되며, 작업이 자주 반복되는 특징이 있다. 코어 데이터는 대부분의 반복되는 코드를 제거해 어플리케이션의 비즈니스 로직이나 컨트롤러 계층에 집중할 수 있게 해준다.

그렇다면 코어 데이터는 무엇인가?

코어 데이터는 모델 레이어 기술이다. 간단히 말하면 코어 데이터는 디스크에 저장될 수 있는 객체 그래프이다. 코어 데이터는 몇 가지 컴포넌트들로 구성이 되어 있다. 대부분의 사례에서 사용될 수 있으며, 세업도 비교적 쉬운 편이다. 각각의 컴포넌트들은 상호 간에 묶여 있다. 각각의 컴포넌트의 개념을 설명하기 전에 아래 그림을 먼저 잘 봐두기 바란다.

출처 https://www.objc.io/issues/4-core-data/core-data-overview/

출처 https://www.objc.io/issues/4-core-data/core-data-overview/

위 그림은 코어 데이터 스택을 나타낸 것이다. 여기에서 스택이라 하면 여러 층의 레이어를 갖추고 수직적으로 데이터를 전달하는 구조를 말한다. 수직적이라는 말은 상위 레이어에서 시작해 하위 레이어로 전달이 진행된다는 의미이다. 데이터는 바로 하위 레이어나 중간에 위치한 레이어로 전달되지 않기 때문에 상위 레이어부터 차례로 접근해야 한다.

위 그림을 참고로 설명하자면 크게 세가지 레이어로 나눌 수 있다.

  • Managed Object Context 레이어
  • Persistent Store Coordinator 레이어
  • Persistent Store 레이어

상위 두 레이어는 메모리에서 데이터를 관리한다고 보면 되고, Persistent Store 레이어로 내려가면 Disk 메모리 영역으로 생각해두면 쉽다.

코어 데이터 메인 컴포넌트

먼저 쉬운 설명을 위해서 상위 개념으로 부터 실제 파일 시스템 영역 차례로 기술해보겠다.

NSManagedObject

전에 코어 데이터를 사용해 보았다면 가장 친근한 클래스 명이라 생각 되어진다. 코어 데이터를 사용하는 개발자가 가장 많이 접하게 되는 객체이기 때문이다. NSManagedObject는 Managed object model의 Entity를 서브 클래싱 받아서 소스 코드에서 사용되어 지는 Value Object인데, 이 객체는 위 그림 과 같이 특정 Managed Object Context 안에서 관리되어 진다고 할 수 있다.

NSManagedObjectContext

Managed object context는 자신이 가지고 있는 Managed object들의 변동 여부를 추적하는 것이 주된 역할이다. 그래서 -save라는 메서드를 호출하면, 변동되어진 Managed object에 한에서만 로컬 데이터 베이스에 변경 요청을 한다. 기본 적으로 코어 데이터 템플릿은 하나의 Managed object context를 사용하고 있지만, 더 복잡한 상황에서는 하나 이상의 Context를 각기 다른 thread에서 사용할 수 있다. Managed object context는 PersistentStoreCoordinator와 통신을 하며 로컬 데이터 베이스에 데이터를 불러오고나, 저장 혹은 삭제를 요청한다.

NSPersistentStoreCoordinator

Persistent Store Coordinator는 스택의 중간에 중개자의 역할을 NSManagedObjectContext와 NSPersistentStore 객체 사이에서 한다. 자체적으로 Cache 영역이 존재하기 때문에 로컬 데이터 베이스에 자료를 요청하기 전에 cache 된 것이 있다면 디스크 메모리까지 접근하지 않고 cache 영역에서 데이터를 활용한다.

NSPersistentStore

Persistent Store은 실제 SQLite 데이터 베이스와 Coordinator 사이에서 데이터 베이스 쿼리 코드를 전환해주고 실 디비와 통신하는 Wrapper의 역할을 한다. 각 데이터 베이스 별로 존재하며 새로운 데이터 베이스를 사용하고자 한다면 NSPersistentStore를 상속 받아 상용해야 한다.

실제 데이터 가지고 오기

사용자가 로컬 데이터 베이스에 있는 데이터를 가지고 오기 위해서 여러가지 과정을 걸치게 된다.

  1. NSManagedObjectContext: 가장 context 안에 자기고 있는 데이터가 있는지 조사한다. 만약 없다면 한단계 아래 레이터에 데이터를 요청한다. 해당 레이어는 임시 메모리에 해당된다.
  2. NSPersistentStoreCoordinator: 위에서 잠깐 언급 했지만 NSPersistentStore를 통해 데이터를 요청하기 전에 자신이 가지고 있는 cache를 먼저 확안하는 작업을 걸친다.
  3. NSPersistentStore: 이 레이어는 직접 디비 쿼리로 데이터 베이스에 자료를 요청하는 작업을 하게 되므로 이 안계 부터는 Disk 메모리를 사용한다고 보면 된다.

위 과정은 NSFetchRequest를 사용하여 가지고 오는 경우가 아닌 objectIdentifier를 가지고 오는 경우를 말한다. NSFetchRequest를 사용하여 데이터를 요청하게 되면 항상 NSPersistentStore 레이어까지 내려가 디스크 메모리에 접근한다고 봐야한다.

XCode가 제공하는 기본 템플릿 말고 독립적으로 코드 사용하기

Xcode 프로젝트를 새로 생성할 때에 ‘코어 데이터 사용하기’라는 옵션을 체크하고 생성하게 되면 .xcdatamodeld라는 확장자를 가진 XML 파일과 함께 AppDelegate 파일에 코어 데이터 관련되 기본 템플릿이 추가되어 프로젝트가 생성이 될 것이다. 아마 많은 사용자들은 제공 해주는 코드를 그대로 사용 할 것으로 생각되어 진다. 하지만 위에서 배운 개념들을 생각하며 AppDelegate에서 분리된 PersistentStack이라는 파일을 통해 코어 데이터를 사용해보는 코드는 다음과 같다. (아래 방법으로 코어 데이터를 세팅 할 때에는 새로운 프로젝트를 생성할 때에 ‘코어 데이터 사용하기’ 체크를 푼다음에 직접 .xcdatamodelId 파일을 추가하며 아래 클래스를 사용해야 한다.)

 

  1. // 헤더 파일
  2. #import <Foundation/Foundation.h>
  3. @import CoreData;
  4.  
  5. #pragma mark – Interface
  6.  
  7. @interface SLKPersistentStack : NSObject
  8. @property(nonatomic, strong, readonly) NSManagedObjectContext *managedObjectContext;
  9. + (instancetype)sharedInstance;
  10. @end
  11.  
  12. // 구현 파일
  13. #import "SLKPersistentStack.h"
  14.  
  15. #pragma mark – Interface extension
  16.  
  17. @interface SLKPersistentStack ()
  18.  
  19. @property(nonatomic, strong, readwrite) NSManagedObjectContext *managedObjectContext;
  20. @property(nonatomic, strong) NSURL *modelURL;
  21. @property(nonatomic, strong) NSURL *storeURL;
  22.  
  23. @end
  24.  
  25. #pragma mark – Implementation
  26.  
  27. @implementation SLKPersistentStack
  28.  
  29. #pragma mark [Getters]
  30.  
  31. - (NSManagedObjectModel *)managedObjectModel{
  32.     return [[NSManagedObjectModel alloc] initWithContentsOfURL:self.modelURL];
  33. }
  34.  
  35. - (NSURL*)storeURL{
  36.     NSURL* documentsDirectory = nil;
  37.     documentsDirectory = [[NSFileManager defaultManager]
  38.                           URLForDirectory:NSDocumentDirectory
  39.                           inDomain:NSUserDomainMask
  40.                           appropriateForURL:nil
  41.                           create:YES
  42.                           error:NULL];
  43.  
  44.     return [documentsDirectory URLByAppendingPathComponent:@"db.sqlite"];
  45. }
  46.  
  47. - (NSURL*)modelURL{
  48.     return [[NSBundle mainBundle] URLForResource:@"SLKCodingExercise"
  49.                                    withExtension:@"momd"];
  50. }
  51.  
  52. #pragma mark [Constructors]
  53.  
  54. + (instancetype)sharedInstance{
  55.     static id sharedInstance = nil;
  56.     static dispatch_once_t onceToken;
  57.  
  58.     dispatch_once(&onceToken, ^{
  59.         sharedInstance = [[[self class] alloc] init];
  60.     });
  61.  
  62.     return sharedInstance;
  63. }
  64.  
  65. - (instancetype)init{
  66.     if (self = [super init]) {
  67.         [self p_setupManagedObjectContext];
  68.     }
  69.  
  70.     return self;
  71. }
  72.  
  73. - (void)p_setupManagedObjectContext{
  74.     self.managedObjectContext = [[NSManagedObjectContext alloc]
  75.                                  initWithConcurrencyType:NSMainQueueConcurrencyType];
  76.     self.managedObjectContext.persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc]
  77.                                                             initWithManagedObjectModel:[self managedObjectModel]];
  78.  
  79.     NSError *error;
  80.     [self.managedObjectContext.persistentStoreCoordinator
  81.                                 addPersistentStoreWithType:NSSQLiteStoreType
  82.                                 configuration:nil
  83.                                 URL:self.storeURL
  84.                                 options:nil
  85.                                 error:&error];
  86.     if (error) {
  87.         NSLog(@"error: %@", error);
  88.     }
  89.  
  90.     self.managedObjectContext.undoManager = [[NSUndoManager alloc] init];
  91. }
  92.  
  93. @end

 
위 클래스는 싱글톤 패턴을 사용하여 만들어 보았다. 그 이유는 프로젝트 소스 코드 여러 군데에서 NSManagedObjectContext를 필요로 할 것이기 때문이다. 하지만 하나이상의 Context를 사용하게 된다면 다른 방법을 생각해봐야 할 것이다. Managed Object Model은 위에서 언급한 .xcdatamodelId 파일에 해당되는 객체이다.

마치며…

해당 글은 코어 데이터의 기본적이 구조와 개념에 대한 내용만을 다루어 보았다. 멀티 스레드 환경에서의 코어 데이터 적용에 대한 부분과 쉽게 읽어지는 코드를 위해서는 꼭 필요한 과정일 것이다. iOS 환경이나 OS X 환경에서 기본적으로 로컬 디비를 사용하겠다고 한다면 코어 데이터를 꼭 한번 고려할텐데 좀 더 기본에 충실하게 무엇이 무엇인지는 알고 사용하자는 의미에서 글을 써보았다. 앞으로 여유가 된다면 코어 데이터의 성능관련 부분과 멀테 스레드 환경에서의 코어 데이터 사용하기 부분을 올려보고자 한다. 혹시 미흡하거나 잘못 된 부분이 있다면 댓글을 남겨 주시길.

참조
  1. https://www.objc.io/issues/4-core-data/core-data-overview/
  2. 코어 데이터 맥 OS X과 아이폰 개발자를 위한 -에이콘 출판사-