Swift

Deep copy & Shallow copy

GREEN.1229 2021. 5. 25. 17:47

안녕하세요. 그린입니다🟢

이번 포스팅에서는 Deep copy(깊은 복사)와 Shallow copy(얕은 복사)에 대해 학습해보겠습니다🧑🏻‍💻

본격적으로 알아보기전!

기본적으로 모든 데이터 타입은 값 혹은 참조 타입을 가지는건 알고 계시죠?

간단히 두 타입에 대해 설명해보자면,

값 타입으로 생성된 데이터는 각각 해당하는 메모리를 따로 소유합니다.

참조 타입으로 생성된 인스턴스는 주소를 공유하여 같은 주소를 참조한 값이 바뀌면 같이 변경됩니다.

이 두 차이를 통해 깊은 복사와 얕은 복사에 대해 어떨때 일어나고 어떻게 사용할지 알아봅시다👍🏻

Deep copy

 : 깊은 복사로 데이터를 그대로 복사하여 복사된 두 데이터 객체는 각각의 메모리를 가집니다.

 : 주로 기본적으로 값 타입의 객체들을 생성하면 깊은 복사로 이뤄집니다.

 

struct Person {
    var name: String
}

var green = Person(name: "green")
var red = green
red.name = "red"

print(green)
print(red)

 

위와 같이 구조체로 깊은 복사가 일어나게되고 다른 객체에 할당하고 값을 변경하게 된다면 기존 객체의 값은 변경되지 않습니다.

왜냐면 스택에 다른 객체가 쌓여 주소값이 다르기 때문을 확인할 수 있습니다.

깊은 복사는 인스턴스가 독립적인게 증명됩니다.

Shallow copy

 : 얕은 복사로 최소한의 복사를 가진다.

 : 데이터를 복사해도 인스턴스 메모리가 새로 생성되는것이 아닌 값의 주소를 복사하여 같은 값을 가르킨다.

 : 주로 기본적으로 참조 타입의 객체들을 생성하면 얕은 복사로 이뤄집니다.

 

class Person {
    var name: String
    
    init(_ name: String) {
           self.name = name
    }
}

var green = Person("green")
var red = green
red.name = "red"

print(green)
print(red)

 

위와 같이 같은 힙에 값을 가지고 스택에는 힙 데이터 실제 값을 가르키는 같은 주소값을 가집니다.

동일하게 네임에 대한 값도 바뀝니다.

같은 메모리 영역을 가르키고 차지하는것이 얕은 복사의 특징입니다.

 

그럼 참조타입에서 깊은 복사를 할 경우도 있을텐데 어떻게 해주면 될까?

이 부분에 대해서는 NSCopying 프로토콜을 채택하여 사용하면 됩니다.

NSCopying

 : copy() 메서드를 사용하여 참조 타입에서도 깊은 복사를 가능하게 해주는 프로토콜

 

class Person: NSCopying {
    var name: String
    var age: Int

    init(_ name: String, _ age: Int) {
        self.name = name
        self.age = age
    }

    func copy(with zone: NSZone? = nil) -> Any {
        return Person(self.name, self.age)
    }
}

var green = Person("green", 10)
var red = green.copy() as! Person
red.name = "red"

print(green.name)
print(red.name)

 

위와 같이 copy() 메서드를 통해 red 인스턴스에 할당해주게 되면 깊은 복사가 일어나 다른 메모리 영역을 가지는걸 확인할 수 있습니다.

참조 타입 내부에 참조 타입을 가진다면 해당 타입에 대해 이뤄진 데이터는 얕은 복사를 하게 됩니다.

class Age {
    var age: Int
    
    init(_ age: Int) {
        self.age = age
    }
}

class Person: NSCopying {
    var name: String
    var age: Age

    init(_ name: String, _ age: Age) {
        self.name = name
        self.age = age
    }

    func copy(with zone: NSZone? = nil) -> Any {
        return Person(self.name, self.age)
    }
}

var age = Age(10)
var green = Person("green", age)
var red = green.copy() as! Person

print(green.age.age)
print(red.age.age)

 

이럴경우에는 참조해온 Age 클래스에서의 copy() 메서드에 해당 타입에 대해 깊은 복사를 지정해줘야됩니다.

 

class Age: NSCopying {
    var age: Int
    
    init(_ age: Int) {
        self.age = age
    }
    
    func copy(with zone: NSZone? = nil) -> Any {
        return Age(self.age)
    }
}

class Person: NSCopying {
    var name: String
    var age: Age

    init(_ name: String, _ age: Age) {
        self.name = name
        self.age = age
    }

    func copy(with zone: NSZone? = nil) -> Any {
        return Person(self.name, self.age.copy() as! Age)
    }
}

var age = Age(10)
var green = Person("green", age)
var red = green.copy() as! Person

print(green.age.age)
print(red.age.age)

 

구조체 타입 내부에 참조 타입을 가질경우?

 : 해당 경우에는 NSCopying 프로토콜은 NSObject 하위 클래스에서만 동작하기에 값 타입의 구조체에서는 상속이 되지 않아 copy() 메서드를 사용할 수 없습니다.

 : 그래서 구조체에서 구현된건 깊은 복사가 이뤄지지만 참조해온 참조 타입은 얕은 복사가 이뤄지게 됩니다.

 : 즉 구조체 내부에 참조 타입을 가질경우는 깊은 복사와 얕은 복사가 디폴트이며 참조 타입을 깊은 복사를 일으키고 싶다면 위와 같이 해당 참조 타입에서 copy()메서드로 구현해줘야 합니다.

이를 토대로 하나 또 알아낸 사실

 : 구조체는 값이 복사되어 변경이 일어날때만 Copy on Write 방식으로 값 복사가 일어난다고 알고 있었습니다.

이에 얕은 복사와 깊은 복사를 학습하니 원리를 알 수 있었습니다. 항상 구조체는 깊은 복사가 일어나기에 매번 메모리의 값을 복사한다면 성능상 안좋을것입니다. 이에 처음 복사시에는 메모리의 주소만 복사가 되는 얕은 복사가 일어나게되고 값의 변화가 생긴다면 이후에 깊은 복사가 일어나도록 해당 기능의 개념이 생겼습니다.

 

 

[참고자료]

https://developer.apple.com/documentation/foundation/nscopying/

https://developer.apple.com/documentation/foundation/nscopying/1410311-copy

https://velog.io/@ellyheetov/Shallow-Copy-VS-Deep-Copy