꿈돌이랜드

코코아 인터널스 - 4장 객체 복사 본문

Programming/iOS

코코아 인터널스 - 4장 객체 복사

loinsir 2023. 8. 9. 16:28
반응형

4.1 NSCopying 계열 프로토콜

코코아 프레임워크에서는 객체를 복사하기 위한 방법으로 <NSCopying> 또는 <NSMutableCopying> 프로토콜을 지정해서 필요한 메서드를 구현하는 방법을 권장한다. <NSCopying> 프로토콜은 객체를 복사하기 위해 클래스에 미리 구현해야 하는 복사용 메서드 목록을 지정해놓은 프로토콜이다. 애플이 만든 코코아 객체는 이미 <NSCopying> 프로토콜을 기반으로 만들어져 있어서 객체를 복사하기 쉽다. <NSMutableCopying> 프로토콜과 <NSCopying> 프로토콜 차이는 복사한 객체가 변경 가능한 객체인지 아닌지에 따라 달라진다.

4.1.1 복사만 가능한 객체

<NSCopying> 프로토콜은 구현해야 할 메서드가 딱 하나뿐이다. 내가 만든 객체가 복사 가능한 객체여야 한다면 <NSCopying> 프로토콜을 선언하고 다음 메서드를 구현하면 된다.

-(id)copyWithZone:(NSZone*)zone; // Objective-C
func copy(with zone: NSZone? = nil) -> Any // Swift

이 메서드 인자 값은 NSZone 타입을 가진 객체 포인터인데 앞서 2장에서 설명했듯이 더 이상 메모리 영역을 지역으로 나눠 사용하지 않는다. 이제 모든 앱은 단일 지역을 가지기 때문에 인자 값은 nil로 넘겨도 된다.

…중략

객체 인스턴스의 내부 속성 데이터를 복사할 때, 내부 데이터를 한꺼번에 인자값으로 전달할 수 있는 초기화 메서드가 있으면 새 객체 인스턴스에 자신의 데이터를 복사하기 쉽다. 내부 데이터 전체를 한꺼번에 초기화할 수 없으면, 각 속성에 대한 접근자 메서드를 통해 개별적으로 내부 데이터를 채워 넣어야 한다. 특히 내부 데이터가 객체 인스턴스라면 내부 객체의 데이터를 복사하기 위해서 할 일은 더 많아진다.

4.1.2 복사와 수정이 가능한 객체

코코아 프레임워크에서 -copy 메서드로 복사하는 객체는 불변(immutable) 객체라고 가정한다.

…중략

만약 수정 가능한 가변(mutable) 객체로 복사하려면 -mutableCopy메서드로 복사해야 한다. 복사하려는 객체가 가변 객체 형태를 지원하면서, 가변 객체로 복사 가능하도록 만들고 싶다면 <NSMutableCopying> 프로토콜을 구현하면 된다. 객체를 복사하는 과정에서 복사할 원래 객체가 가변인지 불변인지는 상관없다.

…중략

4.1.3 요약

가변 객체와 불변 객체를 모두 지원한다면 <NSCopying>과 <NSMutableCopying> 프로토콜 메서드를 모두 구현해야 한다.

4.2 얕은복사 vs 깊은복사

NSArray처럼 내부에 다른 객체를 포함하는 경우에는 객체를 복사할 때 주의해야 할 사항이 있다. 객체가 객체를 포함하고 있다는 것도 메모리 참조 관점에서 보면 참조하는 대상 객체의 메모리 주소를 포인터 변수로 접근하는 것일 뿐이다.

…중략

참조하는 포인터에 있는 주소 값만 복사하는 방식을 얕은 복사라고 한다.

…중략

포인터에 포인터 값을 그대로 할당 해도, 새로운 객체를 만드는 것이 아니다. 단지 동일한 객체를 참조할 뿐이다. 그러므로, mutableCopy 메시지를 보내서 새로운 객체를 만들고 내부 데이터를 복사해야 한다.

…중략

그러나 새로운 가변 배열을 복사해서 만들더라도 여전히 문제가 생긴다. 왜냐하면 NSMutableArray를 포함해서 파운데이션 프레임워크에 있는 모든 클래스는 얕은 복사 형태로 구현되어 있기 때문이다. NSMutableArray를 가변 복사하면 다음 그림 처럼 집합 내부에서 참조하는 Pen객체를 복사해서 새로운 객체를 만드는 것이 아니라, 참조 포인터만 복사한다.

…중략

이렇게 가변 배열 내부에 있는 객체들까지 복사하고 싶으면 깊은 복사 방식을 지원해야 한다. 다행히 배열 계열 컬렉션 클래스에는 -initWithArray: copyItems: 메서드가 있다. 다른 컬렉션 클래스에도 비슷하게 copyItems:로 끝나는 초기화 메서드가 있다. 두 번째 인자 값에 YES를 넘기면 컬렉션 내부에서 참조하는 객체에 자동적으로 -copyWithZone: 메시지를 보내서 복사한다. 하지만 초기화 메서드 외에 깊은 복사를 위한 <NSDeepCopying> 프로토콜 같은 것은 존재하지 않는다.

4.2.2 깊은 복사

…중략

깊은 복사는 복사하는 객체 내부에서 다른 객체를 참조하는 경우에 꼭 고민해야 할 문제다. 얕은 복사만 해도 문제가 없는지, 아니면 깊은 복사를 해서 모두 새로운 객체로 만들어야 하는지 판단을 내려야 한다.

…중략

배열 내부 객체들을 복사하더라도, 객체 내부에 참조하는 객체가 또다시 복사되지 않으면 진정한 의미의 깊은 복사가 아니다.

…중략

깊이 우선 탐색 방식으로 하위 노드들부터 탐색해서 새로운 객체를 만들어서 복사하고, 이어서 다른 노드를 반복 탐색하다 보면 모든 객체를 복사할 수 있다. 다만 객체 그래프를 따라서 깊이 우선 탐색 방식으로 깊은 복사를 하더라도 객체 참조 관계가 기존 객체와 항상 완벽하게 동일하다고는 할 수 없다. 탐색을 하다 보면 어떤 객체는 중간에 여러 객체에서 여러 번 참조가 될 수도 있고, 특정 객체들은 순환 참조 문제가 있을 수도 있다. 약한 참조를 갖고 있으면 해당 객체를 복사하고 약한 참조로 지정해야만 한다. 객체 관계를 완벽하게 복원하려면 깊은 복사가 어떤 형태의 객체 그래프로 그려지는지 확인한 뒤 참조 관계에 따라서 복사 방식을 결정해야 한다.

4.3 아카이브

코코아 프레임워크에서는 객체 그래프를 탐색해서 깊은 복사를 구현하기 위해 두 가지 방식을 제공한다. 첫 번째는 코어 데이터를 활용하는 방식이다. 대부분 코어 개발자가 데이터를 iOS에서 ORM처럼 활용해서 데이터베이스에 손쉽게 접근하기 위한 방식으로 이해하고 있지만, 사실 코어 데이터는 객체 그래프를 저장하기 위한 프레임워크다. 두 번째는 <NSCoding> 프로토콜과 이름 있는 아카이브(Keyed-Archive) 클래스를 활용해서 객체를 인코딩하는 것이다.

4.3.1 객체 직렬화와 아카이브의 차이

코코아 프레임워크에서 객체 직렬화는 아카이브와 마찬가지로 객체 그래프를 따라 객체의 데이터 내용을 저장하는 방식이다. 하지만 차이점은 있다. 직렬화는 주로 문자열, 배열이나 사전 컬렉션에 담겨 있는 계층 구조와 참조하는 객체 데이터만 직접적으로 저장한다. 만약 여러 곳에서 하나의 객체를 다중 참조하고 있으면, 참조마다 동일한 내용의 객체를 여러 개 저장한다. 결과적으로 다시 객체화 하면(deserialized) 동일한 객체를 다중 참조하는 것이 아니라 각기 다른 객체가 만들어진다. 더구나 데이터 값만 저장하기 때문에, 다시 객체화할 때 가변 객체 NSMutableArray인지, 불변 객체 NSArray인지 판단해서 복원할 수 없다.

객체 직렬화와 직접적으로 관련이 있는 클래스에는 NSPropertyList Serialization이 있다. 이 클래스는 파운데이션 객체 중에서 NSDictionary, NSArray, NSString, NSDate, NSData, NSNumber 타입으로 저장되어 있는 데이터 구조만 XML 기반 프로퍼티 목록(property list, 흔히 plist) 파일로 직렬화해서 저장한다. …중략… NSUserDefaults는 내부적으로 NSPropertyListSerialization을 사용하기 때문에 직렬화를 지원하는 객체 클래스가 제한적이다. 그래서 UIColor같은 클래스를 직렬화하려면 NSData를 사용해서 바이너리로 바꿔야만 한다.

4.3.2 <NSCoding> 프로토콜

앞서 설명한 XML 기반의 프로퍼티 목록 구조는 단순하고 작은 규모의 객체끼리의 객체 그래프를 저장하는 데 적합하다. 객체 관계가 복잡하거나 일정 규모 이상으로 커지면 plist 방식을 사용하기에 부적합하다. 그리고 plist는 클래스 타입을 모두 지원하지 않으며, 가변 객체나 다중 참조 관계도 원래대로 복원하지 못하다. 이런 제약 사항을 지원해야 하는 경우라면, 프로퍼티 목록 대신 <NSCoding> 프로토콜을 사용해야 한다.

<NSCoding> 프로토콜은 다음과 같이 객체 인스턴스를 인코딩하거나 다시 객체로 디코딩하기 위한 메서드 두 개만 있는 프로토콜이다.

-(void)encodeWithcoder:(NSCoder *)encoder;
-(id)initWithCoder:(NSCoder *)decoder;

-encodeWithCoder: 메서드는 해당 객체의 인스턴스 변수를 인코딩하고, -initWithCoder: 메서드는 인자 값으로 넘겨지는 decoder 객체에서 데이터를 찾아서 새로운 객체를 초기화한다.

…중략

NSCoder 클래스는 메모리상에 있는 객체 인스턴스 변수를 다른 형태로 변환하기 위한 인터페이스를 선언한 추상화 클래스로, 일부 제한된 기능만 구현되어 있다. 실제로는 NSKeyedArchiver, NSKeyedUnarchiver, NSPortCoder 같은 하위 클래스 구현체를 사용한다.

객체 그래프와 아카이브

…중략

NSCoder에서는 객체 그래프를 보다 정확하게 저장하고 복원하기 위해서 뿌리 객체(root object)와 조건부 객체(conditional objects)라는 두 가지 개념을 사용한다.

뿌리 객체는 객체 그래프에 대한 탐색을 위한 시작점을 의미한다. 아카이브를 시작하는 시점이 될 뿐이다. 뿌리 객체부터 탐색을 시작해서 기존에 인코딩했던 객체를 다시 참조하는 경우에는 새로운 객체로 인코딩하는 것이 아니라 인코딩한 기존 객체를 참조한다. NSCoder 클래스의 encodeRootObject: 메서드를 사용해서 처리한다.

조건부 객체(conditional object)는 객체 그래프에서 반드시 아카이브하지 않아도 되는 참조 객체를 의미한다. 다른 표현으로는 소유권 관계가 명확해서 어느 시점에 반드시 인코딩이 되는 특정한 객체를 참조하기 때문에 다시 인코딩할 필요는 없는 객체를 말한다. encodeConditionalObject:forKey: 메서드를 사용해서 조건부 객체를 인코딩하면 된다. 만약 아카이빙하는 동안 무조건 객체가 전혀 없이 조건부 객체만 인코딩 할 경우, 다시 디코딩해서 복원할 때 해당 객체는 참조할 수 없고 nil을 반환한다. 따라서 조건부 객체는 약한 참조 형태로 인코딩한다고 이해하면 된다.

4.3.3 이름 있는 아카이브

이름 있는 아카이브 방식으로 객체를 인코딩할 때 NSKeyedArchiver 클래스를 사용한다.

…중략

디코딩을 할 때 반드시 고려해야 하는 사항이 있다. 해당하는 키 값에 대한 데이터가 없을 수도 있다는 것이다. 만약 해당 키 값에 해당하는 데이터를 찾을 수 없으면 객체에 대해 nil이나 NO, 0.0 같은 기본 값을 반환한다. 반환값 대신 키 값에 대한 데이터가 존재하는지 여부만 판단하고 싶다면 -containsValueForKey: 메서드 리턴 값이 YES인지 확인한다. 디코딩하는 클래스에 대한 버전 정보를 기록했다가 비교하는 방식을 사용하는 방법도 있는데, NSCoder 클래스가 버전 정보를 별도로 관리해주지 않기 때문에 버전 정보를 직접 저장해야만 한다.

아카이브 델리게이트

NSKeyedArchiver나 NSKeyedUnarchiver 클래스는 델리게이트를 지정해서 각 객체를 인코딩 또는 디코딩하는 시점에 알림을 받을 수 있다.

…중략

4.3.4 아카이브 만들기

객체 그래프를 아카이브하는 간단한 방법은 NSKeyedArchiver 클래스에 있는 +archivedDataWithRootObject:toFile: 클래스 메서드나 +archivedDataWithRootObject: 클래스 메서드를 사용해서 뿌리 객체부터 아카이브를 만드는 것이다. 다음 코드는 aPen 객체를 /tmp/apen-archived 파일로 아카이브한다.

Pen *aPen = [[Pen alloc] init];
NSString *filePath = @"/tmp/apen-archived";
BOOL success = [NSKeyedArchiver archiveRootObject:aPen toFile: filePath];

뿌리 객체로 접근하는 방식을 이용하지 않고, 직접 원하는 객체를 아카이브 하려면 클래스 메서드 대신 -initForWritingWithMutableData: 메서드를 활용해서 NSKeyedArchiver 객체 인스턴스를 먼저 생성하고 초기화해야만 한다. 그후 개별적으로 -encodeObject:forKey: 메서드로 직접 인코딩을 하고, 마지막에 -finishEncoding 메서드로 인코딩 작업이 끝났다고 알려줘야 한다. 이와 같은 흐름을 예시코드로 살펴보면 다음과 같다.

Pen *aPen = [[Pen alloc] init];
NSString *filePath = @"/tmp/apen-archived";
NSMutableData *data = [NSMutableData data];
NSKeyedArchiver *archiver
	= [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];
[archiver encodeObject:aPen forKey: @"apen-unique-key"];
[archiver finishEncoding];
BOOL result = [data writeToFile:filePath atomically:YES];

4.3.5 아카이브 해제하기

아카이브해놓은 파일이나 바이너리 데이터를 해제해서 다시 객체화하는 과정도 아카이브에서 사용했던 메서드와 이름만 드를 뿐 방식은 유사하다. 클래스 메서드를 사용해서 뿌리 객체로 아카이브한 경우에는 마찬가지로 NSKeyedUnarchiver 클래스에 구현되어 있는, 뿌리 객체부터 해제하는 +unarchiveObjectWithFile: 또는 unarchiveObjectWithData: 클래스 메서드를 사용하면 된다.

NSString *filePath = @"/tmp/apen-archived";
Pen *aPen = [NSKeyedArchiver unarchiveObjectWithFile:filePath];

아카이브 해제 과정을 직접 처리하려면 아카이브 객체와 마찬가지로 아카이브 해제용 객체 인스턴스를 먼저 생성해야 한다. NSKeyedUnarchiver 클래스는 -initForReadingWithData: 초기화 메서드를 사용해서 인스턴스를 초기화해야 한다. 그 이후에 -decodeObjectForKey: 메서드로 직접 디코딩을 하고, 마지막에 -finishDecoding 메서드로 작업이 끝났다는 것을 알려줘야 한다. 이와 같은 흐름을 예시 코드로 살펴보면 다음과 같다.

NSString *filePath = @"tmp/apen_archived";
NSData *data = [NSData dataWithContentsOfFile:filePath];
NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
Pen *aPen = [unarchiver decodeObjectForKey:@"apen-unique-key"];
[unarchiver finishDecoding];
디코딩 팁: 만약 아카이브할 때 뿌리 객체부터 인코딩 작업을 한 아카이브를 -decodeObjectForKey: 메서드를 사용해서 디코딩하고 싶을 때는 키 값으로 “root”를 사용하면 된다.

4.3.6 객체 인코딩과 디코딩

NSKeyedArchiver와 NSKeyedUnarchiver는 객체 타입뿐만 아니라 저장해야 하는 데이터 타입마다 인코딩, 디코딩하는 개별 메서드를 제공한다. 이 메서드가 선언된 곳은 NSCoder 클래스고, NSCoder 서브 클래스 각각에 별도로 구현되어 있다.

…중략

특정 객체를 인코딩할 때 -encodeObject:forKey: 메서드를 호출하면 <NSCoding> 프로토콜로 정의한 -encodeWithCoder: 메서드를 호출한다.( 스위프트에서는 func encode(with coder: NSCoder) 해당 메서드 내부에서 객체 인스턴스 변수를 인코딩하면 된다.

인코딩한 객체를 디코딩할 때 -decodeObjectForKey: 메서드를 호출하면 객체를 복원하기 위해서 -initWithCoder: 메서드를 호출한다.(스위프트에서는 init?(coder: NSCoder)) 이 메서드 내부에서 객체 인스턴스 변수를 디코딩하면 된다.

4.3.7 <NSSecureCoding> 프로토콜

해당 프로토콜은 iOS6과 OS X 마운틴 라이언부터 지원하며, 기존 <NSCoding> 프로토콜에 보안성을 강화한 확장판이다. +supportsSecureCoding 클래스 메서드로 안전한 인코딩 방식 지원여부를 판단한다. -decodeObjectForKey: 메서드 대신 -decodeObjectOfClass:forKey: 메서드를 사용해서 아카이브한 파일 내부 클래스를 대체하는 공격 방식에 대비할 수 있다. 아카이브한 바이너리 데이터를 네트워크상으로 주고 받는 경우에 해킹 위험이 있어서 보안성을 강화한 버전이다. 특히 XPC 방식으로 객체를 아카이브해서 주고 받을 때는 <NSSecureCoding> 프로토콜을 반드시 구현해야만 하도록 강제하고 있다.9


Uploaded by N2T

반응형