본문 바로가기

Project

Coredata Background 처리에 관하여


격주 릴리즈의 바쁜 스케쥴안에서 시간을 쪼개 코어 데이터 처리 로직을 백그라운드에서 처리하려는 작업을 조금씩 진행시켜 이번버젼에야 겨우 적용할수 있었다. 여기에 약 6개월간에 걸쳐 내가 겪은 시행착오에 관한 기록을 남긴다.

1. 기존설계의 문제점

  모든 처리를 MainThread에서 하도록 구현되어있었기 때문에 화면이 멈추는 경우가 자주 있었다.
Main Thread에서 처리하지 않으면 안되었던 이유는 UI의 변경은 반드시 Main Thread에서 하지 않으면 안되기때문이다.


모든 작업을 main Thread에서 처리하더라도 발생하는 한가지 이슈가 있었는데,

비동기 리퀘스트(Asynchronous Request)와 Block을 이용할때 request시에 취득한 NSManagedObject는 respoonse때에 다시 사용할경우 문제가 생길 가능성이 있다.

ex)

a. Request - A 유저 데이터를 이용하여 request를 보냄
b. A 유저데이터가 삭제됨
c. Response - A 유저 데이터를 그대로 이용하여 작업 하지만 이미 삭제되어있기때문에 fault된상태이기때문에
crash 를 유발할수있다.

이때문에 response블록에서는 사용될 데이터는 moc로부터 새로 취득하여 사용하지 않으면 안되었었다.



2. 개선 - 2개의 NSManagedObjectContext

 기동시에 데이터를 정기적으로 체크하지 않으면 안되는 로직이 있어서 해당 작업을 배치로 묶어서 백그라운드에서 처
리하도록 수정하였다.
 수정방법은 배치에 사용될 NSManagedObjectContext(이하 MOC)는 직접 DB(persistent)로부터 취득하여 생성하여
main thread사용중인 moc에 영향이 가지않도록 하였다.

 이 작업을 진행하며 발견된 이슈가 있는데

- Fetch Result Controller(이하 FRC)의 델리게이션은 Main Thread에서 일어나야한다(UI)
FRC델리게이션으로인해 table view의 갱신이 일어나게되고 이 작업은 항상 main thread에서 실행되어야만 하는 문제가 있었다.

하지만 데이터의 저장타이밍(moc save)에 FRC에 대한 델리게이션이 일어나기때문에 save타이밍에
직접 merge를 구현하여 main thread에서 실행되도록 수정하였다.

save시에 notification(NSManagedObjectContextDidSaveNotification)을 observer하여
MainThread에서 noti데이터로부터 main context에 merge하도록 수정하였다

....

            [[NSNotificationCenter defaultCenter] addObserver:moc selector:@selector(merge:) name:NSManagedObjectContextDidSaveNotification object:moc];
....

- (void)merge:(NSNotification *)notification

{

......

    dispatch_async(dispatch_get_main_queue(), ^{

        [mainManagedObjectContext mergeChangesFromContextDidSaveNotification:notification];

    });

....



3. 개선 - 2개의 Context 그리고 GCD QUEUE

모든 데이터의 처리는 아래의 2개의 context를 이용하여 데이터를 처리하기로 결정하엿다.

a. Read 및 UI용 Main Context
FRC에 설정되거나, viewDidLoad시에 사용될 데이터을 위해서만 사용된다.
이 MOC에대한 수정 및 Save는 금지된다.

b. 데이터 수정용 private context
수정이 필요한 NSManagedObject들은 이 MOC로부터 취득한다.
UI에서는 절대 사용하지 않도록한다.

위의 콘텍스트를 두가지로 나눈이유는 merge conflict 머지경합을 방지하기 위해서이고
기본 비지니스로직의 처리는 private MOC의 performBlock 안에서 하도록 강제한다.

만약 로직에 UI변경이 필요하다면 

dispatch_sync(dispatch_get_main_queue(), ^{ UI 변경 });

을 이용하여 처리하도록 한다.

 이렇게 함으로서 자연적으로 처리가 비동기, 백그라운드에서 실행하게 되며 기본적으로 UI작업과 독립적으로 이루어지기 때문에 앱이 멈추는 경우는 거의 사라졌다.

이 작업을 진행하면서 발견한 몇가지 이슈가 있는데 

-  MOC의 생성과 사용은 항상 같은 Thread이어야만 한다.
GCD queue를 이용해 block을 dispatch 했을경우, block내의 moc가 다른 thread에서 실행될경우에 데이터가 엉키거나 크래쉬되는 문제가 있었다.

 쉽게설명하면 
GCD Private Queue

BLOCK 1
{
   MOC A를 생성
} Thread A에서 실행

BLOCK 2
{
  MOC A를 가지고 작업
} Thead B에서 실행

Private Queue이기때문에 기본 시리얼하게 실행되겠지만 , moc가 생성된 thread가 아닐경우 문제가 생길 가능성이 있다.


- IOS4의 MOC에는 perform block이 존재하지 않는다.

IOS5에서 추가된 기능중하나는 GCD Queue를 이용한 core data 처리관련 기능들이 추가되었다는것이다.
NSManagedObjectContext 를 생성할때 ConcurrencyType를 지정할수가 있다.

NSPrivateQueueConcurrencyType
 이 타입으로 생성한 MOC는 performBlock을통해 실행되어 사용된다면 특별히 신경쓰지 않아도 thread safe가 보장된다.

NSMainQueueConcurrencyType
 performBlock내의 처리로직들은 항상 main thread에서 실행된다.



IOS5에서는 private context를 NSPrivateQueueConcurrencyType으로 생성하여 block내에서 실행한다면 쉽게 백그라운트 처리가 가능해졌다.

하지만 문제는 IOS4 , IOS4를 위해서 직접performBlock을 구현하고 private thread를 생성하여 해당 block의 실행을
동일한 thread로 강제하였다.

ex

        privateQueueContextThread_ = [NSThread newDefaultModeRunLoopThread];

        [privateQueueContextThread_ performBlockAndWait:^{

            privateQueueContext_ = [[NSManagedObjectContext alloc] init];

        }];



직접 prvateQueueContext의 초기화를 privateQueueContextThread에서 실행하고

    @autoreleasepool {

        if (NO == [NSManagedObjectContext instancesRespondToSelector:@selector(performBlock:)]) {

            __unsafe_unretained Class mocClass = [NSManagedObjectContext class];

            IMP performBlockIMP = class_getMethodImplementation(mocClass, @selector(iOS4PerformBlock:));

            IMP performBlockAndWaitIMP = class_getMethodImplementation(mocClass, @selector(iOS4PerformBlockAndWait:));

            

            class_addMethod([NSManagedObjectContext class], @selector(performBlock:), performBlockIMP, "v@:@?");

            class_addMethod([NSManagedObjectContext class], @selector(performBlockAndWait:), performBlockAndWaitIMP, "v@:@?");

        }

    }

performBlock을  구현하였다.

- (void)iOS4PerformBlock:(dispatch_block_t)block
{
  [privateQueueContextThread performBlock:block]


이로서 ios4,ios5에서도 동일한 코드로서 백그라운드 처리가 가능하게되었다.

4. Etc 몇가지 팁

- 비동기로 처리시 UI작업에서 사용하기전에 삭제된 NSManagedObject가 아닌지 체크하여라

- (BOOL)wasDeleted

{

    id myObject = [self.managedObjectContext existingObjectWithID:[self objectID] error:NULL];

    return myObject == nil;

}


위의 함수를 NSManagedObject의 category로 등록하여 , 사용전에 wasDeleted로 체크한다면
릴리즈 데이터의 엑세스로인한 crash이슈는 막을수 있을것이다. 



- Core data에서 sqlite 의 데이터중 name값이 NULL값을 찾고싶을때?

[NSPredicate predicateWithFormat:@"name = NULL"]


5. 마지막으로

 사실 위의 코드로 실제 백그라운드를 바로 구현하기는 어렵겠지만, 문제의 원인과 수정에 대한 방향은
잡을수 있으리라 생각된다.


p.s 이 슬라이드에서 내가 한 방법과 완전히 같은 방식으로 멀티쓰레드 코어데이터를 구현했다. (11월10일)
http://www.slideshare.net/Inferis/adventures-in-multithreaded-core-data