[iOS/OSX] CoreData #3 NSFetchedResultsController

CoreData 에서는 NSFetchedResultsController 라는 컨트롤러가 제공된다. UI 툴킷에서만 붙던 컨트롤러라는 이름이 약간 어색하긴 한데 상당히 편리하게 써 먹을 수 있는 기능이 제공된다.

이 포스트의 예제는 좀 축약된 형태라서 부족할지도 모르겠지만 참고사항이 될 수 있으면 좋겠다.

NSFetchedResultsController

아래의 예제는 NSFetchedResultsController 를 생성하는 예제로 역시 이전 포스팅의 내용이 그대로 이어진다.
NSFetchRequest *fetch = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:@"TestEntity" 
                                          inManagedObjectContext:self.context];
[fetch setEntity:entity];

NSSortDescriptor *sort = [[NSSortDescriptor alloc] initWithKey:@"when" ascending:YES];
[fetch setSortDescriptors:[NSArray arrayWithObject:sort]];

[fetch setFetchBatchSize:25];

NSFetchedResultsController *frc = 
    [[NSFetchedResultsController alloc] initWithFetchRequest:fetch 
                                        managedObjectContext:self.context 
                                          sectionNameKeyPath:nil 
                                                   cacheName:@"CACHENAME"];

NSError *error = nil;
[frc performFetch:&error];
if (error) {
    NSLog(@"Failed to perform fetch");
    return;
}

frc.delegate = self;
우선 NSFetchRequest 에 NSSortDescriptor 가 이용된다. 이는 앞서 이야기 한 정렬 기능을 적용한 것이다.

차이점으로 FetchBatchSize 같은 항목도 보이고 cacheName 도 보이고 이전의 fetch 할 때의 모양과는 좀 다르다. 그리고 delegate 도 보인다.

추신) 위의 코드에선 생략되어 있지만 frc 라고 생성한 오브젝트는 잘(?) 보관해 두는 식으로 구현하면 편하다.

레코드를 읽기 및 갯수 알아내기

NSIndexPath 를 이용해 특정 위치의 레코드를 마음껏 읽을 수 있다. 예를 들어 첫 번째 row 의 레코드를 읽을 때는
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection0];
TestEntity *e = [frc objectAtIndexPath:indexPath];
이런 방식으로 읽을 수 있다.

읽어들인 전체 레코드의 갯수는 아래와 같은 식으로 읽을 수 있다.
id section = [[src sections] objectAtIndex:0];
NSInteger count = [section numberOfObjects];
위 예제는 section 0의 모든 레코드 갯수를 구하는 예제이다.​ 물론 section을 어떻게 쓰느냐에 따라 달라진다.

정체

NSFetchedResultsController 는 컨텍스트(NSManagedObjectContext)의 executeFetchRequest 와 거의 비슷한 기능을 제공한다. DB 에서 내용을 읽고 검색하고 정렬해서 메모리에 로드하는건 동일하다. 다만 batch size 라던가 cache 라던가 등등 메모리 점유율과 퍼포먼스를 위한 추가 기능들이 제공된다.

하지만 이것 뿐 만이 아니다. delegate와 관련된 내용이 어떻게 보면 핵심이다.

NSFetchedResultsControllerDelegate 프로토콜

delegate 는 당연히 NSFetchedResultsControllerDelegate 프로토콜을 구현한 객체가 할당되어야 한다. 이 프로토콜은 아래와 같은 정의를 갖는다.
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller;

- (void)controller:(NSFetchedResultsController *)controller 
   didChangeObject:(id)anObject 
       atIndexPath:(NSIndexPath *)indexPath 
     forChangeType:(NSFetchedResultsChangeType)type 
      newIndexPath:(NSIndexPath *)newIndexPath;

- (void)controller:(NSFetchedResultsController *)controller 
  didChangeSection:(id)sectionInfo 
           atIndex:(NSUInteger)sectionIndex 
     forChangeType:(NSFetchedResultsChangeType)type;

- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller;
사실 영어 의미로 무엇인지 유추가 가능하다면 끝인 이야기다.

이 프로토콜은 컨트롤러가 담당하고 있는 엔티티(테이블)의 내용이 변화 되었음을 알려주기 위한 용도이다.

CoreData 와 관련된 첫 번째두 번째 포스팅에서 삽입, 삭제, 수정에 대한 것을 이야기 했었다. 만약 이런 삽입이나 수정, 삭제 같은 동작이 발생하면 위의 delegate 에 위임받은 셀렉터가 호출된다.

예를 들어 삽입(INSERT)이 일어나면 controller:didChangeObject... 셀렉터가 아래와 같은 정보로 호출된다:
  • type 은 NSFetchedResultsChangeInsert 
  • anObject 에 실제 엔티티 모델 오브젝트가 들어있다. (예에서는 TestEntity 오브젝트)
  • indexPath 는 생성일 경우 nil 이 들어있을 것이다. (수정이나 삭제가 아니고선 이건 쓸 일이 없으니까)
  • newIndexPath 는 새로 삽입된 오브젝트의 Index Path 가 들어있다.
UITableView 를 사용하는 앱을 개발 중이라면 편리하게 쓸 수 있을 것이다. 즉 위 셀렉터가 호출 될 때 UITableView 를 업데이트 하도록 하면 된다. UITableView 에도 위와 비슷하게 beginUpdates, endUpdates 메서드가 있다. 그리고 insertRowsAtIndexPaths 나 deleteRowsAtIndexPaths 그리고 reloadRowsAtIndexPaths 가 있다. delegate의 패턴과 거의 비슷하다.

NSFetchedResultsController 의 가치를 가장 잘 보여주는 예가 바로 이 UITableView 와의 연계이다. 데이터베이스에 레코드가 바로 추가되면 delegate 를 이용해 테이블뷰의 내용을 쉽게 갱신 할 수 있도록 구조화 되어있다. 물론 수정이나 삭제 시에도 마찬가지이다. NSIndexPath 를 그대로 ​사용하므로써 데이터소스와의 연계도 쉽게 가능하다.

결론

NSFetchedResultsController 를 이용하면 데이터를 관리하는 코드와 UI를 담당하는 로직을 쉽게 분리 할 수 있게 도와준다.

만약 네트워크로 내용을 가져와서 표시하는 앱이라면 아래와 같은 식으로 로직을 분리 할 수 있다.
  1. 네트워크로 필요한 데이터를 다운로드 받고
  2. 받은 데이터를 코어데이터를 이용해 데이터베이스에 삽입한다.
  3. 그럼 UI 에서는 자동으로 delegate 를 통해 데이터베이스가 바뀌었다는 것을 통보받고 이 때 UI를 업데이트 하도록 하면 된다. 
이걸로 그 가치는 충분하다.

만약 이런 기능이 없었다면 데이터베이스와 네트워크 등의 로직을 연결해서 순차적으로 동작하게 해야만 한다. 프로그래머로썬 상당히 골치아픈 일이다.

추신) section 에 관해서는 생략했다. 필요하다면 어떻게 쓰는지 찾아보자.

관련​포스트:

댓글

superverse님의 메시지…
유용한 정보 감사합니다

이 블로그의 인기 게시물

소수점 제거 함수 삼총사 ceil(), floor(), round()

버전(Version)을 제대로 이해하기