본문 바로가기
아이폰 개발/ios 개념&튜토리얼

iOS 코어데이터 예제(feat: 기초 개념)

by 인생여희 2020. 12. 13.

 

iOS 코어데이터 예제(feat: 기초 개념)

 

iOS 코어 데이터 방식

코어 데이터는 데이터 베이스의 빠른 속도와 효율성, 객체 직렬화의 객체지향 이점을 모두 한곳에 모아두었다.

 

 

엔티티와 관리 객체

특정 객체(User,Book등..)처럼 모델 객체의 실제 인스턴스 작업을 할 때는 관리 객체의 인스턴스를 다룬다. 이런 객체는 NSManagedObject 클래스나, NSManagedObject를 상속한 서브클래스의 인스턴스가 된다. 모델러에서 특정 커스텀 서브클래스를 지정하지 않는다면 아래 코드 처럼 key value coding으로 객체의 속성에 접근한다.

 

    //NSManagedObject 인스턴스 불러왔다고 가정
    NSManagedObject *book;
    
    //조회
    NSString *name = [book valueForKey:@"name"];
    NSString *type = [book valueForKey:@"type"];
    
    //입력
    [book setValue:@"name" forKey:@"core data"];
    [book setValue:@"type" forKey:@"it"];

 

NSManagedObject의 서브클래스를 제공해서 관리 객체에 접근자 메소드와 속성을 노출 할 수도 있다.

    //NSManagedObject 인스턴스 불러왔다고 가정
    NSManagedObject *book;
    
    //조회
    NSString *name = [book name];
   
    //입력
    book.name = @"core data";

 

관계

모델링할 때, 일대다, 다대다, 일대일 같은 모델링용어가 있다. 예를 들어, 의사 환자의 관계는 일대다 이므로, 이에 대한 역관계는 다대일이다. 이런 관계를 데이터 모델러에서 모델링하면 양 관계를 명시적으로 모델링해야 하고, 하나에 대한 역관계도 설정해야 한다. 역관계를 명시적으로 설정함으로써, 코어 데이터가 데이터의 무결성을 자동으로 유지할 수 있다. 환자에게 특정 의사를 연결했다면 환자는 의사의 리스트에 자동으로 등록된다. 

각 관계의 이름을 지정해서 엔티티의 속성에 비슷하게 노출 되게 한다. 위에서 말한 key value 방식을 사용해도 되고, 커스텀 서브클래스에 자신만의 접근자와 속성 선언을 제공할 수도 있다.

 

    //Patient 인스턴스 불러왔다고 가정
    Patient *aPatientObject;
    
    //환자의 담당 의사 구하기
    Doctor *aDoctorObject = [aPatientObject valueForKey : @"doctor"];
    
    //다른 환자 객체 이미 불러왔다고 가정
    Patient *anotherPatientObject;
    
    //위의 첫번째 환자의 의사를 다른 환자에게도 배정
    anotherPatientObject.doctor = aDoctorObject;
    
    //역관계는 자동으로 설정된다.
    NSLog(@"의사의 환자들 : %@" , [aDoctorObject patients])
    
    
    /*
     
     의사의 환자들 (aPatientObject,  anotherPatientObject )
     
     */

 

코어 데이터는 대다 (to-many) 관계를 포함한 객체 컬렉션의 어떤 순서도 유지하지 않는다는 점을 주의해야 한다. 순서가 중요하다면 각 객체에 인덱스를 기록하는 등 개발자 스스로 관리해야 한다.

 

코어 데이터로 작업을 하면 MySQL, MS SQL 처럼 유일 식별자를 모델링하거나 관련 레코드 간 테이블 조인을 다룰 필요가 없다. 코어 데이터는 이런 것을 백그라운드에서 처리하므로 개발자는 객체 간 관계만 잘 정의하면 된다. 그러면 프레임워크가 자동으로 나머지 부분을 처리한다.

 

관리 객체 콘텍스트(ManagedObjectContext)

코어 데이터와 관리 객체를 다루게 되면 관리 객체 콘텍스트로 알려져 있는 콘텍스트 범주에서 작업을 하게 된다. 이 콘텍스트는 데이터를 디스크(sqllite)에 영구 저장하고, 객체의 컨테이너인 것처럼 동작한다. 개념상으로는 PC의 문서를 가지고 작업하는 것과 비슷하다. 

 

관리 객체 콘텍스트도 문서와 비슷하게 작동한다. 저장돼 있는 데이터를 필요한 시점에 불러올 책임이 있고, 객체에 가해지는 변화를 메모리상에 기억하며, 저장할 시점이 되면 디스크에 변한 내용을 기록한다. 개발자가 직접 관리 객체 콘텍스트에게 저장하라는 신호를 주지않으면 콘텍스트 내부 객체에 생긴 변화는 모두 임시적이고, 디스크에 영향을 주지 않는다. 

 

하지만 일반적인 문서와달리 한번에 여러개의 관리 객체 콘텍스트와 동시에 작업을 할 수가 있다. 모두 동일한 데이터에 관련돼 있더라도 마찬가지다. 예를 들어 동일한 환자 객체를 두 개의 콘텍스트에 불러올 수 있고, 하나의 환자에만 변화를 줄 수 있다. 첫번째 콘텍스트의 내용을 저장하라고 신호하기 전까지는 객체에 생긴 변화가 다른 콘텍스트에 영향을 주지 않는다. 저장 신호가 내려지면 두번째 콘텍스트에 데이터 변화가 생겼음을 알리고, 원한다면 다시 불러오게 할 수 있다. 

 

객체 불러오기

관리 객체 콘텍스트는 NSFetchRequest 객체로 디스크에서 객체를 불러오기도 한다. 불러오기 요청은 최소한 엔티티 이름을 갖고 있다. 원한다면 모든 환자 정보를 영구 저장소에서 불러올 수 있는데, 불러올 엔티티에 Patient를 지정하고, 관리 객체 컨텍스트에게 요청하면 된다. 관리객체 컨텍스트는 실행결과를 배열로 반환한다. 위에서도 말했듯이, 결과를 원하는데로 정렬할려면 프리디케이트나 다른 처리를 해주어야 한다.

 

폴팅과 유니큐잉

코어데이터는 최소한의 메모리 사용을 위해서, 폴팅 기술을 사용한다. 예를들어 Patient 를 메모리에 불러왔을때, 환자에게 지정된 의사 객체에 접근하기 위해 Doctor 객체도 불러와야한다. 그리고 의사에 종속된 모든 환자정보도 불러와야 한다. 이렇게 되면 수천개의 자료를 불러와야 한다. 

코어데이터는 이런 문제를 해결하기 위해서 개발자가 질의한 관리객체 하나만 불러오고 관계는 폴트에 설정한다. 환자의 의사 이름을 질의하는 등 관계에 접근하려고 하면 폴트가 실행되고, 코어데이터가 요청한 객체를 불러온다. 마찬가지로 새롭게 불러온 의사 객체의 관계가 폴트에 설정돼 필요할 때 불러올 수 있다. 이 과정은 자동으로 처리되므로 개발자가 신경 쓸 부분은 없다.

관리객체콘텍스트는 한번 객체를 로딩하고 나면 뒤이어 오는 불러오기 시도에는 항상 이미 존재하는 인스턴스를 반환한다.

 

    Patient *aPatientObject;        //첫번째 불러오기 요청
    
    Doctor *aDoctor = aPatientObject.doctor;
    
    
    
    Patient *bPatientObject;        //두번째 불러오기 요청
    
    Doctor *bDoctor = bPatientObject.doctor;


    /*
     두 환자가 동일한 의사를 공유한다면 폴트가 실행된 수 반환하는 의사 인스턴스는 언제나 동일하다.
     */
    
    if(aDoctor == bDoctor){
        NSLog(@"두 환자는 같은 의사 객체를 공유한다.")
    }
    

 

이 과정을 유니큐잉이라고 한다. 어떤 관리 객체 콘텍스트든 단 하나의 객체 인스턴스만을 제공한다.

 

 

영구저장소와 영구 저장소 코디네이터

데이터는 영구 저장소(persistent store) 내부 디스크에 저장된다. ios 디바이스에서는 일반적으로 SQLite 저장소를 말한다. 바이너리 저장소나 커스텀 타입을 지정할 수 있지만, 이렇게 하면 모든 객체 그래프를 메모리에 불러와야 하기 때문에 제한된 자원을 가진 디바이스에서는 문제가 되기 쉽다. 

영구저장소에 직접적으로 명령을 보내거나 어떻게 데이터를 저장하는지 걱정할 필요가 없다. 관리객체콘텍스트와 영구 저장소 코디네이터의 관계에만 의지하면 된다.

영구저장소 코디네이터는 관리 객체 콘텍스트에 대한 중재자 처럼 동작한다. 코디네이터에게 여러개의 영구저장소와 상호작용하라고 지시할 수도 있어, 코디네이터가 관리객체 콘텍스트가 접근하는 저장소 연합을 노출하기도 한다.

 

 

코어데이터 스택설정

영구저장소에 들어 있는 데이터를 다룰 때는 객체의 스택을 만들 필요가 있다. 스택 하단은 디스크상 실제 영구 저장소이고, 그 위에 저장소와 다음 레벨을 이어주는 영구 저장소 컨트롤러, 관리 객체 콘텍스트가 차례로 온다. 또한 하나의 코디네이터에 하나 이상의 영구 저장소와 관리 객체 콘텍스트를 둘 수도 있다.

 

ios coredata stack

 

 

 

앱델리게이트는 coredata의 .xcdatamodeld 파일에 담겨 있는 정보인 관리 객체 모델을 추적한다. 또한 영구 저장소 코디네이터와 관리객체 콘텍스트의 참조 모델을 갖고 있다. applicationDocumentsDirectory 는 데이터가 디스크에 저장돼야 하는지 결정하는데 사용한다. 

#import <UIKit/UIKit.h>

@interface AppDelegate : NSObject <UIApplicationDelegate> {

}

@property (nonatomic, retain) IBOutlet UIWindow *window;

@property (nonatomic, retain, readonly) NSManagedObjectContext *managedObjectContext;
@property (nonatomic, retain, readonly) NSManagedObjectModel *managedObjectModel;
@property (nonatomic, retain, readonly) NSPersistentStoreCoordinator *persistentStoreCoordinator;

- (void)saveContext;

- (NSURL *)applicationDocumentsDirectory;

@property (nonatomic, retain) IBOutlet UINavigationController *navigationController;

@end

 

applicationDocumentsDirectory 메소드는 도큐먼트의 경로를 반환한다.

 

/**
애플리케이션의 도큐먼트 경로를 반환 한다.
 */
- (NSURL *)applicationDocumentsDirectory
{
    return [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
}

 

다음으로 persistentStoreCoordinator 메소드는 영구 저장소 코디네이터를 설정해서 앱의 도큐먼트 디렉토리에 위치한 SQLite 저장소 파일에 접근할 수 있게 한다. 영구 저장소는 managedObjectModel 메소드가 제공한 모델을 사용해서 초기화 된다.

 

/**
 앱에 대한 persistentStoreCoordinator 를 반환합니다.
 코디네이터가 아직 존재하지 않으면 생성되고 응용 프로그램의 저장소가 여기에 추가됩니다.
 */
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator
{
    if (__persistentStoreCoordinator != nil)
    {
        return __persistentStoreCoordinator;
    }
    
    NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"TemplateProject.sqlite"];
    
    NSError *error = nil;
    __persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
    if (![__persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error])
    {
        NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
        abort();
    }    
    
    return __persistentStoreCoordinator;
}

 

 

managedObjectModel 메소드는 .xcdatamodeld 파일에서 생성한 NSManagedObjectModel 객체를 반환한다. 프로젝트를 컴파일 하면 xcdatamodeld 데이터 모델이 .momd 리소스로 컴파일 되고 앱의 번들에 저장된다.

 

/**
 응용 프로그램의 관리 개체 모델을 반환합니다.
 모델이 아직없는 경우 애플리케이션의 모델에서 생성됩니다.
 */
- (NSManagedObjectModel *)managedObjectModel
{
    if (__managedObjectModel != nil)
    {
        return __managedObjectModel;
    }
    NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"TemplateProject" withExtension:@"momd"];
    __managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];    
    return __managedObjectModel;
}

 

스택의 상단에는 managedObjectContext 메소드가 보인다. 이 메소드는 영구 저장소 코디네이터를 사용해 콘텍스트를 설정한다.

 

/**
 응용 프로그램에 대한 관리 개체 컨텍스트를 반환합니다.
 컨텍스트가 아직 존재하지 않으면 작성되고 애플리케이션의 영구 저장소 코디네이터에 바인드됩니다.
 */
- (NSManagedObjectContext *)managedObjectContext
{
    if (__managedObjectContext != nil)
    {
        return __managedObjectContext;
    }
    
    NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
    if (coordinator != nil)
    {
        __managedObjectContext = [[NSManagedObjectContext alloc] init];
        [__managedObjectContext setPersistentStoreCoordinator:coordinator];
    }
    return __managedObjectContext;
}

 

파일 위쪽에 있는 awakeFromNib 메소드를 보면 managedObjectContext 에 대한 RootViewController 프로퍼티를 설정하는 것을 확인 할 수 있다. 예제 코드에서는 관리 객체 모델, 영구저장소 코디네이터, 콘텍스트에 대한 설정이 연속적으로 발생한다.

 

- (void)awakeFromNib
{
    RootViewController *rootViewController = (RootViewController *)[self.navigationController topViewController];
    rootViewController.managedObjectContext = self.managedObjectContext;
}

 

마지막으로 applicationWillTerminate 메소드가 saveContext 메소드를 호출해서 관리 객체 콘텍스트 내부 객체에 변화가 있는지 확인하고 변화가 있다면 저장한다. 이는 컨텍스트에 생긴 변화는 앱이 종료될때 저장됨을 의미한다. 

 

- (void)applicationWillTerminate:(UIApplication *)application
{
    //응용 프로그램이 종료되기 전에 응용 프로그램의 관리되는 개체 컨텍스트에 변경 사항을 저장합니다.
    [self saveContext];
}

 

관리 객체 콘텍스트를 설정하고 나면 루트 뷰 컨트롤러에 전달된다. 이 뷰 컨트롤러에는 실제로 관련 데이터를 불러오고 표시할 수도 있다. 

 

다음 포스팅에서는 이번 포스팅에서 다룬 개념으로 예제 프로젝트를 만들어 본다.