ProgrammingInObjectiveC:Chapter 18
- 18장
- 객체 복사하기
18장 :: 객체 복사하기
이 장에서는 객체를 복사하는 작업에 관련된 세세한 사항을 다룬다. 얕은 복사와 깊은 복사라는 개념을 소개하고 Foundation 프레임워크에서 사본을 만드는 법을 설명할 것이다.
8장 「상속」에서 다음과 같은 단순한 대입 명령문으로 한 객체를 다른 객체에 할당하면 어떤 일이 발생하는지 설명했다.
origin = pt;
이 예제에서 origin 과 pt 는 모두 다음처럼 정의된 XYPoint 객체다.
@interface XYPoint: NSObject
{
int x;
int y;
};
...
@end
대입이란 것이 그저 객체 pt 의 주소를 origin 에 복사해 넣는것뿐임을 기억하자.
대입 연산이 끝나면, 두 변수는 메모리의 동일한 주소를 가리키게 된다. 다음 메시지로 인스턴스 변수를 바꾸면, 변수 origin 과 pt 가 참조하는 XYPoint 객체의 x, y 좌표가 바뀌게 된다. 둘다 메모리에서 동일한 객체를 참조하기 때문이다.
[origin setX: 100 andY: 200];
Foundation 객체들도 마찬가지다. 한 변수를 다른 변수에 대입하면 그저 객체 참조가 하나 더 생길 뿐이다(17장 「메모리 관리」에서 설명했듯이 레퍼런스 카운트가 올라가지는 않는다), 따라서 dataArray 와 dataArray2 가 모두 NSMutableArray 객체라면, 다음 명령문은 두 변수가 동시에 참조하는 동일한 배열에서 첫째 원소를 제거한다.
dataArray2 = dataArray;
[dataArray2 removeObjectAtIndex: 0];
copy 와 mutableCopy 메서드
Foundation 클래스에는 객체의 사본을 생성해 주는 copy 와 mutableCopy 라는 메서드가 있다. 객체를 복사하기 위해서는, 사본을 만들기 위한 프로토콜인 NSCopying 을 따르는 메서드를 구현해야 한다. 만일 여러분의 클래스가 객체의 수정 가능한 사본과 불가능한 사본을 구분하여 만든다면, NSMutableCopying 프로토콜을 따르는 메서드도 구현해야 한다. 이 방법은이 절의 뒷부분에서 배운다.
Foundation 클래스의 copy 메서드로 돌아가자, NSMutableArray 의 두 객체 dataArray2 와 dataArray 가 주어졌다고 하자.
dataArray2 = [dataArray mutableCopy];
이 명령문은 메모리 안에서 dataArray 사본을 생성하여 모든 원소를 복사한다. 그 후, 다음 명령문을 실행한다.
[dataArray2 removeObjectAtIndex: 0];
이로써 dataArray2 의 첫째 원소는 제거되지만, dataArray 의 첫째 원소는 그대로 유지된다. 프로그램 18.1 은 이 점을 보여 준다.
프로그램 18.1
#import <Foundation/NSObject.h>
#import <Foundation/NSArray.h>
#import <Foundation/NSString.h>
#import <Foundation/NSAutoreleasePool.h>
int main (int argc, char *argv[])
{
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
NSMutableArray *dataArray = [NSMutableArray arrayWithObjects:
@"one", @"two", @"three", @"four", nil];
NSMutableArray *dataArray2;
// 간단한 대입
dataArray2 = dataArray;
[dataArray2 removeObjectAtIndex: 0];
NSLog (@"dataArray: ");
for (NSString *elem in dataArray )
NSLog (@" %@", elem);
NSLog (@"dataArray2: ");
for ( NSString *elem in dataArray2 )
NSLog (@" %@", elem);
// 복사하고 복사본의 첫 원소를 삭제한다.
dataArray2 = [dataArray mutableCopy];
[dataArray2 removeObjectAtIndex: 0];
NSLog (@"dataArray: ");
for ( NSString *elem in dataArray )
NSLog (@" %@", elem);
NSLog (@"dataArray2: ");
for ( NSString *elem in dataArray2 )
NSLog (@" %@", elem);
[dataArray2 release];
[pool drain];
return 0;
}
프로그램 18.1 의 출력결과
dataArray:
two
three
four
dataArray2:
two
three
four
dataArray:
two
three
four
프로그램 18.1 의 출력결과(재실행)
dataArray2:
three
four
프로그램은 수정 가능한 배열 객체인 dataArray 를 정의하고 배열의 네 원소를 스트링 객체인 @"one", @"two", @"three", @"four" 로 설정한다. 이미 이야기했듯이 다음과 같이 대입문을 작성해도 그저 메모리에서 동일한 배열을 가리키는 참조를 하나 더 만들뿐이다.
dataArray2 = dataArray;
dataArray2 에서 첫째 객체를 제거하고 두 배열 객체의 원소들을 출력하면 당연히 첫 번째 원소(스트링 @"one")가 두배열 객체 레퍼런스에서 사라진다.
그 다음에는 dataArray 의 수정 기능한 사본을 만들어 dataArray2 에 대입한다. 이제 메모리에 각기 다른 수정 가능한 배열 두 개가 생성되었고 둘 다 원소를 세 개씩 담고 있다. 여기서 dataArray2 의 첫째 원소를 제거해도, dataArray 의 내용에는 아무런 영향이 없다. 프로그램의 출력 결과에서 마지막 두 줄을 보면 이를 확인할 수 있다.
객체의 수정 가능한 사본을 만든다고 해서 복사되는 객체도 수정할 수 있어야 하는 것은 아니다. 수정 불가능한 사본도 마찬가지다. 수정 가능한 객체에서 수정 불가능한 사본을 만들수도 있다.
또한 배열의 사본을 만들때 배열에 담긴 각 원소의 리테인 카운트도 복사로 인해 1 씩 자동으로 증가한다. 따라서 배열 사본을 만들고 원래 배열을 릴리스해도 사본은 여전히 유효한 원소들을 보유하게 된다.
프로그램에서 dataArray 의 사본은 mutableCopy 를 사용하여 만들었으므로 여러분이 메모리를 릴리스해 줘야 한다. 지난 장에서 메모리 관리 규칙을 이야기할 때, 복사 메서드로 생성한 객체는 직접 릴리스해 줘야 한다고 이야기했다. 이 때문에 프로그램 18.1 에서 다음 줄을 추가하였다.
[dataArray2 release];
얕은 복사와 깊은 복사
프로그램 18.1 에서는 dataArray 의 원소를 수정 불가능한 스트링으로 채워 넣는다(스트링 상수 객체는 수정할 수 없다는 사실을 기억하자). 프로그램 18.2 에서는 여기에 수정 가능한 스트링을 대신 집어넣어 배열안의 스트링 값을 바꿀 수 있도록 만들 것이다. 프로그램 18.2 를 보고 출력된 결과를 이해해 보자.
프로그램 18.2
#import <Foundation/NSObject.h>
#import <Foundation/NSArray.h>
#import <Foundation/NSString.h>
#import <Foundation/NSAutoreleasePool.h>
int main (int argc, char *argv[])
{
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
NSMutableArray *dataArray = [NSMutableArray arrayWithObjects:
[NSMutableString stringWithString: @"one"],
[NSMutableString stringWithString: @"two"],
[NSMutableString stringWithString: @"three"],
nil
};
NSMutableArray *dataArray2;
NSMutableArray *mStr;
NSLog (@"dataArray: ");
for ( NSString *elem in dataArray )
NSLog (@" %@", elem);
// 사본을 만들고, 스트링 중 하나를 바꾼다.
dataArray2 = [dataArray mutableCopy];
mStr = [dataArray objectAtIndex: 0];
[mStr appendString: @"ONE"];
NSLog (@"dataArray: ");
for ( NSString *elem in dataArray )
NSLog (@" %@", elem);
NSLog (@"dataArray2: ");
for ( NSString *elem in dataArray2 )
NSLog (@" %@", elem);
[dataArray2 release];
[pool drain];
return 0;
}
프로그램 18.2 의 출력결과
dataArray:
one
two
three
dataArray:
oneONE
two
three
dataArray2:
oneONE
two
three
다음 명령문을 작성해 dataArray 의 첫째 원소를 가져왔다.
mStr = [dataArray objectAtIndex: 0];
그런 다음, 다음 명령문을 사용하여 이 원소에 스트링 @"ONE" 을 덧붙여다.
[mStr appendString: @"ONE"];
원래 배열과 사본 배열의 첫째 원소값을 살펴보자. 둘 다 수정되었다. dataArray 에서 첫째 원소가 바뀐 이유는 이해할 만하다. 그런데 사본까지 바뀐 이유는 무엇일까? 컬렉션에서 원소를 받아올때, 해당 원소의 새 복사본이 아니라 레퍼런스를 새로 받아 온다. 따라서 objectAtIndex: 메서드가 dataArray 에 호출되었을 때, 반환된 객체는 dataArray 의 첫째 원소와 메모리상에서 동일한 객체다. 따라서 출력 결과에서 볼 수 있듯이, 이후에 스트링 객체 mStr 를 수정하면 dataArray 의 첫째 원소도 바뀌는 효과가 나타난다.
그러면 복사한 배열은 어떻게 되는 것일까? 왜 사본 배열의 첫 원소도 바뀌었을까? 기본적으로 '얕은 복사'가 수행되기 때문이다. 따라서 배열을 mutableCopy 메서드로 복사하면, 새 배열 객체를 담을 메모리 공간이 할당되고 각 원소가 새 배열에 복사되어 들어간다. 그러나 원래 배열에서 각 원소를 복사하여 새 위치에 넣는 일은 사실 원소의 레퍼런스를 복사하여 다른 배열에 집어넣는 것이다. 그래서 최종적으로 두 배열의 원소들은 메모리에서 동일한 스트링을 참조하게 된다. 이것은 이장 초반에 다룬, 객체를 다른 객체에 대입하는 작업과 마찬가지다.
배열의 각 원소를 구별되는 복사본으로 만들려면, '깊은 복사(deepcopy)'를 수행해야 한다. 이 말은 배열내 객체의 레퍼런스만이 아니라 각 객체 내용의 사본까지 만든다는 의미다(배열의 원소가 배열 객체라는 말이 무슨 뜻일지 생각해 보라). 그러나 Foundation 클래스에 있는 copy 와 mutableCopy 메서드를 사용할 때는 기본적으로 깊은 복사를 하지 않는다. 19장 「아카이빙」 에서 Foundation 에 있는 아카이빙 기능으로 객체의 깊은사본을 만드는 방법을 볼 것이다.
배열, 딕셔너리, 세트의 사본을 만들 때는 이 컬렉션들의 새로운 사본을 얻게 된다. 그러나 컬렉션 값에 변화를 주고 싶은데, 사본에는 영향을 끼치고 싶지 않다면 각 원소를 직접 복사해야 한다. 예를 들어 보자. 프로그램 18.2 에서 dataArray2 의 첫 번째 원소를 바꾸고 싶은데 dataArray 는 그대로 두고 싶다면, 다음과 같이 (stringWithString: 같은 메서드를 사용하여) 새 스트링을 만들고 dataArray2 의 첫 항목에 저장할 수 있다.
mStr = [NSMutableString stringWithString: [dataArray2 objectAtIndex: 0]];
그런 다음 mStr 에 변화를 주고, replaceObject:atIndex:withObject: 메서드를 사용하여 배열에 추가할 수 있다.
[mStr appendString @"ONE"];
[dataArray2 replaceObjectAtIndex: 0 withObject: mStr];
배열의 객체를 바꾼 후에도 mStr 와 dataArrdy2 의 첫째 원소는 메모리에서 동일한 객체를 참조하고 있음을 알아챘을 것이다. 따라서 프로그램에서 mStr 를 바꾸면, 배 열의 첫 번째 원소도 바뀌게 될 것이다. 만일 이런 결과를 원하지 않는다면, replaceObject:atIndex:withObject: 메서드가 알아서 객체를 리테인하므로, 언제든 mStr 를 릴리스하고 새 인스턴스를 생성할 수 있다.
NSCopying 프로토콜 구현하기
만일 여러분의 클래스 중에서 하나에 대해, 예를 들어 주소록에 copy 메서드를 사용해보자.
NewBook = [myBook mutableCopy];
그러면 다음과 유사한 오류 메시지를 보게 될 것이다.
*** -[AddressBook copyWithZone:]: selector not recognized
*** Uncaught exception:
*** -[AddressBook copyWithZone:]: selector not recognized
이미 언급한 대로, 여러분의 클래스에 복사 기능을 구현하려면 NSCopying 프로토콜에 따른 메서드를 하나 혹은 둘 정도 구현해야 한다.
1부 「Objective-C 2.0」 에서 물리도록 사용한 Fraction 클래스에 copy 메서드를 추가하는 방법을 살펴볼 것이다. 여기서 복사할 때 쓰는 기법은 다른 클래스 에서도 대개 잘 작동할 것이다. 만일, 그 클래스가 Foundation 클래스의 서브클래스라면 좀 더 복잡한전략을 구현해야 할 수도있다. 수퍼클래스가 이미 자신의 복사 전략을 구현해 놓았을지 모르니 그 점도 알아두어야 한다.
기억하는가? Fraction 클래스는 정수 인스턴스 변수 numerator 와 denominator 를 담고 있었다. 이 객체들 가운데 하나를 복사 하려면, 새 분수를 담을 공간을 할당하고 그저 두 정수의 값을 새 분수안으로 복사해 넣으면 된다.
NSCopying 프로토콜을 구현할 때는, 클래스에서 copyWithZone: 메서드를 구현해야만 copy 메시지에 응답할 수 있다(copy 메시지는 그저 인수를 nil 로 하여 copyWithZone: 메시지를 여러분의 클래스에 보내 준다). 이전에도 말했지만, 만일 수정 가능한 사본과 불가능한 사본을 구분하고 싶다면, NSMutableCopying 프로토콜에 따라 mutableCopyWithZone: 메서드도 구현해야 한다. 두 메서드를 모두 구현했다면 copyWithZone: 메서드는 수정 불가능한 사본을 반환하고, mutableCopyWithZone: 은 수정 가능한 사본을 반환해야 한다. 객체의 사본을 수정할 수 있게끔 만든다고 해서 복사되는 객체도 수정 가능해야 하는 것은 아니다(반대 상황도 마찬가지다). 수정이 불가능한 객체에서 수정이 가능한 사본을 만들어야 할때도 충분히 있을만하다{예로,스트링 객체를 생각해 보자).
@interface 지시어는 다음과 같이 붙인다.
@interface Fraction: NSObject <NSCopying>
Fraction 은 NSObject 의 서브클래스이고 NSCopying 프로토콜을 따른다.
구현파일인 Fraction.m 에 다음과 같이 새로운 메서드 정의를 추가하자.
-(id) copyWithZone: (NSZone *) zone
{
Fraction *newFract = [[Fraction allocWithZone: zone] init];
[newFract setTo: numerator over: denominator];
return newFract;
}
zone 인수는 프로그램에서 생성하고 다룰 수 있는 또다른 메모리 zone 과 관련되어 있다. 메모리를 많이 할당하는 프로그램을 작성하는 데다가, 이 메모리들을 zone 으로 묶어 할당을 최적화하고 싶다면? 이 zone 들을 다뤄야만 한다. copyWithZone: 에 넘기는 값을 받아 메모리 할당 메서드인 allocWithZone: 에 넘겨 준다. 이 메서드는 지정한 zone 에 메모리를 생성한다.
새 Fraction 객체를 생성한 후, 수신자의 numerator 와 denominator 변수를 그 객체에 복사한다. copyWithZone: 메서드는 객체의 새로운 사본을 반환해 준다. 이 작업은 여러분의 메서드에서 처리한다.
프로그램 18.3
// 분수 복사하기
#import "Fraction.h"
#import <Foundation/NSAutoreleasePool.h>
int main (int argc, char *argv[])
{
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
Fraction *f1 = [[Fraction alloc] init];
Fraction *f2;
[f1 setTo: 2 over: 5];
f2 = [f1 copy];
[f2 setTo: 1 over: 3];
[f1 print];
[f2 print];
[f1 release];
[f2release];
[pool drain];
return 0;
}
프로그램 18.3 의 출력결과
2/5
1/3
프로그램은 Fraction 객체인 f1 을 생성하고 2/5 로 설정한다. 그 후, 여러분의 객체에 copyWithZone: 메시지를 보내는 copy 메서드를 호출하여 사본을만든다. 이 메서드는 새 Fraction 객체를 만들어 f1 값을 복사하여 넣은 뒤, 결과를 반환한다. main에서 그 결과를 f2 에 대입한다. 그 이후에 f2 의 값을 분수 1/3 로 설정하여 원래 분수 f1 에 아무런 영향이 없는지 확인한다. 다음 코드를 보자.
f2 = [f1 copy];
프로그램에서 위 코드를 다음과 같이 바꿔 준다.
f2 = f1;
프로그램 마지막에서 f2 의 release 를 없애주면 다른 결과가나온다.
여러분의 클래스에서 서브클래스가 만들어진다면, copyWithZone: 메서드가 상속될 것이다. 그런 경우 메서드에서 다음 코드를 바꿔 줘야 한다.
Fraction *newFract = [[Fraction allocWithZone: zone] init];
이를 다음 코드로 변경해 주어야 한다.
Fraction *newFract = [[[se1f class] allocWithZone: zone] init];
이렇게 하면 copy의 수신자인 클래스에서 새 객체를 생성해낼 수 있다(예를 들어, 이 클래스의 서브클래스를 만들어 NewFraction 이라는 이름을 붙여다고 하자. 그 결과로는 Fraction 객체 대신에 상속된 메서드에서 NewFraction 객체가 새로 생성되어야 한다).
만일 수퍼클래스에도 NSCopying 프로토콜을 구현한 클래스를 위해서 copyWithZone: 메서드가 작성되어 있다고 하자. 그럴 때는 먼저 수퍼클래스에 copy 메서드를 호출하여 상속받은 인스턴스 변수를 복사한 후에 클래스에 여러분이 추가한 인스턴스 변수를 (존재한다면) 복사하는 코드를 포함시켜 줘야 한다.
여러분의 클래스에서 얕은 복사를 구현할지, 깊은 복사를 구현할지 결정해야만 한다. 결정한 후 문서화하여 여러분의 클래스를 쓰는 사용자들에게 알려 주기만 하면 된다.
세터 메서드와 게터 메서드에서 객체 복사하기
세터 메서드와 게터 메서드를 구현할 때는 인스턴스 변수에 무엇을 저장하는지,또 무엇을 읽어 오는지, 그리고 이 값들을보호할 필요가 있는지를 생각해 봐야 한다. 예를 들어, AddressCard 객체 중 하나에 setName: 메서드를 이용해 이름을 설정하는 상황을 생각해 보자.
[newCard setName: newName];
newName 이 새 카드의 이름을 담은 스트링 객체라고 가정하자. 그리고 세터 루틴 안에서 매개변수를 해당하는 인스턴스 변수에 그냥 대입만 했다고하자.
-(void) setName: (NSString *) theName
{
name = theName;
}
이제 나중에 프로그램이 newName 에 저장된 문지를 일부 바꾼다면 어떻게 될까? 주소 카드에서 대응하는 필드도 동일한 스트링 객체를 참조하기 때문에, 둘 다 의도하지 않게 바뀔 것이다.
이제까지 봤지만, 안전한 방법은 세터 루틴에서 객체 사본을 만들어 이런 예상치 못한 상황을 막는 것이다. 이 방법을 적용하기 위해서, 우리는 alloc 메서드로 새 스트링 객체를 생성하고, initWithString: 으로 그 값을 메서드에 제공된 매개변수의 값으로 설정하였다.
또한 다음과 같이 copy 를 사용하여 setName: 메서드를 작성해도 된다.
-(void) setName: (NSString *) theName
{
name = [theName copy];
}
물론 이 세터 루틴으로 메모리를 제대로 관리하려면, 먼저 이전값을 오토릴리스 해줘야 한다.
-(void) setName: (NSString *) theName
{
[name autorelease];
name = [theName copy];
}
만일 인스턴스 변수의 프로퍼티를 선언할 때 copy 속성을 지시해 주면 자동 생성된 메서드는 클래스의 (직접 작성하거나 상속
받은) copy 메서드를 사용할 것이다. 다음 프로퍼티 선언을 보자.
@property (nonatomic, copy) NSString *name;
이러한 선언으로 인해 다음과 같이 동작하는 자동 생성 메서드가 생성될 것이다.
@property (nonatomic, copy) NSString *name;
-(void) setName: (NSString *) theName
{
if (theName != name) {
[name release]
name = [theName copy];
}
}
여기서 nonatomic 을 사용하면 시스템에게 프로퍼티 접근지를 뮤텍스(mutex: mutually exclusive, 상호 배타적)' lock(잠금) 으로 보호하지 말라고지시한다. 스레드 안전코드(thread safe code)를 작성하는 사람들은 뮤텍스 락을 사용하여 두 스레드가 동시에 동일한 코드를 실행해도 처절한 문제가 발생하지 않도록 한다. 그러나 이 lock 들은 프로그램 동작을 느리게 만들 수 있다. 그러니 코드가 단일 스레드에서만 작동한다면, lock 을쓰지 않아도된다.
만일 nonatomic 이 지정되지 않거나 기본 설정인 atomic 을 지정해 주었다면, 인스턴스 변수는 뮤텍스 lock 으로 보호될 것이다. 게다가, 자동 생성된 게터 메서드는 인스턴스 변수의 값이 반환되기 전에 인스턴스 변수를 리테인하고 오토릴리스할 것이다. 가비지 컬렉션을 지원하지 않는 환경에서는 이를 통해 인스턴스 변수의 새 값을 설정하기 전에, 세터 메서드의 호출로 이전 값을 릴리스하여 인스턴스 변수가 덮어 쓰이는 것을 방지한다. 게터 메서드에서 리테인은 이전 값이 해제되지 않도록 보장한다.
비록 가비지 컬렉션 환경에서는 리테인/오토릴리스 문제가 무의미하지만(이 메서드들에 대한 호출은 무시되므로) 뮤텍스 lock 문제는그렇지 않다. 따라서 코드가 멀티스레드 환경에서 돌아간다면 atomic 접근자 메서드를 쓰는 것을 고려하자. |
게터 루틴에서도 '인스턴스 변수의 값을 보호해야 하는 것은 동일하다. 만일 객체를 반환하면, 반환한 값을 바꿔도 인스턴스 변수의 값에는 영향을 주지 않아야 한다. 이렇게 하려면, 인스턴스 변수의 사본을 만들어서 반환한다.
copy 메서드를 구현하는 작업으로 돌아오다. 복사하는 인스턴스 변수가 수정 불가능한 객체(예를들어,수정이 불가능한 스트링 객체)를 담는다면, 객체 내용을 새로 복사할 필요가 없을 수도 있다. 그럴 때는 그저 객체를 리테인하고 새 레퍼런스를 생성하는 것으로 충분하다. 예를 들어, 멤버가 name 과 email 인 AddressCard 클래스의 copy 메서드를 구현하고 있다고 하자. 다음과 같이 copyWithZone: 을 구현하면 충분할 것이다.
-(AddressCard *) copyWithZone: (NSZone *) zone
{
AddressCard *newCard = [[AddressCard allocWithZone: zone] init];
[newCard retainName: name andEmail: email];
return newCard;
}
-(void) retainName: (NSString *) theName andEmail: (NSString *) theEmail
{
name = [theName retain];
email = [theEmail retain];
}
여기서는 인스턴스 변수를 복사할 때 setName:andEmail: 메서드를 사용하지 않았는데, 이 메서드가 인수의 새 사본을 만들기 때문에 이 예제의 목적과는 맞지 않아서이다. 그 대신 새 메서드 retainName:andEmail: 을 사용하여 두 변수를 리테인하기만 한다. (물론 copyWithZone: 메서드 내에서 newCard 의 두 인스턴스 변수를 직접 설정해 줄수도 있다. 그러나 그렇게 하려면 지금까지 다루지 않았던 포인터 연산을 해야 한다. 사실, 포인터 연산이 좀더 효율적이다. 그리고 이 클래스의 사용자에게 공개적으로 사용하지 않는 [retainName:andEmail:] 메서드를 노출시키지 않으므로 언젠가는 하는 법을 익혀야 할 것이다. 그러나 지금은 때가 아니다!)
복사된 카드의 소유지는 원본 name 과 email 멤버를 수정할 수 없으니 여기서는 인스턴스 변수를 (완전한 사본을 만드는 것 대신) 리테인하는 것만으로 충분하다. 이 상황에도 맞는 말인지 생각해 봐도 좋다(힌트! 세터 메서드와 관련이 있다).
연습문제
1. AddressBook 클래스에 NSCopying 프로토콜에 따라 copy 메서드를 구현하라. mutableCopy 메서드도 구현하는 펀이 을바르다고 생각하는가? 그렇다면 그 이유는 무엇인가?
2. 8장에서 정의한 Rectangle 과 XYPoint 클래스를 수정하여 NSCopying 프로토콜을 따르도록 만들자. 두 클래스에 copyWithZone: 메서드를 추가한다. Rectangle 을 수정해서 자신의 XYPoint 멤버 인 origin 을 복사할 때 XYPoint 의 copy 메서드를 사용하도록 하자. 여기서 이 클래스들이 수정 가능한 사본과 불가능한 사본을 모두 구현하는게 맞는가? 그렇다면 그 이유를 설명하라.
3. NSDictionary 딕셔너리 객체를 생성하고 키-객체 묶음을 몇 개 채워 넣자. 그 다음에 수정 가능한 사본과 불가능한 사본을 모두 만들자. 이 사본들이 깊은 복사인가, 얕은 복사인가? 답을 검증하라.
4. 이 장에서 구현한 copyWithZone: 메서드를 이용해 새 AddressCard 를 만들었다면 이를 위해 할당된 메모리는 누가 해제해야 하는가? 왜 그런가?