출처 : 맥부기
Head First iPhone Development
1. 아이폰에서의 데이터 관리
아이폰에서는 여러가지 방식의 데이터 저장 공간을 사용할 수 있다.
3가지 정도로 구분할 수 있는데
SQLite 를 직접 사용하는 방법
바이너리 파일을 사용하는 방법
(이것은 다시 property list 를 사용하는 방법과 객체 archiving을 이용하는 방법 2가지가 있다)
마지막으로 코어 데이터를 이용하는 방법이 있다.
* SQLite
우선 가장 접근하기 쉬운 것이 SQLite를 직접 사용하는 방법이다.
기존에 웹이나 기타 RDBMS 관련 개발을 해본 분들은 쿼리를 직접 작성하여
entity 간의 관계를 지정하고 검색 조건을 위한 where 절을 만들고 하면서
익숙하게 데이터를 처리 할 수 있다.
* Binary
기존 언어에서 객체 직렬화라는 부분을 생각하면 될 것이다.
객체 직렬화(serialization)란 데이터를 저장하고 있는 클래스의 인스턴스를
그대로 바이너리 파일로 저장하는 기법이다.
JAVA의 예를 들면 HashMap을 상속받은 클래스에 나라 이름을 key로 하고
그 나라 수도를 value 로 하여 저장을 한뒤 이 클래스의 인스턴스를 파일로 저장시키는 방법이다.
쉽게 말해, 텍스트 에디터로 열었을 때 이상한 문자로 보여지는 파일이
특정 애플리케이션에서는 정상적인 문자로 보여지는 파일들은 모두 이러한 객체 직렬화를 통해
저장되었다고 생각하면 쉽다.
간단한 예로 한글 *.hwp 파일은 텍스트 에디터에서는 이상한 문자로 보이지만 아래 한글
프로그램에서는 정상적인 문서로 보이는 것과 같은 것이다.
* Core Data
마지막으로 애플에서 구현해 놓은 코어 데이터를 이용하는 방법이다.
앱의 경우 애플에서 구현해 놓은 이 방식을 익히고 사용하는 것이 더 편할 듯 한데
익숙하지 않은 구조라 처음 사용이 어렵다.
일종의 ORMapping 역할을 하는 API 이다.
JAVA로 보자면 iBatis 나 Hibernate 에 해당한다고 보면 된다.
원칙적으로 따지자면 개발자에게는 편한 구조이다.
일일이 쿼리를 작성하지 않아도 데이터를 관리할 수 있기 때문이다.
쉽게 생각해,
필요한 매서드에 필요한 인자만을 넘겨서 호출하면 Core Data 가 알아서
쿼리도 날려주고 결과 값도 돌려주고 한다는 것이다.
하지만 이 캡슐화(객체 지향의 주요 개념 중 하나로 핵심 로직을 인터페이스 뒤로 숨기고
개발자는 외부로 드러난 인터페이스만을 통해 API 를 다룰 수 있게 하는 것)는
처음에는 많이 헷갈린다.
어쨌든 애플에서 지원하는 만큼 앱개발에는 최적화가 되어 있겠지만
세밀한 컨트롤을 원하는 개발자들에게는 약간 답답한 면이 존재한다.
코어데이터는 애플이 맥 OS 에서 아이폰으로 가져온 것이니만큼,
편하고도 강력한 기능을 가지고 있다.
- 사용자 정의 객체를 읽고 저장할 수 있는 기능
코어 데이터는 엔티티 기술문에 기술된 사용자 객체를 읽고 저장할 수 있다.
또한 객체관의 관계, 다른 버전의 데이터로 이전, 필수 및 선택 필드,
그리고 필드값 검증 (field validation) 도 처리할 수 있다.
- 데이터를 다양한 방법으로 저장 가능
코어 데이터는 데이터가 실제로 어떻게 저장되는 지 신경 쓰지 않게 해준다.
코어 데이터에 어떻게 저장하는지만 지정하면, 앱에서는 SQLite 데이터 베이스에
저장하는지 임의의 이진 파일에 저장하는 지 신경쓰지 않아도 된다.
- 메모리 관리 및 실행취소(undo)와 실행복귀(redo) 지원
코어 데이터는 메모리 상의 객체를 관리하는 데 매우 효율적이다.
그리고 객체에 대한 변경도 추적하고 있다.
그래서 실행취소와 실행복귀를 사용할 수 있고, 커다란 데이터베이스를
필요한 만큼의 페이지 단위로 읽을 수도 있다.
그러나 코어데이터는 시작하기 전에
코어 데이터에게 객체에 대해서 설명해 주어야 한다.
2. Core Data 의 구조
본론으로 들어가 보자
이 그림은 '위키북스'에서 나온 'More 아이폰 3 프로그래밍' 이라는 책에 있는 그림이다.
각 부분을 설명하면
1) Persistent store : 영구 저장소라고 해석되고 가장 최종 데이터가 저장되는 영역이다.
물리적으로는 하드디스크(메모리)에 저장된 데이터를 가지고 있는 바이너리 파일이라고 생각하자.
2) Data Object Model : Persistent store 에 저장된 내용들을 논리적으로 보여주는 객체
Xcode 상에 보여지는 xcdatamodel 파일을 생각하면 된다.
3) Persistent store Coordinator :
영구 저장소와 Managed Object Context 를 이어주는 가교역할을 한다고 생각하자.
항상 영구 저장소에서 무너가 실제적인 것을 가지고 오려고 하면 직접 영구 저장소에
접근 하는 것이 아니라 이 Persistent store Coodinator 를 거쳐야 하는 것이다.
(이런 것도 사실 객체 지향의 일환으로 핵심적인 내용들은 보다 안정적인 위치에 감춰두고
개발자들에게는 제한된 인터페이스만을 제공하여 불필요한 변경으로 인한 문제를 줄이는 동시에
내부 API가 변경이 되더라도 개발자들은 딱히 수정을 하지 않아도 되는 데 그 목적이 있다)
4) Managed Object Context :
객체 관리 컨텍스트 인데 개발자의 작업은 여기서부터 시작된다고 생각해도 된다.
위의 3가지는 최초 애플리케이션이 실행되는 시점에 한 번 코딩을 하면 되고,
이 Managed Object Context 부터는 수시로 사용하게 된다.
5) Entity Description :
Entity 는 쉽게 말해 RDBMS 의 테이블을 생각하면 된다.
특정 Entity 와 관련된 각종 정보를 가져올 수 있다.
Managed Object Model 이나 property (RDBMS 의 필드 혹은 컬럼이라 생각하면 됨)
또는 타 Entity 와의 관계(relationship) 등의 정보를 가져오거나 설정할 수 있다.
6) Managed Object : Managed Object Context 에서 관리되는 객체 중의 하나
쉽게 말하면 Entity 를 클래스 파일로 만들어 놓을 것이라고 생각하면 된다.
이후 실제 코딩 작업 설명에 자세히 말씀드리겠지만 xcdatamodel 파일을 통해
구성된 Entity 들은 클래스 파일로 만들 수 있다.
이렇게 생성된 클래스 파일은 같은 이름의 Entity 의 구조(스키마,schema)를
표현하긴 하나 실제로 인스턴스화 되어 사용될 때는 같은 이름의 Entity에
저장된 데이터 Row 를 의미한다고 보면된다.
Tip - Entity 클래스 파일 만드는 방법
1. 우선 .xcdatamodel 을 선택하여 데이터 모델 설계화면을 연다.
2. 좌측 상단의 Entity 목록 화면에서 Entity 하나를 선택한다.
7) Fetch Request : 속칭 '쿼리를 날리다' 이다. 실제 이 과정에서 결과값을 받아오게 된다.
8) Predicate : '술어'라고 해석되 있는데 where 조건에 해당하는 객체이다.
3. 실제 소스 보기
우선 디버깅을 위해 찍은 로그를 보자
(내가 한거 아님 맥부기 마즈다님이 만드신 앱)
최초 실행되는 iPhotoDiaryAppDelegate 에서 단순히 실행되는 순서대로 매서드 명을 찍은 것이다.
2010-09-02 22:43:14.742 iPhotoDiary[2610:307] DEBUG : applicationDidFinishLaunching
2010-09-02 22:43:14.753 iPhotoDiary[2610:307] DEBUG : managedObjectContext
2010-09-02 22:43:14.759 iPhotoDiary[2610:307] DEBUG : persistentStoreCoordinator
2010-09-02 22:43:14.764 iPhotoDiary[2610:307] DEBUG : applicationDocumentsDirectory
2010-09-02 22:43:14.778 iPhotoDiary[2610:307] DEBUG : managedObjectModel
2010-09-02 22:43:14.925 iPhotoDiary[2610:307] DEBUG : managedObjectContext
2010-09-02 22:43:14.931 iPhotoDiary[2610:307] DEBUG : managedObjectContext
2010-09-02 22:43:14.936 iPhotoDiary[2610:307] DEBUG : managedObjectContext
2010-09-02 22:43:14.741 iPhotoDiary[2610:307] DEBUG : applicationDocumentsDirectory
다음은 소스입니다.
- (void)applicationDidFinishLaunching:(UIApplication *)application{
// 이 부분에서 앞으로 데이터를 사용할 필요가 있는 컨트롤러들에게
// ManagedObjectContext 들을 할당해 주고 있습니다.
// 처음에는 Core Data 의 구조를 몰라서 이러한 순서를 통해 ManagedObjectContext 를
// 가져온 것이 아니라 각 화면 컨트롤러에서 별도로 ManagedObjectContext 의
// 인스턴스를 만들어서 사용했더니 계속 Entity를 찾을 수 없다는 오류가 뜨더군요.
appMainViewController.managedObjectContext = [self.managedObjectContext];
calendarView.managedObjectContext = [self managedObjectContext];
diaryListController.managedObjectContext = [self managedObjectContext];
eventListController.managedObjectContext = [self managedObjectContext];
// 요거는 다음번에 설명을 하겠지만 binary 형태의 저장 중
// property list 를 이용한 저장관련 내용이다.
[self writeToPlist];
[window addSubview:tabBarController.view];
[window makeKeyAndVisible];
}
호출 순서대로 설명을 해보자면
최초에
appMainViewController.managedObjectContext = [self.managedObjectContext];
호출을 하게 되면 영구 저장소에 접근하기 위해서 persistentStoreCoordinator 를 호출하게 되고
persistentStoreCoordinator 는 영구 저장소가 위치한 경로를 알아내기 위해
applicationDocumentsDirectory 를 호출 하게 되는 것이다.
이후로 3번 더 managedObjectContext 를 호출 하였는데 더 이상 PersistentSotreCoordinator 와
applicationDocumentsDirectory 가 호출되지 않은 이유는 managedObjectContext와
persistentStoreCoordinator 가 지연 로딩, 즉 이미 해당객체가 있으면 기존의 객체를 리턴하고
없으면 새로 생성해서 넘겨주는 것이다. 그렇기 때문에 최초 managedObjectContext 호출 후
객체가 생성되었으므로 이후 3번의 호출에는 managedObjectContext 객체를 새로 생성하는
것이 아니라 기존 객체만을 돌려주어 이후 매서드들이 호출되지 않는 것이다.
나머지 매서드들은 최초 프로젝트 생성시 코어데이터를 사용하겠다는 옵션에 체크를 했다면 자동으로 생성되는 매서드들이라 코드는 생략한다.
프로젝트 생성시 네비게이션 베이스, (아이패드)스플릿 뷰 베이스, 유틸리티 어플리케이션, 윈도우 베이스 등의 프로젝트에는 User Core Data for Storage 라는 옵션이 나온다.
물론 프로젝트 생성시 이 옵션을 체크하지 않았어도 데이터 모델을 만들 수 있다.
Groups & Files 에서 적절한 위치에 오른쪽 버튼 클릭 후
Add -> New File ... 을 선택하면 iPhone OS 의 Resource 항목에 Data Model이 있다.
물론 이렇게 만들었을 경우에는 아래 4개의 매서드들을 모두 코딩으로 해주어야 한다.
managedObjectContext
persistentStoreCoordinator
managedObjectModel
applicationDocumentsDirectory
4. 정리
중요한 것은 Core Data 의 구조와 흐름이 어떻게 되느냐 이다.
Persistent store 에 applicationDocumentDirectory 로 영구 저장소의 경로를 확인한 Persistent store Coordinator 를 통해 접근하여 ManagedObjectContext 를 가져오고 개발자들은 ManagedObjectContext 를 이용하여 구체적인 작업을 하면 된다.
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;
}
if (fetchedResultsController != nil) {
return fetchedResultsController;
} //이부분
다음에는 쿼리문을 준비하는 과정이다.
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity =
[NSEntityDescription entityForName:@"DiaryData"
inManagedObjectContext:managedObjectContext];
[fetchRequest setEntity:entity];
fetchRequest 인스턴스를 하나 만들고 managedObjectContext 내에 있는
EntityDescription 으로부터 'DiaryData' 라는 이름의 Entity(테이블)를 가져와서
쿼리에 사용하겠다고 설정한 것이다.
즉 'Diary Data'라는 테이블을 쿼리의 대상으로 하겠다고 선언한 것이다.
* 주의할 것은 이때의 DiaryData 는 테이블을 의미하고 있지만 나중에 나오게 될
DiaryData 의 인스턴스는 DiaryData 라는 테이블에 저장되어 있는 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가지 정보가 설정되어 있다.
데이터를 불러올 대상 테이블과 데이터를 불러올 때의 정렬에 대한 정보.
마지막으로 이렇게 설정된 쿼리문을 가지고 managedObjectContext로 부터
Resultset 을 가져오게 되는 문장이 바로 다음 내용이다.
NSFetchedResultsController *aFetchedResultsController =
[[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
managedObjectContext:managedObjectContext sectionNameKeyPath:nil
cacheName:@"Root"];
self.fetchedResultsController = aFetchedResultsController;
fetchedResultsController.delegate = self;
세번째 인자는 테이블 뷰를 사용하게 되는 경우 섹션 이름으로 사용하게 될 property(컬럼)를
지정하는 것이고 마지막 인자는 캐시로 사용할 파일 이름이다.
여기까지 하면 일단 기본 기능은 처리가 된다. 해당 Entity 의 모든 데이터들을
NSSortDescriptor 들의 기준에 따라 불러오게 되는 것이다.
하지만 이 것만으로는 당연히 부족한다. 단지 한 건의 데이터를 사용하기 위해서
100건 1000건의 데이터를 불러와야 한다면 엄청난 낭비이다.
물론 100건 1000건 불러온다고 해서 엄청난 부하가 걸리거나 하진 않지만 말이다.
물론 이를 해결할 옵션 메서드들이 있다. 자세한것은 API 문서 활요.. (썅..)
6. 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
주요 사용 정보는 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에 관한 내용을 마무리하도록 하겠습니다.
애초의 계획은 일정 내용이 진행되면 해당 내용에 관련된 소스 코드 전체를 공개하고 이후 다음 진행하고 또 관련 소스 공개하고...이런 식으로 하려 했는데 소스를 부분부분 자르려니 쉽지도 않고 또 보시는 입장에서도 단편적인 소스는 이해하기가 어려울 것 같아 소스 공개는 이 [실전 소스 분석]이 모두 끝난 후 전체 소스를 한꺼번에 공개하는 것으로 방향을 잡았습니다. 이 점 참고하시기 바랍니다. 1. Predicate 사용하기 지난 시간까지 코어 데이터의 개념, 필요한 메서드들, 테이블 뷰 컨트롤러를 통한 코어 데이터의 사용 등에 대해 알아보았습니다. 사실 기본 개념이나 원리를 몰라도 API 내용만을 가지고도 충분히 사용할 수 있을만큼 코어 데이터의 사용법은 간단합니다. 더불어 테이블 뷰와의 관계도 잘 만들어져 있어 NSFetchedResultsControllerDelegate에 선언된 메서드를 구현하면 별다른 코딩 없이 코어 데이터의 변경사항(입력, 수정, 삭제)이 즉시 테이블 뷰에 반영이 됩니다. 개발자에게는 정말로 편한 방법이라고 할 수 있을 것입니다. 하지만 항상 테이블의 모든 데이터를 화면에 출력하는 것은 아닙니다. 특정 날짜에 등록된 데이터, 혹은 특정 성씨를 가진 사람들의 집합. 또는 득정 지역별 데이터 등 어떤 조건에 따라 전체 데이터 중 일부만을 필요로 하는 경우가 대다수일 것입니다. 이 때 사용하는 것이 Predicate이고 이 것은 RDBMS에서 SQL문을 작성할 경우 where 조건절에 해당하는 내용을 담게 됩니다. Predicate 역시 클래스를 사용하는 것 자체는 어렵지 않습니다. 다만 인자로 넘기는 Prediccate의 포맷을 잘 알아두어야 합니다. 가끔 언급을 했듯이 제가 진행하는 이 내용은 '강좌'라기보다는 제 소스를 같이 분석해보는 '소스 분석'에 가깝기 때문에 이러한 세세한 부분까지는 다루지 않겠습니다. 단지 Predicate 클래스를 어떤 식으로 사용하는지만 간단히 언급하도록 하겠습니다. 이전 시간까지 언급한 클래스 중에 NSFetchRequest는 'SQL문'에 해당한다고 앞서 말씀드렸습니다. 그러므로 where 조건절에 해당하는 predicate는 당연히 fetchRequest에 포함되어야 합니다. 다음과 같은 코드를 작성하시면 끝입니다. 다음은 이전 내용에서 보여드린 fetchedResultsController 메서드의 일부분입니다. // Attribute의 이름을 설정 NSString *attributeName = @"sectionDate"; // predicate 생성 NSPredicate *predicate = [NSPredicate predicateWithFormat:@"%K LIKE[cd] %@", // where 조건문 attributeName, [conditionString stringByAppendingString:@"*"]]; NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init]; // fetchRequest에 predicate을 설정함 [fetchRequest setPredicate:predicate]; . . . 위 내용 중 눈여겨보실 것은 predicate를 생성하는 부분인데요. 그 중에서도 인자로 넘어가는 문자열을 구성하는 부분을 잘 보셔야 합니다. 먼저 위 predicate의 인자로 넘어가는 문자열을 SQL문으로 바꿔보면 다음과 같습니다. 만일 conditionString의 값이 '2010년 8월'이라면 이렇게 되는 것입니다. 물론 이 밖에도 많은 표현이 가능하지만 앞서 말씀드린 대로 자세한 내용은 API 문서를 참고해주세요…^^;;; 해당 내용은 'Predicate Programming Guide'의 Predicate Format String Syntax절에 있습니다. 2. 테이블 뷰를 사용하지 않는 화면에서의 데이터 사용 (NSManagedObjectContext클래스의 executeFetchRequest메서드) 이전 시간에는 테이블 뷰를 통해 데이터를 화면에 보여주는 내용에 대해 살펴보았었습니다. 하지만 항상 테이블 뷰를 통해 테이터를 보여주기만 하는 것은 아니죠. 때론 UIView를 통해 데이터를 표현해 줄 수도 있습니다. 테이블 뷰를 통해 데이터를 표현하는 경우에는 section정보와 row정보등이 필요하며 이러한 경로를 통해 원하는 데이터를 불러오기 위해 indexPath라는 객체를 사용한다는 것을 말씀드린 바가 있습니다. 또한 데이터의 변경사항을 테이블 뷰에 반영하기 위해 NSFetchedResultsControllerDelegate에 선언된 4개의 메서드들이 필요하게 됩니다. 하지만 테이블 뷰가 아닌 곳에 데이터를 표현하는 경우는 위와 같은 정보들이 필요 없는 상황이 대부분이며 따라서 몇가지 구현은 생략을 하여도 됩니다. 즉, 단순히 필요한 여러건의 데이터만을 가져와 사용하는 경우에는 단지 NSFetchRequest 클래스만을 사용하여 처리 할 수 있게 되는 것입니다. 제가 만든 애플리케이션인 iPhotoDiary의 첫 화면은 등록된 아이들의 데이터를 가져와 그 중 한 아이의 신상 정보를 UIView 기반의 화면에 보여주는 것입니다. 제가 필요한 것은 단지 등록된 아이들의 데이터와 각각의 아이들을 선택할 경우 키워드가 될 아이들의 이름 뿐입니다. 섹션 정보도 필요 없고 또 이 화면에서는 데이터의 변경이 이루어지지도 않습니다. 따라서 직접 만든 다음의 메서드 하나에서 데이터의 사용이 완결됩니다. 인자로 넘어가는 것은 등록된 아이의 이름입니다. 어차피 많아야 3-4명의 아이들을 등록하여 사용하는 경우가 대부분일 것이라 판단했고 그렇다면 각각의 아이에 대해 코어 데이터에 접근하는 것은 낭비라고 생각하여 일단 이 함수에서 모든 데이터를 가져 온 후 데이터 건수만큼 아이들을 선택할 수 있는 버튼을 만들고 버튼을 클릭할 경우 이 함수에서 불러온 데이터 배열(ManagedObject의 배열)에서 이름을 비교하여 해당 데이터를 가져오도록 처리하였습니다. 따라서 이 함수에는 아주간단한 내용만으로 데이터를 가져오고 있습니다. (그리고 사실상 인자로 넘어오는 cname이 필요가 없죠...^^;;;) 코드를 보시면 NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init]; [fetchRequest setEntity:[NSEntityDescription entityForName:@"ChildData" inManagedObjectContext:managedObjectContext]]; NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"birthday" ascending:YES]; NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:&sortDescriptor count:1]; [fetchRequest setSortDescriptors:sortDescriptors]; NSError *error; self.childDatas = [managedObjectContext executeFetchRequest:fetchRequest error:&error]; [fetchRequest setSortDescriptors:sortDescriptors]; 이전까지의 내용은 잘 아실 것입니다. fetchRequest를 생성하고 대상 테이블을 ChildData로 지정하고 정렬 기준을 birthday로 하여 생일이 빠른 아이가 먼저 나오도록 하였습니다. 그리고 마지막으로 managedObjectContext의 executeFetchRequest 메서드를 통해 쿼리를 실행하고 이 메서드의 return값으로 데이터의 배열을 얻어오게 된 것입니다. 참고로 결과 값을 받은 self.childDatas는 NSArray객체입니다. 메서드의 이후 내용은 데이터의 값을 가지고 여러가지 가공을 하여 화면의 각 요소에 출력하는 내용입니다. 이와같이 데이터의 변경이 없는 단순 조회성 데이터의 사용은 managedObjectContext 객체만으로 충분합니다. 3. 정리 코어 데이터 사용의 끝자락이다보니 오늘은 좀 일찌감치 마무리를 합니다…^^;;; 초반에도 말씀을 드렸다시피 이 내용들은 전문적인 내용에 대한 설명이 아니라 초보 개발자의 시행착오를 그대로 보여드리는 것이 목적이기에 결과는 동일하지만 과정이 나쁜 그런 코드들이 많이 보이실 것으로 생각됩니다. 하지만 어느 정도 프로그래밍의 경험이 있으신 분 들은 과정이 얼마나 중요한지 잘 아실 것입니다. 그래서 이미 완성된 코드를 다시 Refactoring하기도 하는 것이죠. 안타깝게도 저는 Objective-C를 처음 접하는지라 어찌어찌 결과물은 만들었지만 잘못된 부분들을 수정하기는 쉽지가 않네요. 그나마 이 실전 소스 분석을 진행하면서 몇군데 어설픈 코드를 바로잡을 수는 있었습니다. 이런 부분에 대해서는 이 글을 보시는 많은 분들이 도움이 필요합니다…^^;;; 간단하게 코어 데이터를 구성하는 객체들을 다시 한 번 짚어보며 마무리를 하도록 하겠습니다. ApplicationDelegate에서 한 번만 설정 1. Persistent store : 영구저장소 RDBMS의 데이터파일(.dbf파일 같은…) 2. Data Object Model : 데이터베이스의 전체적인 모양, Schema, ERD 3. Persistent store Coordinator : 영구 저장소에 접근하기 위한 객체 4. Managed Object Context : 객체 관리 컨텍스트. 코어 데이터를 사용하기 위한 핵심 객체 필요한 위치에서 수시로 사용되는 객체들 5. Entity Description : Entity 즉, 테이블과 관련된 정보를 갖고 있는 객체 6. Managed Object : 기본적으로 Entity의 구조(Schema)를 표현하나 쿼리의 결과로서 받아지는 데이터의 Row를 표현하기도 함 7. Fetch Request : 데이터를 불러오기 위한 쿼리를 표현하는 객체 8. Predicate : 쿼리문 중 where 조건절을 표현하는 객체 9. Fetched Results Controller : 코어 데이터로부터 가져온 데이터들을 계층적으로 관리하는 객체 테이블 뷰를 통한 데이터 표현에 사용된다. 4. Tip - Data Migration 코어 데이터로 작업을 하면서 가장 난감했던 상황이 .xcdatamodel 파일을 통해 Entity들과 각 Entity의 relationship 및 property들을 만들어놓고 나중에 이 것을 수정하여 빌드하고 실행시키면 데이터 모델이 서로 다르다고 에러를 토해내는 상황이었습니다. 즉, 수정된 데이터 모델을 적용시키는 방법을 몰랐던 것입니다. 그래서 많은 분들이 알고 계시겠지만 데이터 모델의 변경을 반영하는 방법을 팁으로 추가합니다. 우선 xcode 화면에서의 설정입니다. 1. xcode의 Groups & Files 창에서 .xcdatamodel 파일을 선택합니다. 2. xcode의 메뉴바에서 Design -> Data Model -> Add Model Version을 선택합니다. 이렇게 하고 Groups & Files 창에서 보시면 기존에 .xcdatamodel 파일이 하나 있던 것이 .xcdatamodeld라는 디렉토리로 바뀌어있고 그 디렉토리 아래 .xcdatamodel 파일이 존재할 것입니다. 현재 사용중인 데이터 모델은 파일 아이콘에 초록 바탕에 체크 표시가 된 그림이 추가됩니다. 그리고 이전 버전의 데이터 모델들은 파일명 끝에 일련 번호가 붙습니다. 이 때 번호가 클 수록 가장 오래된 버전입니다. 이런 과정을 거친 실제 데이터 모델 화면입니다. 다음은 ApplicationDelegate의 persistentStoreCoordinator 메서드에서 다음 코드를 추가합니다. NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys: [NSNumber numberWithBool:YES], SMigratePersistentStoresAutomaticallyOption, [NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption, nil]; 이 Dictionary객체는 persistentStoreCoordinator를 생성할 때 인자로 들어가게 됩니다. 전체 코드는 다음과 같습니다. - (NSPersistentStoreCoordinator *)persistentStoreCoordinator { if (persistentStoreCoordinator != nil) { return persistentStoreCoordinator; } NSString *storePath = [[self applicationDocumentsDirectory] stringByAppendingPathComponent:@"NewIPhotoDiary.sqlite"]; NSURL *storeUrl = [NSURL fileURLWithPath:storePath]; NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys: [NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption, [NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption, nil]; persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel: [selfmanagedObjectModel]]; NSError *error; if (![persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeUrl options:options error:&error]) { // Update to handle the error appropriately. NSLog(@"Unresolved error %@, %@", error, [error userInfo]); exit(-1); // Fail } return persistentStoreCoordinator; } 만일 managedObjectModel 메서드도 아래 코드와 다르다면 아래 코드처럼 바꿔주세요. - (NSManagedObjectModel *)managedObjectModel { if (managedObjectModel != nil) { return managedObjectModel; } NSString *path = [[NSBundle mainBundle] pathForResource:@"NewIPhotoDiary"ofType:@"momd"]; NSURL *momURL = [NSURL fileURLWithPath:path]; managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:momURL]; return managedObjectModel; } 이제 빌드하고 실행시키시면 잘 돌아갈 것입니다…^^ 5. 마무리 처음으로 아이폰 앱을 만들 때만큼 좌충우돌, 시행착오의 연속이네요. 어렵사리 하나의 항목에 대한 글을 마쳤는데 부족한 부분이 너무 많이 느껴집니다. 이 글을 읽는 많은 분들의 양해를 바랄 뿐입니다…^^;;; 본격적으로 시작된 내용이 코어 데이터라서 더 많이 어려웠는지 모르겠습니다. 일단 이 부분에 대해서는 겸허하게 여러분들의 비판과 내용 수정을 기다리도록 하겠습니다. 이제 데이터를 처리하는 부분이 완성되었으니 다음시간부터는 iPhotoDiary의 화면을 순서대로 따라가면서 어떤 식으로 작업을 하였는지에 대해 설명을 드리도록 하겠습니다. 바로 다음 시간에는 첫 화면인 등록된 아이의 신상정보와 아이 선택 버튼이 있는 화면을 소스를 보면서 분석해보도록 하겠습니다. 앞으로는 그리 어려운 내용은 없을 것 같네요…^^ 긴 글 읽어주셔서 감사하고 다음 시간에 뵙겠습니다.
'개발 > App Developer' 카테고리의 다른 글
트위터 API 관련 (1) | 2010.12.23 |
---|---|
앱 개발자 등록 관련 블로그/사이트 (0) | 2010.11.17 |
* 놈에 대한 참고 하나더 (0) | 2010.11.12 |
앱에서 카메라 사용시 프레임웤 관련 (0) | 2010.11.12 |
맥부기펌] 9/2 트위터 인증변경관련 (0) | 2010.10.06 |