객체 복사

2장 메모리 관리에서 객체 인스턴스를 메모리에 만들고, 객체를 포인터 변수로 참조하면서 소유권을 갖거나, 소유권 없이 참조하는 방식에 대해 설명했다.
3장 자동 메모리 관리에서는 ARC 방식을 기준으로 자동 참조 계산과 참조 방식에 따라 다른 구현 방식에 대해 설명했다.

이번에는 이미 만들어진 객체 인스턴스를 참조하는 경우와 달리, 객체 인스턴스 데이터를 새로운 객체 인스턴스로 복사해야하는 경우 필요한 프로토콜을 살펴본다.
그리고 얕은 복사가 아닌 깊은 복사를 위한 아카이브 방식에 대해 알아보자

NSCopying 계열 프로토콜

코코아 프레임워크에서는 객체를 복사하기 위한 방법으로 NSCopying / NSMutableCopying 프로토콜을 지정해서 구현하는 방법을 권장한다.
NSCopying 프로토콜은 객체를 복사하기 위해 클래스에 미리 구현해야 하는 복사용 메서드 목록을 지정해놓은 프로토콜이다.
애플이 만든 코코아 객체들은 이미 NSCopying 프로토콜을 기반으로 만들어져있어서 객체를 복사하기 쉽다.

복사만 가능한 객체

NSCopying 프로토콜은 구현해야 할 메서드가 딱 하나이다.
내가 만든 객체가 복사가능한 객체여야 한다면, NSCopying 프로토콜 내에 있는 해당 메서드를 구현하면 된다.

-(id)copyWithZone: (NSZone*)zone;

앞서 2장에서 설명했듯 현재는 메모리 영역을 zone으로 나누지 않기때문에, 인자는 nil을 넘겨도 된다.
-> 현재 모든 앱은 단일 존(기본 존)을 갖기 때문

final class AObject: NSObject, NSCopying {
    var num1: Int
    let num2: Int

    init(_ num1: Int, _ num2: Int) {
        self.num1 = num1
        self.num2 = num2
        super.init()
    }

    func copy(with zone: NSZone? = nil) -> Any {
        let newObj: CopyTest = .init(num1, num2)
        return newObj
    }
}

final class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        let obj1: CopyTest = .init(0, 1)
        let obj2 = obj1.copy()
        //obj1 과 obj2 는 같은 num1과 num2를 갖고, 서로 다른 주소값을 갖는다 -> 등가성은 갖지만, 동일성은 갖지 않는다.
        obj1.num1 = 100
        //obj1 과 obj2 는 같은 num2를 갖고, 서로 다른 num1 값과 주소값을 갖는다
        print(obj1.num1)
        print((obj2 as? CopyTest)?.num1 ?? 0)
    }
}

위의 코드는 obj1과 obj2가 복사하여도 서로 다른 주소값을 갖는 객체를 가르키고 있기때문에, 얕은 복사이다.

복사와 수정이 가능한 객체

  • 코코아 프레임워크에서 -copy 메서드로 복사하는 객체는 불변(immutable) 객체라고 가정한다.
    • NSString 객체 인스턴스를 복사하면 내부 문자열 데이터를 복사해서 새로운 NSString 객체 인스턴스를 만들지만, 이 문자열 객체 내용은 못 바꿈
  • 만약 수정가능한 가변(mutable) 객체로 복사하려면 -mutableCopy 메서드로 복사해야 한다.
    • 해당 메서드는 NSMutableCopying 프로토콜 내에 존재한다.
  • 객체를 복사하는 과정에서 복사할 원래 객체가 가변인지 불변인지는 상관없다.
    • 새로 만들 객체가 가변인지 불변인지만 중요함

요약

애플 프레임워크에 포함된 클래스는 대부분 NSCopying과 NSMutableCopying 프로토콜을 구현하고 있다.
직접 개발한 클래스도 복사 가능한 객체라면, 애플이 만든 클래스와 마찬가지로 직접 구현을 하면 된다.


얕은 복사(shallow copy) vs 깊은 복사(deep copy)

NSArray처럼 내부에 다른 객체를 포함하는 경우에는 객체를 복사할 때, 주의해야 한다.
참조 포인터 변수를 복사해서 포인터에 있는 힙공간 주소값을 복사한다고해서, 참조하던 객체와 동일한 복사본이 하나 더 생기진 않는다.
참조하는 객체를 가르키는 포인터 변수만 하나 더 생길 뿐이다.
이처럼 주소값만 복사하는 방식을 '얕은 복사' 라고한다.

얕은 복사

@interface PenHolder : NSObject <NSCopying> {
  NSMutableArray *_pens;
}
-(void) addPen:(Pen*)pen;
-(void) removePen:(Pen*)pen;

@end

@implementation PenHolder

- (id)copyWithZone:(NSZone *)zone {
  PenHolder *copiedHolder = [[[self class] alloc] init];
  copiedHolder->_pens = [_pens mutableCopy];
  return copiedHolder;
}

@end

PenHolder 클래스는 Pen 객체를 참조하는 가변 배열 컬렉션 NSMutableArray 객체를 포함한다.
PenHolder 객체를 복사하는 -copyWithZone: 메서드는 새로운 PenHolder 객체 인스턴스 copiedHolder를 만들고, 자신의 _pens 배열을 복사해서 copiedHolder 객체 _pens 변수에 설정한다.

해당 복사는 새로운 객체를 만드는 것이 아닌, 기존의 _pens 배열을 참조하는 copiedHolder를 참조하는 포인터가 생기는 것이다.

하지만 Foundation 프레임워크 내에 있는 모든 클래스는 얕은 복사로 구현되어 있다.

이 상태에서 copiedHolder 인스턴스 내 _pens 집합에 있는 Pen 객체를 수정하면, 기존의 penHolder 인스턴스 내 _pens 집합의 Pen 객체로 같이 변경된다.

깊은 복사

앞서 말한 것처럼 NSArray 계열 컬렉션 클래스의 경우 -initWithArray: CopyItems: 초기화 메서드를 활용해서 생성할 때만 깊은 복사가 가능하다.

만약 객체에서 객체를 포함하고, 또 그 객체에서 다른 객체를 포함한다면 -> DFS 방식으로 하위 노드들부터 탐색해서 새로운 객체를 만들어서 복사하고, 이어서 다른 노드를 탐색하다보면 모든 객체를 복사할 수 있다.

하지만 DFS 방식으로 깊은 복사를 하더라도 객체 참조 관계가 기존 객체와 항상 완벽하게 동일하다고는 할 수 없다.
-> 탐색을 하다 보면 어떤 객체는 중간에 여러 객체에서 여러번 참조될 수도 있고, 특정 객체들은 순환 참조 문제가 있을 수도 있다.
-> 약한 참조를 갖고 있으면 해당 객체를 복사하고 약한 참조로 지정해주어야 한다.

따라서 복잡한 참조 관계를 가진 객체를 복사하는 경우에는 객체 참조 그래프를 활용하는 것이 좋다

요약

코코아 프레임워크의 클래스는 객체를 복사할 경우, 얕은 복사 형태로 참조 관계를 유지한다.
특히 NSArray, NSSet 같은 컬렉션 객체나 다른 객체를 참조하는 객체를 복사하는 경우에는 깊은 복사를 고민해봐야 한다.

+ Recent posts