레거시 코드를 공부하면서 옵젝씨 공부 내용 좀 정리하려고 했는데..
어쩌다보니 첫 글이 다 건너뛰고 복사로 넘어와버렸다...
요즘에는 뭐 대부분 Swift로 코드를 짜기 때문에 옵젝씨는 공부할 필요가 없지 않을까 싶었는데
뭐.... 회사에서 레거시가 옵젝씨로 쓰여져있다면 어쩔 수가 없다... 공부해야지...
얕은 복사와 깊은 복사
이는 Swift를 공부해도 많이 접하는 개념이다.
내가 가장 먼저 접했던건 Class와 Struct의 객체 할당 시 복사 방식의 차이를 공부할 때 알게 됐다.
Class는 참조 타입이다. 그래서 Class 타입의 인스턴스를 다른 변수에 할당하게 되면 참조 값이 복사된다. 값 자체가 복사되는 것이 아니기 때문에 원본 값의 내부 정보를 변경하게 되면 동일한 참조 주소가 복사된 변수의 내부 정보도 변경된다.
반면 Struct는 값 타입이다. 인스턴스를 다른 변수에 할당하게 되면 값 자체에 복사가 일어나서 할당받은 변수는 독자적인 값을 같게 된다. 그래서 원본 변수의 내부 정보를 변경해도 복사된 변수의 내부 정보는 변하지 않는다.
이렇게 Class와 Struct의 차이를 이용해서 얕은 복사가 필요할 땐 Class로 객체를 생성하고 깊은 복사가 필요할 땐 Struct를 이용해 객체를 생성해왔다.
하지만 문제는 옵젝씨에서다...
옵젝씨에는 우리에게 친숙한 구조체가 없다.
옵젝씨에서 사용하는 구조체는 Swift의 구조체보다는 C언어에서의 구조체라고 봐야한다.
둘의 차이는 AI 선생에게 정리해달라 부탁했다.

대충 이렇다고 한다~~
아무튼 그러면 옵젝씨에서 객체 타입을 생성하기 위해선 class를 사용해야한다는건데....
그럼 깊은 복사가 필요할 땐 어떡하지..?
먼저 옵젝씨의 프로퍼티 옵션을 보자!
(바로 건너뛰어서 복사로 와버린 업보..)
@property는 인터페이스에서 변수를 선언할 때 사용한다.
이 방법으로 변수를 선언하게 되면 인스턴스 변수, 세터, 게터가 자동으로 생성된다.
@property는 생성할 때 옵션을 설정할 수 있는데 다음 코드들을 보자
//Objc
@property (nonatomic, retain) NSString *name;
@property (nonatomic, assign) NSString *name;
@property (nonatomic, copy) NSString *name;
MRC환경에서 주로 쓰이는 프로퍼티 속성들이다.
retain은 값을 세터로 할당할 때 retain count가 1이 증가하고 assing의 경우 retain count가 증가하지 않는다.
ARC로 따지면 strong과 weak라고 볼 수 있다. (assign의 경우 자동으로 nil 처리가 안된다는 점에서 unowned와 더 유사하긴 하다)
물론 옵젝씨도 ARC환경이라면 retain 대신 strong, assign 대신에 weak를 쓴다.
그렇다면 copy는 무엇인가? copy로 설정하는 경우 세터로 할당할 때 할당하는 타입의 copy 메서드 반환값을 속성에 할당하게 된다.
- (void)setValue: (NSString *)newValue
{
if (newValue != value)
[value release];
value = [newValue copy];
}
세터의 내부 동작은 다음과 같을 것이다.
그래서 NSString의 경우 copy 속성으로 설정해주면 복사값이 생성되어 원본 값과 독립된 복사본을 가질 수 있다.

아하! 그럼 기본 클래스 타입 속성들도 저렇게 하면 바로 복사가 되겠구나!!

?...?
응 아니야~ 바로 런타임 에러 발생~
내가 만든 DeepCopyClass라는 클래스 타입을 copy 속성으로 선언하고 런타임에 해당 변수에 값을 할당하려하니 바로 런타임 에러가 발생했다.
왜일까? 그건 바로 내 커스텀 클래스에는 copy 메서드가 구현이 되어있지 않기 때문이다.
그렇다면 어떻게 내 커스텀 클래스를 복사할 수 있을까?
필요 메서드가 구현이 되어 있지 않아..? 설마 프로토콜?
그렇다 바로 NSCopying 프로토콜을 채택해서 copyWithZone 메서드를 구현해줘야한다.
우선 DeepCopyClass의 인터페이스를 보자

아주 간단하게 NSString과 NSMutableArray를 가지고 있는 클래스이다. (배열이 왜 retain 속성인지는 이따가 설명하겠다)
여기에 이제 NSCopying 프로토콜을 채택한다.

그 다음 이제 구현부로 가자!!
구현부에서는 필요한 메서드를 구현해주면 된다.
(프로토콜 채택 후 경고 메시지 뜨면 fix 버튼을 눌러 메서드 생성하는건 국룰)

이렇게 copyWithZone을 구현해주면 이제 아까 떴던 런타임 에러는 사라졌다!
NSCopying이 채택되어 있는 클래스에서 copy를 호출하게 되면 copyWithZone:nil 이 호출되게 된다.
(Zone은 메모리 영역을 의미한다)
이렇게 복사는 끝...?일줄 알았으나 아직 끝이 아니다.
위에서 봤듯이 array는 copy 속성이 아닌 retain 속성이다.
NSMutableArray의 경우 copy를 통해 값이 복사되어 들어갈 경우 NSArray로 바뀐다고 한다.
나는 불변성을 원하지 않는데 멋대로 불변성이 생겨버리는 것이다.
그래서 우선 copy 대신 retain으로 설정한다.
그리고 이제 새로운 배열을 copy되는 변수에 집어 넣는다, 다음과 같이

이렇게 하면 새로운 배열을 생성해서 복사본에 넣기 때문에 배열을 복사해서 넣을 수 있다!
하지만 또또또 끝이 아니다
나는 단순하게 클래스의 복사를 구현하기 위해 이 글을 시작한게 아니다
깊 은 복 사
바로 깊은 복사를 구현해야 한다.
배열을 저렇게 복사하면 원본값과 복사본의 각각 요소들은 같은 참조로 묶여 있기 때문에 원본을 수정하면 copy의 값도 바뀐다. 즉 아직은 얕은 복사라는 거다. 테스트 코드르 보자


이렇듯 둘 다 같은 참조를 바라보고 있기 때문에 수정사항 또한 공유하게 된다.
그렇다면 깊은 복사를 하려면 어떻게 할까?

array에 새로운 값을 넣어줄 때 copyItems 옵션을 YES로 추가해주면 된다!!
이렇게 되면

이제 복사본은 원본 수정에 영향을 받지 않는 모습을 볼 수 있다!
이번에 실습한 것은 간단한 구조의 클래스 객체였지만 객체가 복잡해질 수록 깊은 복사를 구현하기 위해 처리해야하는 것은 많아진다.
하지만 클래스에서 복사 방식을 내가 커스텀해볼 수 있다는 걸 배웠다는 점에서 아주 뿌듯하다.
그리고 NSCopying은 Swift에서도 채택할 수 있다. 그래서 내가 사용하는 클래스 객체에 깊은 복사를 구현해주고 싶다면 NSCopying 프로토콜을 채택해서 내가 직접 복사 코드를 구현해보면 좋을 것 같다!(물론 난 그냥 구조체 쓸거다)
얼른 옵젝씨 공부도 끝낼 수 있는 날이 오길...