본문 바로가기
  • SDXL 1.0 + 한복 LoRA
  • SDXL 1.0 + 한복 LoRA
Development/iPhotoDiary(BabyPhotoDiary)

[옛 글] [실전 소스 분석] 4. 이제 Core Data좀 써보자!

by 마즈다 2013. 7. 18.
반응형

최초 작성일 : 2010/09/10 18:24 


0. 정정


지난 시간에 코어 데이터 관련 객체들 설명 중 ManagedObject에 대한 설명을 다음과 같이 하였습니다.


쉽게 말하면 Entity를 클래스 파일로 만들어놓은 것이라고

생각하시면 됩니다. 이후 실제 코딩 작업 설명에 자세히 말씀드리겠지만 xcdatamodel 파일을 통해

구성된 Entity들은 클래스 파일로 만들 수 있습니다.

다만 이 클래스 파일이 항상 필요한 것은 아니고 개발자가 별도의 메서드를 추가시키고자 할 때나

사용을 하게 됩니다.


한데 이 설명이 적절치 못한 것 같아 정정합니다.


우선 .xcdatamodel 파일을 통해 Entity를 실제 클래스 파일로 만들 수 있게 된다는 부분까지는

맞습니다. 그리고 이 클래스파일은 NSManagedObject 클래스를 상속받아 만들어지는데요.

하지만 이 이렇게 생성된 클래스 파일은 같은 이름의 Entity의 구조(스키마, schema)를 표현하긴

하나 실제로 인스턴스화 되어 사용될 때는 같은 이름의 Entity에 저장된 데이터 Row를 의미한다고

보셔야 할 것 같습니다. 이 내용은 아래 본문에 다시 언급하고 있습니다.


Tip Entity 클래스 파일 만드는 방법


1. 우선 .xcdatamodel을 선택하여 데이터 모델 설계 화면을 엽니다.

2. 좌측 상단의 Entity 목록 화면에서 Entity 하나를 선택합니다.




3. Groups & Files창의 필요한 위치에서 오른쪽 마우스 클릭 후 Add -> New File… 을 선택합니다.

4. New File 팝업창에서 iOS->Cocoa Touch Class를 선택하면 우측 화면에 Managed Object Class라는

   새로운 항목이 보입니다. 이것을 선택합니다.




5. 진행하다보면 현재까지 만든 모든 Entity목록을 보여주는 화면이 보이고 이 화면에서 클래스를 만들

   Entity를 선택해서 클래스 파일을 만들면 됩니다.





1. 복습


먼저 지난 시간 분석한 내용을 요점 정리 해보면 다음 4 개의 메서드로 집약될 것입니다.


managedObjectContext

persistentStoreCoordinator

managedObjectModel

applicationDocumentsDirectory


이 메서드들 중에서도 가장 중요하다고 할 수 있는 것은  managedObjectContext입니다.

물론 나머지 3개의 메서드들도 당연히 중요하지만 일단 자주 등장하고 자주 사용하게 된다는

면에서 managedObjectContext가 중요한 것입니다. 간단하게 그 흐름을 되집어보면


1. managedObjectContext의 인스턴스 를 구하기 위해 같은 이름의 메서드를 호출합니다.

2. managedObjectContext를 구하기 위해서는 영구 저장소에 접근해야 하기 때문에 영구

   저장소에 접근하기 위해 사용되는 persistentStoreCoordinator의 인스턴스를 얻게됩니다.

3. persistentStoreCoordinator를 얻기 위해 필요한 정보인 영구 저장소가 존재하는 경로와

   영구 저장소에 저장되어있는 데이터의 논리적인 구현을 표현하는 managedObjectModel

   을 구하게 됩니다.


   이를 위해서 applicationDocumentsDirectory와 managedObjectModel 메서드가 호출됩니다.


이런 과정을 거쳐 얻게 되는 managedObjectContext에는 우리가 사용하게 될 Entity(테이블)와

property(필드 또는 컬럼), 그리고 relationship(Entity간의 관계)에 대한 정보들이 들어있습니다.

그래서 본격적인 데이터 관련 작업은 managedObjectContext에서 시작된다고 보시면 됩니다.


위의 4가지 메서드는 프로젝트 생성시 'Use Core Data for storage'옵션을 체크하면 자동으로

코드가 생성된다는 것도 지난시간에 말씀드렸습니다. 그리고 특별한 이유가 없는한 이렇게

자동 생성된 4개의 메서드는 전혀 수정하실 필요도 없이 그냥 사용하면 됩니다.


2. managedObjectContext로부터 시작하기


지난 시간에 언급한 내용중에 제 실수를 하나 말씀드렸습니다.

그것은 managedObjectContext를 사용하는데 managedObjectContext가 필요한 뷰 컨트롤러에서

ApplicationDelegate로부터 managedObjectContext를 가져오는 것이 아니라 ApplicationDelegate에서

각각의 뷰 컨트롤러에 managedObjectContext를 할당해 주었던 것입니다. 이렇게 될 경우 아무래도

managedObjectContext를 사용하지 않는 시점에도 managedObjectContext 객체의 retain count가

올라간다든지 하는 등의 문제가 발생할 소지가 있을 것입니다.


즉, 특정 뷰 컨트롤러에서 managedObjectContext가 필요하다면 viewDidLoad 정도의 위치에서

ApplicationDelegate에 접근하여 managedObjectContext를 얻어와야 한다는 말입니다.

다음 코드처럼 말이죠.


iPhotoDiaryAppDelegate *appDelegate =

     (iPhotoDiaryAppDelegate *)[[UIApplication sharedApplication] delegate];

self.managedObjectContext = appDelegate.managedObjectContext;


이제부터 이 뷰 컨트롤러에서는 managedObjectContext의 인스턴스를 사용할 수 있게 된 것입니다.


그럼 이 managedObjectContext를 이용해서 어떤 작업을 하게되느냐…


우선 잠시 말을 돌려서 일반적인 RDBMS에서의 작업 과정을 살펴보도록 하죠.

우선 DBMS에 Connection을 해야 합니다. 그리고나서는 적절한 쿼리를 DBMS로 전송을 하게되고

DBMS에서는 쿼리를 파싱한 후 쿼리가 요청하는 내용에 대해 Resultset을 돌려주게 됩니다.

이후에는 개발자가 Resultset을 가지고 적절히 화면을 구성하여 데이터를 화면에 뿌려주게 되는

것이죠.


이 과정을 코어 데이터에 적용을 시켜보면

우선 managedObjectContext를 DBMS에 대한 연결 정보라고 생각하셔도 될 것입니다.

이 managedObjectContext의 인스턴스를 얻었다는 것은 DB에 연결하여 데이터를 가져올 준비가

되었다는 의미입니다.


그러면 이제 쿼리를 전송하고 쿼리에서 요청한 결과셋을 가지고 와야 하는데요.

이러한 작업을 수행하는 객체가 바로 NSFetchedResultsController클래스의 객체입니다.

NSFetchedResultsController클래스의 인스턴스를 통해 다음의 메서드를 수행함으로써

영구 저장소로부터 필요한 데이터들의 결과값을 가져오게 됩니다.


- (BOOL)performFetch:(NSError **)error


실제 사용 예는 다음과 같습니다.


NSError *error;

if (![[self fetchedResultsController] performFetch:&error]) {

// Update to handle the error appropriately.

NSLog(@"Unresolved error %@, %@", error, [error userInfo]);

exit(-1);  // Fail

}


여기서 fetchedResultsController인스턴스는 fetchedResultsController라는 메서드를 통해서

얻게 되는데요. 이 때 fetchedResultsController 서드 내에서 managedObjectContext가 인자로

쓰이게 됩니다.


fetchedResultsController의 내용은 다음과 같습니다.


- (NSFetchedResultsController *)fetchedResultsController {

    if (fetchedResultsController != nil) {

        return fetchedResultsController;

    }

    

// Create and configure a fetch request with the Book entity.

NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];

NSEntityDescription *entity =

            [NSEntityDescription entityForName:@"DiaryData" 

                                 inManagedObjectContext:managedObjectContext];

[fetchRequest setEntity:entity];

// Create the sort descriptors array.

NSSortDescriptor *birthdayDescriptor =

            [[NSSortDescriptor alloc] initWithKey:@"writedate" ascending:YES];

NSArray *sortDescriptors =

            [[NSArray alloc] initWithObjects:birthdayDescriptor, nil];

[fetchRequest setSortDescriptors:sortDescriptors];

// Create and initialize the fetch results controller.

NSFetchedResultsController *aFetchedResultsController =

         [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest  

          managedObjectContext:managedObjectContext sectionNameKeyPath:nil  

          cacheName:@"Root"];

self.fetchedResultsController = aFetchedResultsController;

fetchedResultsController.delegate = self;

// Memory management.

[aFetchedResultsController release];

[fetchRequest release];

[birthdayDescriptor release];

[sortDescriptors release];

return fetchedResultsController;

}


한 줄 한 줄 분석을 해보면 먼저 fetchedResultsController도 지연로딩을 사용하고 있습니다.

fetchedResultsController 인스턴스가 존재하면 기존 인스턴스를 반환하고 그렇지 않으면

새로 생성을 하여 반환합니다.


if (fetchedResultsController != nil) {

return fetchedResultsController;

}


다음에는 쿼리문을 준비하는 과정입니다.


NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];

NSEntityDescription *entity =

            [NSEntityDescription entityForName:@"DiaryData" 

                                 inManagedObjectContext:managedObjectContext];

[fetchRequest setEntity:entity];


fetchRequest 인스턴스를 하나 만들고 managedObjectContext내에 있는 EntityDescription으로부터

'ChildData'라는 이름의 Entity(테이블)를 가져와서 쿼리에 사용하겠다고 설정하는 것입니다.

즉, 'ChildData'라는 테이블을 쿼리의 대상으로 하겠다고 선언한 것입니다.


※ 주의할 것은 이 때의 ChildData는 테이블을 의미하고 있지만 나중에 나오게될 ChildData의 인스턴스는

ChildData라는 테이블에 저장되어있는 Row에 해당한다고 보셔야 한다는 것입니다. 이 내용은 이후 다시 한 번

반복하도록 하겠습니다.


다음의 내용은 SQL문의 order by절에 해당한다고 보시면 됩니다.

정렬 기준 property(컬럼)와 정렬 방법(오름차순 혹은 내림차순)을 지정하게 됩니다.


NSSortDescriptor *birthdayDescriptor =

            [[NSSortDescriptor alloc] initWithKey:@"writedate" ascending:YES];

NSArray *sortDescriptors =

            [[NSArray alloc] initWithObjects:birthdayDescriptor, nil];

[fetchRequest setSortDescriptors:sortDescriptors];


두 번째 줄을 보시고 짐작들 하셨겠지만 NSSortDescriptor는 여러개를 지정할 수 있습니다.

NSSortDescriptor의 배열이 최종적으로 fetchRequest에 지정되는 것이죠.


여기까지 해서 쿼리(fetchRequest)에는 2가지 정보가 설정되었습니다.

데이터를 불러올 대상 테이블과 데이터를 불러올 때의 정렬에 대한 정보입니다.


마지막으로 이렇게 설정된 쿼리문을 가지고 managedObjectCContext로부터 Resultset을 가져오게 되는

문장이 바로 다음 내용입니다.


NSFetchedResultsController *aFetchedResultsController =

         [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest  

          managedObjectContext:managedObjectContext sectionNameKeyPath:nil  

          cacheName:@"Root"];

self.fetchedResultsController = aFetchedResultsController;

fetchedResultsController.delegate = self;


세번째 인자는 테이블 뷰를 사용하게 되는 경우 섹션 이름으로 사용하게 될 property(컬럼)를 지정하는 것이고

마지막 인자는 캐시로 사용할 파일 이름입니다.


여기까지 하면 일단 기본 기능은 처리가 됩니다. 해당 Entity의 모든 데이터들을 NSSortDescriptor들의

기준에 따라 불러오게 되는 것이죠.


하지만 이 것만으로는 당연히 부족합니다. 단지 1건의 데이터를 사용하기 위해서 100건 1000건의 데이터를

불러와야 한다면 엄청난 낭비겠죠. 물론 100건 1000건 불러온다고 해서 엄청난 부하가 걸리거나 하진

않겠지만 말이죠…^^;;; 물론 이를 해결할 옵션 메서드들이 있지만 말입니다. 자세한 내용은 API 문서를

참고하세요.


어찌되었건 여기까지 정상적으로 데이터를 불러와서 이제 사용하기만 하면 되는 시점에 왔네요.


3. TableViewController에서 사용하기


UITableViewController는 UITableViewDataSource 프로토콜을 구현하고 있는데요.

여기에는 테이블 뷰 이용시 필수라고 할 수 있는 몇가지 메서드가 구현되어야 합니다. 다음과 같은 메서드들이죠.


- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath

- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section


프로토콜 이름에서도 알 수 있듯이 이 메서드들은 모드 데이터 소스와 관련된 내용이고 따라서 필요한

중요 정보들을 코어 데이터 처리를 통해 가져온 fetchedResultsController 인스턴스를 통해 가져오게 됩니다.

주요 사용 정보는 section 정보입니다. 물론 fetchedResultsController 생성시  sectionNameKeyPath:nil

코딩한 경우에는 섹션이 나누어지지 않기 때문에 섹션 수는 1로 나오겠죠.


섹션 수를 가져오는 코딩입니다.


- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {

NSInteger count = [[fetchedResultsController sections] count];

.

.

.

}


리고 각 섹션에 포함된 데이터 수를 가져오는 코딩입니다.


- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {

NSInteger numberOfRows = 0;

if ([[fetchedResultsController sections] count] > 0) {

id <NSFetchedResultsSectionInfo> sectionInfo = [[fetchedResultsController sections] objectAtIndex:section];

     numberOfRows = [sectionInfo numberOfObjects];

}

.

.

.

}


다음으로 사용하는 정보는 특정 인덱스에 있는 실 데이터의 클래스(ManagedObject 타입의 클래스)를

가져오게 됩니다.


데이터 클래스를 가져오는 코딩입니다.

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {

diaryData =

(DiaryData *)[fetchedResultsControllerobjectAtIndexPath:indexPath];

.

.

.

}


마지막으로 섹션 이름을 가져오는 코딩입니다. 섹션이름은 fetchedResultsController 생성시 sectionNameKeyPath:에 넣어준 값입니다.

물론 sectionNameKeyPath:nil로 한 경우 이 값은 없죠.


- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {

return [[[fetchedResultsController sections] objectAtIndex:section] name];

}



※ 참고로 이 때 사용되는 인덱스 정보는 index가 아니라 indexPath입니다. 이름에서도 알 수 있듯이 특정 위치의

인덱스 값 하나만을 의미하는 것이 아니라 여러층으로 중복된 배열 구조의 인덱스 경로를 모두 표시하는

객체라고 볼 수 있겠죠. 이 것을 통해 fetchedResultsController의 구조나 fetchedResultsController로부터

실 데이터를 불러오는 과정을 유추해볼 수 있습니다.


즉, fetchedResultsController는 중첩된 배열 구조로 section이라는 배열이  있고 이 배열은 그 요소로

데이터  Row의 배열을 가지고 있는 2차원 배열 구조로 보시면 된다는 것입니다. 아래 그림과 같겠죠.






만일 'Row12'라는 데이터를 찾아야 한다면 우선 section 인덱스인 2를 찾아야 하고 다음 데이터의 인덱스인

1을 찾아가야 해서 2 -> 1의 경로를 가져야 합니다. 바로 이러한 인덱스의 경로를 indexPath가 표현하고

있는 것이죠.


두말할 것 없이 자세한 내용은 API 문서를 참고하세요…^^;;;




※ 앞서도 언급했지만 Entity와 데이터 Row를 잘 구분하셔야 합니다.

일단 .xcdatamodel을 통해 생성된 Entity들은 기본적으로 NSManagedObject타입의 클래스입니다.

위의 예제 코드에서는 'DiaryData'라는 Entity에서 데이터를 가져온 것입니다. 하지만 실제로


(DiaryData *)[fetchedResultsController objectAtIndexPath:indexPath];


이 코드를 통해 가져온 DiaryData 클래스의 인스턴스는 Entity를 표현한 것이 아니라 Entity

저장된 Data Row, 즉, 한 건 한 건의 데이터를 의미한다고 보시는 것이 옳을 것입니다.



여기까지 진행되었다면 일단 화면상의 테이블 뷰에 데이터 리스를 뿌리는 것 까지는 문제없이 실행될 것입니다.


4. 요약


오늘도 쓸데없이 말만 길어졌고 내용은 여전히 혼란스럽네요.

그렇기 때문에 저 역시 여러분의 도움이 많이 필요합니다…^^;;; 제가 틀렸거나 잘못 이해하고 있는 부분은

언제든지 말씀을 해주세요.


일단 지난 시간에 managedObjectContext까지 만들었고 오늘은 이 managedObjectContext를 이용하여 실제 데이터를

가지고 있다고 볼 수 있는 fetchedResultsController를 생성한 후 테이블 뷰 컨트롤러를 이용하는 화면에서 이

fetchedResultsController를 이용하여 UITableViewController(UITableViewDataSource 프로토콜)의 몇몇 메서드를

이용하여 section정보와 실 데이터 row의 정보를 가져오는 방법을 알아보았습니다.


하지만 아직 predicate는 사용하지 않았네요…^^;;;


5. 마무리


여기까지 일단 코어 데이터의 여러 객체들을 이용하여 테이블 뷰에 데이터를 출력하는 내용까지는

정리를 해보았습니다. 하지만 꼭 데이터를 테이블 뷰에만 출력하는 것은 아니죠.

그래서 다음시간에는 FetchedRequest 객체와 Predicate 객체를 이용하여 데이터 한 건만 불러와 화면에

보여주는 내용으로 Core Data에 관한 내용을 마무리하도록 하겠습니다.

반응형