본문 바로가기
프로그래밍 놀이터/iOS

[Objective-C] 객체 복사와 저장

by 돼지왕 왕돼지 2018. 1. 5.
반응형


 [Objective-C] 객체 복사와 저장

출처 : OS X 구조를 이해하면서 배우는 Objective-C Chap 12.


Notice : 정리자(돼지왕 왕돼지)가 remind 하고 싶은 내용이나 모르는 내용 기반으로 정리하는 것이기 때문에 구체적인 내용은 책을 사서 보시기를 권장드립니다.



13.1. 객체 복사


* 13.1.1. 얕은 복사와 깊은 복사



* 13.1.2. 존이란


-

Cocoa 에서 이전에는 동적으로 확보된 메모리 관리를 할 때 존(zone)이라고 부르는 영역을 이용했다.

현재(Objective-C 2.0 또는 가비지 컬렉션을 사용할 때) 사용하지 않지만 copyWithZone: 과 같은 함수들의 인수 형태로 형식적으로 남아 있다.



-

동적으로 메모리 영역이 확보되는 힙(heap)영역에는 주소 공간 중에서도 상당히 넓은 구획이 사용된다.

한편, 공간적인 지역 참조성이나 가상 메모리 관리 관점에서는 동시에 사용되는 경향이 많은,

관련성이 높은 데이터군은 메모리에서 가까운 위치에 모아서 배치하는 쪽이 효율적인 메모리 접근이 가능하다.


따라서 힙 안에 몇 가지 구획을 두고 관련 깊은 데이터와 객체는 특정 구획에서 메모리가 할당 되도록 한다.

이 구획이 존이다.



-

존은 각각에 메모리 관리를 하는 기능이 있어 필요에 따라 크기를 증가시킨다.

디폴트 존은 프로그램 실행 개시 직후부터 생기며 따로 지정하지 않으면 인스턴스 객체는 디폴트 존에서 생성된다.



-

존은 메모리 접근의 효율화를 노리고 설계한 구조지만 애플 사의 문서를 보면 보통은 디폴트 존만 이용해도 충분한 실행 효율을 얻을 수 있다고 되어 있는데, 요즘은 거의 사용되지 않는다.

다만, 인스턴스 생성과 복사 구조에 그 흔적이 남아 있다.


+(id)allocWithZone:(NSZone*)zone;


NSZone 형은 존을 표현하기 위한 데이터 구조이다.

모던 런타임은 인수 zone 을 무시하므로 보통은 NULL 을 넘긴다.

메서드 기능은 alloc 과 같다.




* 13.1.3. 복사 메서드 정의


-

NSObject 에는 copy 라는 메서드가 있어 리시버를 복사해서 새로운 인스턴스를 작성할 수 있다.

하지만 실제로 복사 처리를 하는 건 copy 가 아니라 copyWithZone: 이라는 인스턴스 메서드이다.

인스턴스 객체에 copy 메시지를 보내면 인수로 NULL 을 지정해서 자기 자신의 copyWithZone: 을 호출하는데, 이 메서드가 실제로 새로운 인스턴스를 생성한다.


이런 구조이므로 인스턴스를 복사하려면 copy 메서드가 아닌 copyWithZone: 메서드를 정의해야 한다.

메서드 copyWithZone: 의 반환값은 복사해서 생성한 새로운 객체지만 어떤 이유로 복사에 실패했을 경우에는 nil 을 돌려준다.



-

프로토콜 NSCopying 안에는 메서드 copyWithZone: 만 선언되어 있으므로 클래스에서 이 프로토콜을 채용해서 메서드를 구현한다.



-

NSCopying 프로토콜은 헤더 파일 Foundation/NSObject.h 에 정의되어 있다.

하지만 NSObject 자체가 이 프로토콜을 채용하진 않는다.

메서드 copy 는 NSObject 에서 편의상 정의해둔 것일 뿐이다.



-

copyWithZone: 정의 방법은 아래와 같다.


1. 슈퍼 클래스가 메서드 copyWithZone: 을 구현하지 않았다면 alloc 과 init 을 사용해서 새로운 인스턴스를 생성하고, 그 인스턴스 변수를 필요에 따라 각각 복사해서 대입한다.

2. 슈퍼 클래스가 메서드 copyWithZone: 을 구현했으면 그것을 호출해서 인스턴스를 생성하고 서브 클래스에서 추가한 인스턴스 변수를 필요에 따라 각각 복사해서 대입한다.

3. 각각의 인스턴스 변수는 공유할 수 있으면 복사하지 않아도 된다. 수동 카운터 관리 방식이라면 retain 이 필요하다.

4. 카운터 관리 방식일 경우 메서드 copyWithZone: 및 copy 는 호출한 쪽이 객체의 소유자가 된다. 따라서 반환값에 autorelease 를 적용해서는 안 된다.

5. 변하지 않는 객체의 클래스에 메서드 copyWithZone: 을 정의할 경우라면 반드시 새로운 객체를 생성해야 하는 건 아니다. 수동 카운터 관리 방식일 경우에는 self 에 retain 을 적용해서 반환한다. ARC 나 가비지 컬렉션이라면 self 를 돌려준다.



-

인스턴스 객체를 바이트 배열로 보고 통째로 복사해서 다른 객체를 돌려주는 함수 NSCopyObject() 함수가 있다.

이 함수는 실수하기 쉽고 위험하므로 비추천되며, Deprecated 되었다.

특히 ARC 를 사용할 때는 사용해서는 안 된다.




* 13.1.4. copy 메서드 예제


-

상속을 생각하는 경우라면 메서드 안에 자기 자신의 고정된 클래스명을 사용하지 않는 것이 좋다.




* 13.1.5. 변경 가능한 복사의 작성


-

mutableCopy 를 사용하면 변경 불가능 객체에서 변경 가능 객체를 생성할 수 있다.


-(id)mutableCopy


이 메서드는 NSObject 에 편의를 제공하기 위해 정의된 것으로, 실제로 인스턴스를 생성하려면 메서드 mutableCopyWithZone: 을 사용한다.


-(id)mutableCopyWithZone: (NSZone*)zone


NSMutableCopying 프로토콜에 선언되어 있으며 NSMutableCopying 프로토콜에는 mutableCopyWithZone: 메서드만 존재한다.

프로토콜은 헤더 파일 foudnation/NSObject.h 에 있으며, NSObject 자체는 이 프로토콜을 채용하지 않는다.



-

만약 어떤 변경 가능 객체와 변경 불가능 객체를 짝으로 클래스 정의하려 한다면 메서드 mutableCopyWithZone: 을 정의해두면 편리하다.




13.2. 아카이브

* 13.2.1. 객체 아카이브화


-

객체를 변환하여 얻은 바이트 배열을 아카이브(archive)라고 한다.



-

아카이브가 모든 데이터 저장에 적합한 것은 아니다.

객체를 아카이브로 저장하려면 저장방법과 복원 방법을 구현해야 한다.

아카이브는 객체의 구체적인 구조와 밀접한 관계가 있으므로 클래스 정의나 객체 사이의 관계를 변경하면 아카이브 방법도 동시에 변경해야 한다.



-

XML 이나 프로퍼티 리스트 등 범용성이 높은 형식으로 데이터를 저장함으로써 프로그램의 생산성과 유지 보수성이 좋아질 때도 많다.




* 13.2.2. Foundation 프레임워크의 아카이브 기능


-

객체를 바이트 배열로 변환하는 것을 아카이브한다, 아카이브화한다, 인코딩한다고 하며 객체로 재변환하는 것을 언아카이브(unarchive)한다, 디코딩한다, 단순히 객체를 복원한다고 한다.



-

Foundation 프레임워크로 작성된 아카이브는 시스템의 아키텍처에 의존하지 않는다.

즉, 파워 PC 에서도 인텔에서도 사용할 수 있다.

객체의 인스턴스 변수 중 정수나 실수 같은 단순한 자료형이나 다른 객체에 대한 참조는 아카이브할 수 있다.

보통의 포인터 자체는 아카이브할 수 없지만, 포인터가 가리키는 자료형은 아카이브할 수 있다.



-

Foundation 프레임워크는 객체 그래프를 아카이브화할 수 있기 때문에 객체를 따라가는 기원이 되는 객체를 루트 객체라고 부른다.

객체 그래프에는 닫힌 경로가 포함되어도 괜찮다.



-

객체를 아카이브 또는 언아카이브하기 위해 NSKeyedArchiver 와 NSKeyedUnArchiver 라는 클래스가 있다.

이들은 추상 클래스인 NSCoder 의 서브 클래스이다.



-

아카이브 대상이 되는 객체는 모두 NSCoding 프로토콜에 적합해야 한다.

이 프로토콜은 Foundation/NSObject.h 에 정의되어 있는데, NSObject 자체는 이 프로토콜을 채용하지 않는다.



-

NSString, NSDictionary 등 Foundation 프레임워크의 주요 클래스는 NSCoding 프로토콜에 적합하다.



-

NSCoding 프로토콜은 다음의 함수들을 갖는다.

-(void)encodeWithCoder:(NSCoder*)aCoder;

-(id)initWithCoder:(NSCoder*)aDecoder;




* 13.2.3. 아카이브화 메서드 정의


-

NSCoding 프로토콜 중 메서드 encodeWithCoder: 는 자신을 아카이브하는 방법을 정의한다.

인수에는 NSCoder(구체적으로 NSKeyedArchiver)가 전달되며, 이것을 아카이버(archiver)라고 부른다.


여기서는 super 에 대해 encodeWithCoder: 를 먼저 호출하고,

다음으로 해당 클래스가 독자적으로 가진 인스턴스 변수를 인코딩한다.



-

아카이브에는 무엇을 인코딩 또는 디코딩할지 지정하기 위해 NSString 문자열을 키로 지정한다.

어떤 클래스의 인스턴스를 아카이브할 때 그 인스턴스 변수의 각각에는 다른 키를 지정해야 한다.

슈퍼 클래스에서 지정된 키를 서브 클래스에서 사용하면 안 된다.

키는 같은 클래스 내부에서 구별할 수 있으면 되므로 다른 클래스 사이에 같은 키가 사용되더라도 문제는 없다.



-

객체의 아카이브에는 메서드 encodeObject:forKey: 를 사용한다.

아카이버는 첫 번째 인수의 객체에 대해 encodeWithCoder:를 호출해서 인코딩하므로 이런 메서드를 재귀적으로 반복하게 된다.

객체 그래프에 닫힌 경로가 있을 때는 같은 객체의 인코딩 요구가 여러 번 발생하는 일이 있는데, 일단 아카이브한 객체를 중복해서 아카이브하지 않는다.



-

C 자료형, 즉 정수나 실수 등에는 각자 대응하는 인코딩용 메서드가 있다.

배열이나 구조체 등은 바이트 배열로 다뤄야 한다.



-

어떤 객체를 아카이브하려고 할 때 그 객체가 참조하는 객체를 모두 아카이브할 필요는 없다.


그러나 아카이브에서 제외시키려는 객체가 다른 인스턴스 변수를 통해 다시 참조될 수 있고, 이때는 유지되어야 한다면,

인코딩하지 않는 encodeConditionalObject:forKey: 라는 메서드를 사용할 수 있다.




* 13.2.4. 언아카이브 메서드 정의


-

아카이브에서 원래 객체를 복원하려면 클래스 각자가 NSCoding 프로토콜의 메서드 initWithCoder: 를 초기자로 정의해야 한다.

인수에는 NSCoder(구체적으로는 NSKeyedUnarchiver) 가 전달되는데 이 객체를 언아카이버(unarchiver)라고 부른다.



-

인코딩과 디코딩의 키가 같다면 디코딩 순서가 무작위이거나 복원하지 않는 변수가 있어도 괜찮다.



-

-(void)encodeObject:(id)objv forKey:(NSString*)key

     인수 객체를 문자열을 키로 해서 인코딩한다.


-(void)encodeConditionalObject:(id)objv forKey:(NSString*)key

     인수 objc 가 객체 그래프 안에서 필요할 때 문자열을 키로 해서 인코딩한다.


-(id)decodeObjectForKey:(NSString*)key

     지정된 키로 인코딩된 객체를 복원한다.

     반환값 객체는 필요하면 retain 을 사용해 유지한다. (수동 카운터 관리방식만)



* 13.2.5. 아카이브와 언아카이버의 초기화 메서드


-

NSKeyedArchiver 인스턴스는 다음 초기자를 갖는다.


-(id)initForWritingWithMutableData:(NSMutableData*)data

     미리 작성해둔 NSmutableData 인스턴스를 인수로 NSKeyedArchiver 인스턴스를 초기화한다.

     인수의 데이타 객체는 유지된다.

     아카이브는 이 데이터 객체에 작성된다.



-

모든 아카이브화가 끝나면 마지막으로 finishEncoding  메서드를 호출해 뒷처리를 해야 한다.


-(void)finishEncoding



-

NSKeyedUnaArchiver 는 다음 초기화 메서드를 갖는다.


-(id)initForReadingWithData:(NSData*)data

     리시버를 인수의 데이터 객체가 가진 아카이브를 복원하기 위해 초기화한다.

     인수 data 는 리시버에 유지된다.


아카이브된 객체 그래프를 복원하려면 하나의 객체를 복원하는 경우와 마찬가지로 메서드 decodeObjectForKey: 를 이용한다.



-

+(BOOL) archiveRootObject:(id)rootObject toFile:(NSString*)path

     NSKeyedArchiver 클래스 메서드로, 루트 객체를 지정해서 아카이브한 결과를 패스가 나타내는 파일에 출력한다.

     처리에 성공하면 YES 를 돌려준다.


+(id) unarchiveObjectWithFile:(NSString*)path

     NSKeyedUnarchiver 클래스 메서드로, 아카이브된 데이터를 패스가 가리키는 파일에서 읽어서 언아카이브하여 루트 객체를 돌려준다.

     처리에 실패하면 nil 을 돌려준다.






13.3. 프로퍼티 리스트


* 13.3.1. 프로퍼티 리스트 개요


-

프로퍼티 리스트(property list)는 Cocoa 환경에서 다양한 정보의 표현 및 저장에 사용하는 표준 데이터 형식이다.

배열이나 사전 등으로 된 객체 구조를 문자열이나 바이트 배열로 표현해서 파일에 저장하거나 거기에서 정보를 객체 형태로 추출할 수 있다.



-

아카이브의 주요 목적은 프로그램 내부의 객체 저장과 복원이다.

따라서 구체적인 클래스와 관련이 적은 정보를 저장하는 데는 적합하지 않다.

프로퍼티 리스트는 프로그램 내부와 관계가 적은 추상도가 높은 정보를 저장, 공유하는 데 적합하다.



-

프로퍼티 리스트는 아스키 코드 형식, XML 형식, 이진 형식의 세 가지가 있다.



-

아스키 코드 형식의 프로퍼티 리스트는 문자열, 데이터, 배열, 사전(NSString, NSData, NSArray, NSDictionary) 이렇게 클래스 네 개를 조합한 구조를 텍스트 형식으로 표현한다.



-

XML 형식의 프로퍼티 리스트에서는 문자열, 데이터, 배열, 사전은 물론, 숫자, 참/거짓 값을 나타내는 NSNumber, 날짜를 나타내는 NSDate 를 포함하고 있는 구조를 표현할 수 있다.



-

이진형식은 이런 구조를 텍스트가 아니라 이진 파일로 저장한다.



-

문자열이나 숫자 같은 기본 데이터는 NSString 과 NSNumber 로 표현하며 이진 데이터 표현에는 NSData 를 이용한다.

NSArray 와 NSDictionary 는 객체의 구조를 작성하기 위해 사용한다.

NSArray 와 NSDictionary 는 중첩될 수 있다.

또한 이런 클래스는 각자 변경 가능 객체를 나타내는 서브 클래스라도 상관없다.

프로퍼티 리스트 전체를 NSDictionary 인스턴스에 저장할 때 이 사전 객체를 루트 사전이라고 부른다.

그리고 사전의 키는 문자열이어야 한다.



-

프로퍼티 리스트를 파일로 저장할 때 파일 확장자로 “.plist” 를 사용하는 습관이 있는데, 이 확장자인 파일은 Xcode 로 편집할 수 있다.



-

프로퍼티 리스트를 각 앱 환경 설정 같은 값을 저장하는 데 사용하는 것이 사용자 기본값의 구조이다.




* 13.3.2. 아스키 코드 형식의 프로퍼티 리스트


-

주로 호환성을 위해 남겨둔 것으로, 파일에서 읽을 수는 있지만 출력을 할 수 없다.

또한 아스키 코드 문자만 사용할 수 있으며 NSNumber 나 NSDate 를 사용할 수 없는 단점이 있어서 실용성이 없다.



-

아스키 코드 형식의 프로퍼티 리스트는 프로퍼티 리스트 구조를 만드는 NSArrray 나 NSDictionary 인스턴스에 대해 메서드 description 을 적용해서 문자열 형식을 취득한다.


반대로 아스키 코드 형식의 프로퍼티 리스트가 내용인 문자열에서 대응하는 객체 구조를 복원하려면 그 문자열 객체에 메서드 propertyList 를 보낸다.


-(id) propertyList 

     아스키 코드 형식 프로퍼티 리스트의 문자열에서 대응하는 객체 구조를 복원해서 돌려준다.

     반환된 구조는 변경 불가능한 객체이다.



-

프로퍼티 리스트 안에 있는 문자열은 기본적으로 “” 로 둘러싸서 표현하는데, 영수문자로 된 문자열이면 인용부호는 생략해도 된다.

이진 데이터는 < > 사이에 16진수를 적어 넣는다.

배열은 ( ) 로 둘러싸고 요소 사이에 콤마를 찍는다.

사전은 { } 로 둘러싸고 키는 보통 문자열로 값과의 사이를 = 로 연결한다.

키와 값의 쌍, 즉 항목의 끝은 세미콜론으로 표시한다.




* 13.3.3. XML 형식의 프로퍼티 리스트


-

NSArray 나 NSDictionary 인스턴스에 메서드 writeToFile:atomically: 를 실행하면 XML 형식 프로퍼티 리스트를 파일로 출력한다.

반대로 파일에서 프로퍼티 리스트를 읽어서 객체를 복원하려면 메서드 initWithContentsOfFile: 을 사용한다.

하지만 그렇게 얻은 객체 구조는 변경 불가능 객체로 구성된다.




* 13.3.4. 프로퍼티 리스트의 변환과 확인


-

프로퍼티 리스트의 표현을 상호 변환하거나 형식을 확인하는 클래스로 NSPropertyListSerialization 이 있다.



-

메서드에 설정하는 NSPropertyListFormat 형의 값과 의미는 다음과 같다.

     NSPropertyListOpenStepFormat : 아스키 코드 형식

     NSPropertyListXMLFormat_v1_0 : XML 형식

     NSPropertyListBinaryFormat_v1_0 : 이진 형식



-

+(NSData*)dataWithPropertyList:(id)plist format:(NSPropertyListFormat)format options:(NSPropertyListWriteOptions)opt error:(NSError**)error

     인수 plist 는 프로퍼티 리스트를 나타내는 객체(사전, 배열 등)로, 이것을 format 으로 지정한 형식(XML 이나 이진형식)의 프로퍼티 리스트로 변환해서 데이터 객체로 돌려준다.

     인수 opt 에는 0을 지정한다. (내부 구현에서 사용하지 않기 때문)


+(BOOL)propertyList:(id)plist isValidForFormat:(NSPropertyListFormat)format

     인수 plist 프로퍼티 리스트가 format 으로 지정한 형식에 적합한지 확인한다.


+(id)propertyListWithData:(NSData*)data options:(NSPropertyListReadOPtions)opt format:(NSPropertyListFormat*)format error:(NSError**)error

     세종류의 형식 중 하나인 프로퍼티 리스트가 저장된 데이터 객체에서 객체 구조를 복원한다.

     인수 opt 에는 0 을 지정한다.

     인수 format 은 변수 포인터로 인수 data 프로퍼티 리스트가 어떤 형식인지 돌려준다.

     format 에 NULL 을 넘기면 결과가 나오지 않는다.





반응형

댓글