ProgrammingInObjectiveC:Chapter 19
- 19장
- 아카이빙
19장 :: 아카이빙
아카이빙은 Objective-C 용어로, 하나 이상인 객체를 나중에 복구할 수 있는 형식으로 저장하는 절차다. 보통은 나중에도 읽을 수 있도록 객체를 파일에 작성하는 작업과 관련 있다. 이 장에서는 데이터를 아카이빙하는 두 가지 방법을 살펴본다. 바로 '프로퍼티 리스트'와 '키-값 코딩'이다.
XML 프로퍼티 리스트로 아카이빙하기
Mac OS X 은 XML 프로퍼티 리스트(혹은 'plist')를 사용하여 사용자 기본 환경설정, 응용 프로그램 설정,구성 설정 정보 등을 저장한다. 따라서,이 리스트들을 생성하고 읽어 들이는 방법을 알면 매우 유용하다. 이것들은 보관할 목적으로 사용되지만, 데이터 구조에 대한 프로퍼티 리스트를 생성하려 하면, 특정 클래스들은 리테인되지 않고 동일한 객체에 대한 다중 레퍼런스가 저장되지 않는다. 또한 객체를 수정할 수 있는 성질이 보존되지 않는다는 한계가 있다.
'옛 스타일' 프로퍼티 리스트는 데이터를 XML 프로퍼티 리스트와는 다른 형식으로 저장한다. 가능하다면 여러분의 프로그램에서 XML 프로퍼티 리스트를 사용하는 편이 좋다. |
사용하는 객체가 NSString, NSDictionary, NSArray, NSDate, NSData, NSNumber 형이라면, 이 클래스들에 구현된 writeToFile:atomically: 메서드를 사용하여 데이터를 파일에 기록할 수 있다. 파일에 딕셔너리나 배열을 기록하는 경우, 이 메서드는 데이터를 XML 프로퍼티 리스트 형식으로 기록한다. 프로그램 19.1 은 15장 「숫자,스트링,컬렉션」에서 생성한 간단한 용어집 딕셔너리를 프로퍼티 리스트 파일로 저장하는 방법을 보여준다.
프로그램 19.1
#import <Foundation/NSObject.h>
#import <Foundation/NSString.h>
#import <Foundation/NSDictionary.h>
#import <Foundation/NSAutoreleasePool.h>
int main (int argc, char *argv[])
{
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
NSDictionary *glossary =
[NSDictionary dictionaryWithObjectsAndKeys:
@"A class defined so other classes can inherit from it.", @"abstract class",
@"To implement all the methods defined in a protocol", @"adopt",
@"Storing an object for later use. ", @"archiving",
nil
];
if ([glossary writeToFile: @"glossary" atomically: YES])
NSLog (@"Save to file failed!");
[pool drain];
return 0;
}
writeToFile:atomically: 메시지를 여러분의 딕셔너리 객체 glossary 에 보내면, glossary 파일에 프로퍼티 리스트의 형태로 딕셔너리가 저장된다. atomically 매개 변수는 YES 로 설정되었다. 즉, 임시 백업파일에 먼저 기록하고, 기록이 성공하면 최종 데이터를 지정한 파일명인 glossary 로 옮기기를 원한다. 이것은 기록하다가 시스템이 크래시를 내는 일이 있을 때 파일에 오류가 생기지 않도록 방지하는 보호책이다. 이 경우 원본 glossary 파일이 (이미 존재했다면)손상되지 않는다.
프로그램 19.1 에서 생성한 glossary 파일 내용을 살펴보면 다음과 같을 것이다.
<?xml version="1.0"encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>abstract class</key>
string<A class defined so other classes can inherit from it.</string>
<key>adopt</key>
<string>To implement all the methods defined in a protocol</string>
<key>archiving</key>
<string>Storing an object for later use. </string>
</dict>
</plist>
이 XML 파일이 키( <key>...</key> )와 값(<string>...</string>)의 묶음으로 기록된 것을 보면 딕셔너리가 파일로 기록 되었음을 알 수 있다.
딕셔너리로 프로퍼티 리스트를 작성하면 딕셔너리의 키는 모두 NSString 객체여야 한다. 배열의 원소나 딕셔너리 값은 NSString, NSArray, NSDictioruuy, NSData, NSDate, NSNumber 객체 중 하나가 될 것이다.
파일에서 XML 프로퍼티 리스트를 읽어 들이려면 dictionaryWithContentsOfFile: 과 arrayWithContentsOfFile: 메서드중하나를사용한다. 데이터를 읽어 들이려면 dataWithContentsOfFile: 메서드를 사용하고, 파일에서 스트링 객체를 읽어 들일때는 stringWithContentsOfFile: 메서드를 사용한다. 프로그램 19.2 는 프로그램 19.1 에서 기록한 용어집을 다시 읽어 내용을 표시한다.
프로그램 19.2
#import <Foundation/NSObject.h>
#import <Foundation/NSString.h>
#import <Foundation/NSDictionary.h>
#import <Foundation/NSEnumerator.h>
#import <Foundation/NSAutoreleasePool.h>
int main (int argc, char *argv[])
{
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
NSDictionary *glossary;
glossary = [NSDictionary dictionaryWithContentsOfFile: @"glossary"];
for ( NSString *key in glossary )
NSLog (@"%@: %@", key, [glossary objectForKey: key]);
[pool drain];
return 0;
}
프로그램 19.2 의 출력결과
archiving: Storing an object for later use.
abstract class: A class defined so other classes can inherit from it.
adopt : To implement all the methods defined in a protocol
Objective-C 프로그램에서 프로퍼티 리스트를 생성해야만 하는 것은 아니다. 프로퍼티 리스트는 어디서 만들어 오든 상관없다. 프로퍼티 리스트는 간단한 문서 편집기로도 작성할 수 있고 Mac OS X 시스템의 /Developer/Application/Utilities 디렉터리에 있는 Property List Editor 를 사용해도 된다.
NSKeyedArchiver 로 아카이빙하기
스트링 , 배열, 딕셔너리만이 아니라 어느 형식의 객체든 파일에 기록할 수 있는 좀더 유연한 방법이 었다. 일단 NSKeyedArchiver 클래스를 사용하여 '키를 갖는' 아카이브를 생성해야 한다.
Mac OS X 10.2 부터 키가 있는 아카이브를 지원해 왔다. 그 전에는 NSArchiver 클래스로 '시권셜 아카이브(sequential archive)'를 만들어야 했다. 시권셜 아카이브는 아카이브의 데이터를 읽을 때, 기록된 순서대로 읽어 들어야만 한다.
키가 있는 아카이브는 아카이브의 각 필드에 이름이 있다. 여러분은 객체를 아카이브할 때, 객체에 이름 혹은 '키' 를 부여해 주는 것이다. 아카이브에서 데이터를 다시 가져올 때 동일한 키를 사용하여 가져온다. 이런 식으로, 순서에 상관없이 객체를 아카이브에 기록하고 읽어 들일 수 있다. 게다가 클래스에 새 인스턴스 변수가 추가되거나 제거되더라도, 프로그램에서 처리할수 있다.
아이폰 SDK에서는 NSArchiver,가 지원되지 않는다. 그러니 아이폰 상에서 아카이빙 하려면 NSKeyedArchiver 를 써야만 한다.
키가 있는 아카이브를 다루려면 (Foundation/NSKeyedArchiver.h) 를 임포트 해야만 한다.
프로그램 19.3은 NSKeyedArchiver 클래스의 archiveRootObject:toFile: 메서드를 사용하여 디스크에 있는 파일에 용어집을 기록한다. 이 클래스를 사용하려면 다음 파일을 프로젝트에 임포트하자.
#import <Foundation/NSKeyedArchiver.h>
프로그램 19.3
#import <Foundation/NSObject.h>
#import <Foundation/NSString.h>
#import <Foundation/NSDictionary.h>
#import <Foundation/NSKeyedArchiver.h>
#import <Foundation/NSAutoreleasePool.h>
int main (int argc, char *argv[])
{
NSAutorelease * pool [[NSAutoreleasePool alloc] init];
NSDictionary *glossary =
[NSDictionary dictionaryWithObjectsAndKeys:
@"A class defined so other classes can inherit from it",
@"abstract class",
@"To implement all the methods defined in a protocol",
@"adopt",
@"Storing an object for later use",
@"archiving",
nil
];
[NSKeyedArchiver archiveRootObject: glossary toFile: @"glossary.archive"];
[pool release];
return 0;
}
프로그램 19.3 은 터미널에 아무것도 출력하지 않는다. 그러나 다음 명령문을 주의해서 살펴보자.
[NSKeyedArchiver archiveRootObject: glossary toFile: @"glossary.archive"];
이 문장은 glossary 딕셔너리를 파일 glossary.archive 에 기록한다. 파일에는 어느 경로명이든 지정할 수 있다. 이 경우 파일은 현재 디렉터리 에 기록된다. 생성한 아카이브 파일은 프로그램 19.4 와 같이 NSKeyedUnarchiver 클래스의 unarchiveObjectWithFile: 메서드로 프로그램에 읽어 들일 수 있다.
프로그램 19.4
#import <Foundation/NSObject.h>
#import <Foundation/NSString.h>
#import <Foundation/NSdictionary.h>
#import <Foundation/NSEnumerator.h>
#import <Foundation/NSKeyedArchiver.h>
#import <Foundation/NSAutoreleasePool.h>
int main (int argc, char *argv[])
{
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
NSDictionary *glossary;
glossary = [NSKeyedUnarchiver unarchiveObjectWithFile:
@"glossary.archive"];
for ( NSString *key in glossary )
NSLog (@"%@: %@", key, [glossary objectForKey: key]);
[pool drain];
return 0;
}
프로그램 19.4 의 출력결과
abstract class: A class defined so other classes can inherit from it.
adopt: To implement all the methods defined in a protocol
archiving: Storing an object for later use.
다음 명령문은 지정한 파일을 열어 내용을 읽는다.
glossary = [NSKeyedUnarchiver unarchiveObjectWithFile: @"glossary.archive"];
이 파일은 이전 아카이브 작업에서 저장했어야 한다. 파일의 전체 경로명을 지정하거나 예제에서 사용한 것처럼 상대 경로명을 지정해도 된다.
프로그램은 용어집을 읽어 들이고나서, 내용을 열거하여 제대로 복구되었는지 확인한다.
인코딩 메서드와 디코딩 메서드 작성하기
NSString, NSArray, NSDictionary, NSSet, NSDate, NSNumber, NSData와 같은 Objective-C 의 기본 객체는 앞서 설명한 방식으로 아카이브하고 복원할 수 있다. 배열에 스트링이나 다른 배열이 저장된 상황처럼 객체가 중첩되어 있는 때도 앞서 설명한 방식으로 충분하다.
이 말은 Objective-C 시스템이 AddressBook 객체를 어떻게 아카이브해야 하는지 모르기 때문에, 이 기법으로 AddressBook 객체를 직접 아카이브할 수 없다는 의미다. 만일 다음 코드를 프로그램에 추가하여 이 객체를 아카이브하려 해보자.
[NSKeyedArchiver archiveRootObject: myAddressBook toFile: @"addrbook.arch"];
이제 프로그램을 실행시켜 보면 다음과 같은 메시지를 보게 된다.
*** -[AddressBook encodeWithCoder:]: selector not recognized
*** Uncaught exception: <NSInvalidArgumentException>
*** -[AddressBook encodeWithCoder:]: selector not recognized
archiveTest: received signal: Trace/BPT trap
이 오류 메시지를 보면 시스템이 AddressBook 클래스에서 encodeWithCoder: 메서드를 찾으려 했지만 그런 메서드는 정의한 적이 없다는 사실을 알 수 있다.
앞서 나열한 객체 외에 다른 객체를 아카이브하려면 시스템에게 여러분의 객체를 어떻게 아카이브 혹은 '인코드'해야 할지 알려 주어야 한다. 객체들을 언아카이브 하거나 '디코드'하는 방법도 알려 주어야한다. 이를 위해서 클래스를 정의하는 부분에 NSCoding 프로토콜에 따른 encodeWithCoder: 와 initWithCoder: 메서드를 추가해야 한다. 주소록 예제에서는, AddressBook 과 AddressCard 클래스에 이 메서드들을 추가해 줘야 한다.
encodeWithCoder: 메서드는 아카이버가 지정된 클래스의 객체를 인코드해야할 때마다 호출된다. 이 메서드는 어떻게 인코드 해야할지 알려 준다. 비슷한 방식으로 initWithCoder: 도 지정한 클래스의 객체를 디코드 해야할 때마다 호출된다.
일반적으로 인코더 메서드는 저장하고 싶은 객체에 있는 모든 인스턴스 변수를 아카이브하는 방법을 지정해 주어야한다. 다행스럽게도 이 작업을 도와주는 메서드들이 있다. 앞서 설명한 기본 Objective-C 클래스들은 encodeObject:forKey: 메서드를 사용하면 된다. (정수나 부동소수점 동) 기본 하부 C 데이터 형은, 표 19.1 에 나열된 메서드 중에서 선택해 사용한다. 디코더 메서드 initWithCoder: 는 반대로 동작한다. 기본 Objective-C 클래스를 디코드할 때는 decodeObject:forKey: 메서드를 사용하고, 기본 데이터 형은 표 19.1에서 적절한 디코더 메서드를 찾아쓴다.
인코더 | 디코더 |
encodeBool:forKey: | decodeBool:forKey: |
encodeInt:forKey: | decodeInt:forKey: |
encodeInt32:forKey: | decodeInt32:forKey: |
encodeInt64:forKey: | decodeInt64:forKey: |
encodeFloat:forKey: | decodeFloat:forKey |
encode Double:forKey | decodeDouble:forKey: |
표 19.1 키가 있는 아카이브에서 기본 데이터 형 인코딩 메서드와 디코딩 메서드 |
프로그램 19.5 AddressCard.h 인터페이스 파일
#import <Foundation/NSObject.h>
#import <Foundation/NSString.h>
#import <Foundation/NSKeyedArchiver.h>
@interface AddressCard: NSObject <NSCoding, NSCopying>
{
NSString *name;
NSString *email;
}
@property (copy, nonatomic) NSString *name, *email;
-(void) setName: (NSString *) theName andEmail: (NSString *) theEmail;
-(NSComparisonResult) compareNames: (id) element;
-(void) print;
// NSCopying 프로토콜의 추가 메서드
-(AddressCard *) copyWithZone: (NSZone *) zone;
-(void) retainName: (NSString *) theName andEmail: (NSString *) theEmail;
@end
프로그램 19.5는 AddressCard 와 AddressBook 클래스에 인코딩 메서드와 디코딩 메서드를 추가하였다.
다음은 AddressCard 클래스에 더한 두 메서드를 구현한 것이다.
-(void) encodeWithCoder: (NSCoder *) encoder
{
[encoder encodeObject name forKey: @"AddressCardName"];
[encoder encodeObject email forKey: @"AddressCardEmail"];
}
-(id) initWithCoder: (NSCoder *) decoder
{
name = [[decoder decodeObjectforKey: @"AddressCardName"] retain];
email = [[decoder decodeObjectforKey: @"AddressCardEmail"] retain];
return self;
}
인코딩 메서드 encodeWithCoder: 에 인수로 NSCoder 객체를 건넨다. NSObject 에게서 AddressCard 를 직접 상속받으므로, 상속받은 인스턴스 변수의 인코딩에 대해 걱정하지 않아도 된다. 만일 걱정해야 하는 경우이고, 클래스의 수퍼클래스가 NSCoding 프로토콜을 따른다면 다음 명령문으로 먼저 상속된 인스턴스 변수를 인코딩 해야한다.
[super encodeWithCoder: encoder];
주소록에는 두 인스턴스 변수 name 과 email 이 있다. 이것들은 모두 NSString 객체이기 때문에 각 변수를 인코딩할 때 encodeObject:forKey: 를 사용할 수 있다. 이 메서드를 사용하면 이 두 변수가 아카이브에 추가된다.
encodeObject:forKey: 메서드는 객체를 인코드하고, 지시된 키의 항목에 객체를 저장한다. 나중에 그 키를 이용해 객체를 찾을 수 있다. 키 이름은 데이터를 인코딩할 때 쓰는 키와 디코딩할 때 쓰는 키가 같다면 아무 이름이나 볼일 수 있다. 충돌이 발생하는 경우는 유일하게 인코딩되는 객체의 서브 클래스에서 동일한 키를 사용할때 뿐이다. 이를 방지하려면 프로그램 19.5 에서 했듯이 인스턴스 변수이름 앞에 클래스 이름을 붙여서 아카이브의 키 이름으로 쓰는 것이다.
encodeObject:forKey: 는 클래스에서 encodeWithCoder: 를 구현한 객체면 모두 사용할 수 있다.
디코딩 절차는 인코딩 절차와 반대다. initWithCoder: 메서드에 넘겨진 인수는 이번에도 NSCoder 객체다. 이 인수에 대해서는 크게 걱정할 것이 없다. 그저, 이 인수가 아카이브에서 추출하고 싶은 각 객체를 위한 메시지를 받는다는 점만 기억하자.
여기서도 AddressCard 클래스가 NSObject 에게서 직접 상속받기 때문에, 상속받은 인스턴스 변수의 디코딩을 걱정할 필요가 없다. 만일, 상속받은 변수가 있다면 디코더 메서드의 시작 부분에 다음 코드를 넣어야 할 것이다(여러분 클래스의 수퍼클래스가 NSCoding 프로토콜을 따른다고 가정하였다).
self = [super initWithCoder: decoder];
각 인스턴스 변수는 decodeObject:forKey: 를 호출하고 그 변수가 인코딩될 때 사용했던 키를 사용하여 디코드된다.
AddressCard 클래스와 유사하게 AddressBook 클래스에도 인코딩 메서드와 디코딩 메서드를 추가해 준다. 인터페이스 파일에서 바꿔야할 부분은 @interface 지시어에서 AddressBook 이 이제 NSCoding 프로토콜을 따른다고 선언해 주는 것 뿐이다. 이렇게 바꿔 주면 코드는 다음과 같을 것이다.
@interface AddressBook: NSObject <NSCoding, NSCopying>
이제 구현 파일에 포함할 메서드 정의를 보자.
-(void) encodeWithCoder: (NSCoder *) encoder
{
[encoder encodeObject: bookName forKey: @"AddressBookBookName"];
[encoder encodeObject: book forKey: @"AddressBookBook"];
}
-(id) initWithCoder: (NSCoder *) decoder
{
bookName = [[decoder decodeObjectForKey: @"AddressBookBookName"] retain];
book = [[decoder decodeObjectForKey: @"AddressBookBook"] retain];
return self;
}
프로그램 19.6 은 변경한 클래스를 테스트해 줄 프로그램이다.
프로그램 19.6 테스트 프로그램
#import "AddressBook.h"
#import <Foundation/NSAutoreleasePool.h>
int main (int argc, char *argv[])
{
NSString *aName = @"Julia Kochan";
NSString *aEmail = @"jewls337@axlc.com";
NSString *bName = @"Tony Iannino";
NSString *bEmail = @"tony.iannino@techfitness.com";
NSString *cName = @"Stephen Kochan";
NSString *cEmail = @"steve@steve_kochan.com";
NSString *dName = @"Jamie Baker";
NSString *dEmail = @"jbaker@hitmail.com";
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
AddressCard *card1 = [[AddressCard alloc] init];
AddressCard *card2 = [[AddressCard alloc] init];
AddressCard *card3 = [[AddressCard alloc] init];
AddressCard *card4 = [[AddressCard alloc] init];
AddressBook *myBook = [AddressBook alloc];
// 주소 카드를 네 개 설정한다.
[card1 setName: aName andEmail: aEmail];
[card2 setName: bName andEmail: bEmail];
[card3 setName: cName andEmail: cEmail];
[card4 setName: dName andEmail: dEmail];
myBook = [myBook initWithName: @"Steve's Address Book"];
// 주소록에 카드를 추가한다.
[myBook addCard: card1];
[myBook addCard: card2];
[myBook addCard: card3];
[myBook addCard: card4];
[myBook sort];
if ([NSKeyedArchiver archiveRootObject: myBook toFile: @"addrbook.arch"] == NO)
NSLog (@"archiving failed");
[card1 release];
[card2 release];
[card3 release];
[card4 release];
[myBook release];
[pool drain];
return 0;
}
이 프로그램은 주소록을 생성하여 파일 addressbook.arch 에 아카이브한다. 아카이브 파일을 생성하는 과정에서 AddressBook 과 AddressCard 의 인코딩 메서드가 호출 되었음에 주의하자. 이를 확인하고 싶다면 NSLog 를 이 메서드들에 추가해 보라.
프로그램 19.7은 파일에서 주소록을 생성하기 위해 아카이브를 메모리로 읽어들이는 방법을 보여 준다.
프로그램 19.7
#import "AddressBook.h"
#import <Foundation/NSAutoreleasePool.h>
int main (int argc, char *argv[])
{
AddressBook *myBook;
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
myBook = [NSKeyedArchiver unarchiveObjectWithFile: @"addrbook.arch"];
[myBook list];
[pool drain];
return 0;
}
프로그램 19.7 의 출력결과
======== Contents of: Steve's Address Book ========
Jamie Baker jbaker@hitmail.com
Julia Kochan jewls337@axlc.com
Stephen Kochan steve@steve_kochan.com
Tony Iannino tony.iannino@techfitness.com
===================================================
주소록을 언아카이빙하는 과정에서 두 클래스에 추가된 디코딩 메서드가 자동으로 호출되었다. 프로그램에 주소록을 다시 읽어 들이기가 얼마나 쉬운지 알아챘을 것이다.
이미 언급했듯이, encodeObject:forKey: 메서드는 내장되어 있는 클래스와, NSCoding 프로토콜에 따른 인코딩 메서드와 디코딩 메서드를 구현해 준 클래스에 대해서 동작한다. 여러분의 인스턴스가 정수나 부동소수점 수 같은 기본 데이터 형을 포함한다면, 이것들을 인코딩하고 디코딩할 방법을 알고 있어야 한다(표 19.1 을보라).
NSString, int, float 형은 세 인스턴스 변수를 보유한 클래스 Foo 를 간단하게 정의해 보자. 이 클래스는 아카이빙할 때 쓸 세터 메서드 하나와 게터 메서드 세 개 그리고 인코딩/디코딩 메서드 두 개를 갖고 있다.
@interface Foo: NSObject <NSCoding>
{
NSString *strVal;
int intVal;
float floatVal;
}
@property (copy, nonatomic) NSString *strVal;
@property int intVal;
@property float floatVal;
@end
다음은 이것들을 구현한 파일이다
@implementation Foo
@synthesize strVal, intVal, floatVal;
-(void) encodeWithCoder: (NSCoder *) encoder
{
[encoder encodeObject: strVal forKey: @"FoostrVal"];
[encoder encodeInt: intVal forKey: @"FoointVal"];
[encoder encodeFloat: floatVal forKey: @"FoofloatVal"];
}
-(id) initWithCoder: (NSCoder *) decoder
{
strVal = [[decoder decodeObjectForKey: @"FoostrVal"] retain];
intVal = [decoder decodeIntForKey: @"FoointVal"];
floatVal = [decoder decodeFloatForKey: @"FoofloatVal"];
return self;
}
@end
인코딩 루틴은 앞에서 본 것과 같이 encodeObject:forKey: 메서드를 사용하여 먼저 스트링 값 strVal 을 인코드한다.
프로그램 19.7 에서는 Foo 객체를 하나 생성해 파일에 저장한 후, 불러들여 언아카이빙한 뒤, 이를 표시한다.
프로그램 19.8 테스트 프로그램
#import <Foundation/NSObject.h>
#import <Foundation/NSString.h>
#import <Foundation/NSKeyedArchiver.h>
#import <Foundation/NSAutoreleasePool.h>
#import "Foo.h" // Foo 클래스의 정의
int main (int argc, char *argv[])
{
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
Foo *myFoo1 = [[Foo alloc] init];
Foo *myFoo2;
[myFoo1 setStrVal: @"This is the string"];
[myFoo1 setIntVal: 12345];
[myFoo1 setFloatVal: 98.6];
[NSKeyedArchiver archiveRootObject: myFoo1 toFile: @"foo.arch"];
myFoo2 = [NSKeyedUnarchiver unarchiveObjectWithFile: @"foo.arch"];
NSLog (@"%@\n%i\n%g", [myFoo2 strVal], [myFoo2 intVal], [myFoo2 floatVal]);
[myFoo1 release];
[pool drain];
return 0;
}
프로그램 19.8 의 출력결과
This is the string
12345
98.6
다음 메시지들은 이 객체에 있는 세 인스턴스 변수를 아카이브한다.
[encoder encodeObject: strVal forKey: @"FoostrVal"];
[encoder encodeInt: intVal forKey: @"FoointVal"];
[encoder encodeFloat: floatVal forKey: @"FoofloatVal"];
표 19.1 에는 char, short, long, long long 같은 몇몇 기본 데이터 형이 나오지 않았다. 이것들은 직접 데이터 객체의 크기를알아내서, 그에 적절한루틴을 사용해야 한다. 예를 들어 shortint 는 보통 16비트이고 int 와 long 은 32비트 이거나 64비트일 수 있다. long long 은 64비트다(13장 「하부 C 언어 기능」 에서 설명한 sizeof 연산자를 사용하여 데이터 형의 크기를 알아낼 수 있다). 따라서 short int 를 저장하려면 먼저 int 에 넣고 encodeInt:forKey: 메서드로 아카이브한다. 아카이브에서 복원하려면 이 절차를 반대로 수행하면 된다. 즉, decodeInt:forKey:를 사용하고 short int 변수에 대입하면 된다.
NSData 를 사용하여 커스텀 아카이브 만들기
앞의 프로그램 예제들처럼 archiveRootObject:toFile: 메서드를 사용하여 객체를 직접 파일에 기록하고 싶지 않을 수도 었다. 예컨대,객체의 전부 또는 일부를 모아, 하나의 아카이브 파일에 저장하고 싶은 경우가 있다. Objective-C 에서 이를 하려면 일반 데이터 스트링 객체 클래스인 NSData 를 사용하면 된다.16장 「파일 다루기」 에서 짧게 설명했다.
16장에서도 언급했지만 NSData 객체는 데이터를 저장할 메모리 공간을 예약하는데 사용한다. 이 데이터 공간은 일반적으로 파일에 저장될 데이터의 임시 저장소가 되기도 하고 디스크에서 읽은 파일 내용을 담는 곳도 된다. 수정할 수 있는 데이터 공간을 생성하는 가장 쉬운방법은 data 메서드를 사용하는 것이다.
dataArea = [NSMutableData data];
이 코드를 작성하면 프로그램이 실행되는 동안 필요에 따라 크기가 늘어나는 빈 버퍼 공간이 생성된다.
간단한 예제를 보자. 주소록을 아카이브하고 동일한 파일에 Foo 객체도 담으려한다. 이 예제는 AddressBook 과 AddressCard 에 키 가 있는 아카이브 메서드를 추가했다고 가정한다(프로그램 19.9를 보라).
프로그램 19.9
#import <Foundation/NSObject.h>
#import <Foundation/NSAutoreleasePool.h>
#import <Foundation/NSString.h>
#import <Foundation/NSKeyedArchiver.h>
#import <Foundation/NScoder.h>
#import <Foundation/NSData.h>
#import "AddressBook.h"
#import "Foo.h"
int main (int argc, char *argv[])
{
NSAutorelesePool * pool = [[NSAutoreleasePool alloc] init];
Foo *myFoo1 = [[Foo alloc] init];
Foo *myFoo2;
NSMutableData *dataArea;
NSKeyedArchiver *archiver;
AddressBook *myBook;
// 프로그램 19.7의 코드를 이곳에 추가하여
//Address Book 클래스의 인스턴스인 myBook이
// 네 개의 주소 카드를 담도록 한다.
[myFoo1 setStrVal: @"This is the string"];
[myFoo1 setIntVal: 12345];
[myFoo1 setFloatVal: 98.6];
// 데이터 영역을 설정하고 NSKeyedArchiver 객체에 이것을 연결한다.
dataArea = [NSMutableData data];
archiver = [[NSKeyedArchiver alloc]
initForWritingWithMutableData: dataArea];
// 이제 객체를 아카이브할 수 있다.
[archiver encodeObject: myBook forKey: @"myaddrbook"];
[archiver encodeObject: myFoo1 forKey: @"myfoo1"];
[archiver finishEncoding];
// 아카이브된 데이터 영역을 파일로 기록한다.
if (]dataArea writeToFile: @"myArchive" atomically: YES
encoding: NSUTF8Encoding error: nil] == NO)
NSLog (@"Archiving failed!");
[archiver release];
[myFoo1 release];
[pool drain];
return 0;
}
NSKeyedArchiver 객체를 생성하고 난 후, initforWritingWithMutableData: 메시지를 보내 아카이빙한 데이터를 기록할 공간을 지정한다. 이 공간은 앞서 만들었던 NSMutableData 객체인 dataArea 영역이다. archiver 에 저장된 NSKeyedArchiver 객체는 이제 인코딩 메시지를 받아 프로그램의 객체를 아카이브할 수 있다. 사실 finishEncoding 메시지를 받을 때까지, 이 객체는 인코딩 메시지로 받은 것을 지정된 데이터 공간에 모두 아카이브하여 저장한다.
여기서는 두 객체를 인코드한다. 첫 번째 객체는 주소록이고 두 번째는 Foo 객체다. 앞서 AddressBook 과 AddressCard, Foo 클래스에 인코더와 디코더 메서드를 구현 하였으므로 우리는 encodeObject:forKey:를 사용할 수 있다(개념은 매우 중요하니 꼭 이해하자).
두 객체를 아카이빙하고 나면, archiver 객체에 finishEncoding 메시지를 보낸다. 이 지점이 지나고 나서는 더는 다른 객체를 인코드할 수 없다. 그러므로 아카이빙 과정을 마칠때 이 메시지를 보내야 한다.
dataArea 라는 이름으로 구분한 저장 공간은 이제 아카이브된 객체를 파일에 기록할수 있는 형태로 담는다.
[dataArea writeToFile: @"myArchive" atomically: YES
encoding: NSUTF8Encoding error: nil]
이 메시지 표현식은 'WriteToFile:atomi때ly:eno떼ng:error: 메시지를 데이터 스트링에 보내,그 데이터를 myArchive라는 이름으로 지정한 파일에 저장하라고 요청해준다.
if 문에서 볼 수 있듯이, writeToFile:atomically:encoding:error: 메서드는 BOOL 값을 반환한다. 파일 기록이 성공하면 YES를 반환하고, 실패하면 NO 를 반환한다(지정한 파일의 경로명이 유효하지 않거나, 파일 시스템이 꽉 찬 경우일 것이다). 아카이브 파일에서 데이터를 복원하는 일은 쉽다. 그저 데이터를 저장했던 과정을 반대로 밟아 나가면 된다. 먼저, 이전과 같은 데이터를 생성해야 한다. 그리고는 아카이브 파일을 그 데이터 공간에 읽어 들인다. 그 다음에는 NSKeyedUnarchiver 객체를 만들어서 지정된 공간에 데이터를 디코드하라고 지시한다. 아카이브한 객체를 추출하고 또 디코드 하려면 디코드 메서드를 호출해 줘야만한다. 이 작업을 마치면 NSKeyedUnarchiver 객체에 finishDecoding 메시지를 보낸다.
프로그램 19.10 에서 이 모든 작업을 볼 수 있다.
프로그램 19.10
#import <Foundation/NSObject.h>
#import <Foundation/NSAutoreleasePool.h>
#import <Foundation/NSString.h>
#import <Foundation/NSKeyedArchiver.h>
#import <Foundation/NSCoder.h>
#import <Foundation/NSData.h>
#import "AddressBook.h"
#import "Foo.h"
int main (int argc, char *argv[])
{
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
NSData *dataArea;
NSKeyedUnarchiver *unarchiver;
Foo *myFoo1;
AddressBook *myBook;
// 아카이브를 읽어 NSKeyedUnarchiver 객체를 연결한다.
dataArea = [NSData dataWithContentsOfFile: @"myArchive"];
if (! dataArea) {
NSLog (@"Can't read back archive file!");
Return 1;
}
unarchiver = [[NSKeyedUnarchiver alloc]
initForReadingWithData: dataArea];
// 이전에 아카이브에 저장했던 객체를 디코드한다.
myBook = [unarchiver decodeObjectForKey: @"myaddrbook"];
myFoo1 = [unarchiver decodeObjectForKey: @"myfoo1"];
[unarchiver finishDecoding];
[unarchiver release];
// 복원이 성공했는지 확인한다.
[myBook list];
NSLog ("%@\n%i\n%g", [myFoo1 strVal],
[myFoo1 intVal], [myFoo1 floatVal]);
[pool drain];
return 0;
}
프로그램 19.10 의 출력결과
======== Contents of: Steve's Address Book =========
Jamie Baker jbaker@hitmail.com
Julia Kochan jewls337@axlc.com
Stephen Kochan steve@steve_kochan.com
Tony Iannino tony.iannino@techfitness.com
===================================================
This is the string
12345
98.6
출력된 결과는 아카이브 파일에서 주소록과 Foo 객체를 성공적으로 복원했음을 보여준다.
아카이버를 사용하여 객체 복사하기
프로그램 18.2 에서 수정 가능한 스트링 원소를 담은 배열 사본을 만들면서 배열의 얕은 사본이 어떻게 만들어 지는지 확인하였다. 이를 통해 실제 스트링 자체는 복사되지 않고 스트링을 가리키는 레퍼런스들만 복사 되었음을 알게 되었다.
Foundation 에 있는 아카이빙 기능을 쓰면 객체의 깊은 사본을 만들 수 있다. 한 예로, 프로그램 19.11은 버퍼에 dataArray 를 아카이빙하고 나서 언아카이빙한 다음, 그 결과를 dataArray2 에 대입했다. 이로써 dataArray 를 dataArray2 에 복사한다. 이 과정에서는 파일을 사용할 필요가 없다. 아카이빙과 언아카이빙 과정은 모두 메모리상에서 일어날 수 있다.
프로그램 19.11
#import <Foundation/NSObject.h>
#import <Foundation/NSAutoreleasePool.h>
#import <Foundation/NSString.h>
#import <Foundation/NSKeyedArchiver.h>
#import <Foundation/NSArray.h>
int main (int argc, char *argv[])
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSData *data;
NSMutableArray *dataArray = [NSMutableArray arrayWithObjects:
[NSMutableString stringWithString: @"one"],
[NSMutableString stringWithString: @"two"],
[NSMutableString stringWithString: @"three"],
nil
];
NSMutableArray *dataArray2;
NSMutableString *mStr;
// 아카이버를 사용해 깊은 사본을 만든다.
data = [NSKeyedArchiver archiveDataWithRootObject: dataArray];
dataArray2 = [NSKeyedUnarchiver unarchiveObjectWithData: data];
mStr = [dataArray2 objectAtIndex: 0];
[mStr appendString: @"ONE"];
NSLog (@"dataArray: ");
for ( NSString *elem in dataArray )
NSLog ("%@", elem);
NSLog (@"\ndataArray2: ");
for ( NSString *elem in dataArray2 )
NSLog ("%@", elem);
[pool drain];
return 0;
}
프로그램 19.11 의 출력결과
dataArray:
one
two
three
dataArray2:
oneONE
two
three
출력 결과를 보면 알 수 있듯 dataArray 2의 첫째 원소를 바꿔도 dataArray 의 첫째 원소에는 아무런 영향이 미치지 않는다. 그 이유는 아카이빙과 언아카이빙 과정을 거쳐 스트링의 새 사본을 만들었기 때문이다.
프로그램 19.11 에서는 다음 두 줄짜리 코드로 복사가 이루어진다.
data = [NSKeyedArchiver archivedDataWithRootObject: dataArray];
dataArray2 = [NSKeyedUnarchiver unarchiveObjectWithData: data];
다음처럼 한 줄짜리 명령문으로 복사해 증가에 대입하는 과정을 없앨 수도 있다.
dataArray2 = [NSKeyedUnarchiver unarchiveObjectWithData:
[NSKeyedArchiver archivedDataWithRootObject: dataArray]];
이 기법을 사용하여 객체의 깊은 사본을 만들거나, 혹은 NSCoding 프로토콜을 지원하지 않는 객체를 복사할 수 있다.
연습문제
1. 프로그램 15.7 에서 소수의 표를 생성했다. 이 프로그램을 수정하여 결과 배열을 primes.pl 파일에 XML 프로퍼티 리스트로 기록하도록 만들자. 그 후 파일 내용을 확인한다.
2. 연습문제 1 에서 생성한 XML 프로퍼티 파일을 읽어 그 값을 배열 객체에 저장하는 프로그램을 작성하라. 배열의 모든 원소를 표시하여 복원 과정이 성공했는지 확인한다.
3. 프로그램 19.2를 수정하여 /Library/Preference 폴더에 저장된 XML 프로퍼티 리스트(.plist 파일) 중 하나의 내용을 표시하라.
4. 다음과 같은 커맨드라인으로 아카이브로 저장된 AddressBook 을 읽고, 입력한 이름을 검색하는 프로그램을 작성하라.