깊은 복사 VS 얕은 복사

ellyheetov·2021년 3월 13일
20
post-thumbnail

Value and Reference type

모든 데이터 타입은 값 타입(value type) 또는 참조 타입(reference type)을 가진다.

  • 값 타입(Value type) : 각각의 고유의 메모리를 소유한다. 스위프트에서 struct, enum, array, tuples 들이 해당 타입에 속한다.
  • 참조 타입(Reference type) : 생성된 인스턴스들은 주소값을 공유한다. 스위프트에서 class가 해당 타입에 속한다.

두 가지 타입 모두 copy 메소드가 존재하는데 과연 깊은 복사(deep copy)가 일어날까? 아니면 얕은 복사(shallow copy)가 일어날까?

깊은 복사(Deep copy)란?

  • 데이터 자체를 통째로 복사한다.
  • 복사된 두 객체는 완전히 독립적인 메모리를 차지한다.
  • value type의 객체들은 깊은 복사를 하게 된다.

아래의 예시에서 arr1은 string 배열을 가진다. arr2에 arr1을 할당 해보자.

이때, arr1에 있는 모든 string들은 깊은 복사가 일어나서 새로운 배열을 생성하여 arr2에 할당한다. (String은 value type이므로 깊은 복사가 일어난다.) 때문에 arr1을 변경하여도 arr2에서는 값이 변경되지 않는다.

아래 예시에서는 string1에 string2를 할당한다. 이 후, string1의 값을 변경하면 string2에 값도 변할까?

아니다. String은 깊은 복사가 일어나므로 string1을 변경하여도 string2는 변경되지 않는다.

이렇게 깊은 복사는 인스턴스가 완전히 독립적이다. 이와 같은 개념은 모든 value type에 적용된다.

그런데 만약 value type이 reference type을 포함하고 있는 경우는 어떻게 될까???🤔 이 질문은 잠시 묶혀 두고, 아래에서 살펴보도록 하자.

얕은 복사(Shallow copy)란?

얕은 복사는 아주 최소한만 복사를 한다. 값을 복사한다 하더라도, 인스턴스가 메모리에 새로 생성되지 않는다. 값 자체를 복사하는 것이 아니라 주소값을 복사하여 같은 메모리를 가리키기 때문이다.

새로운 인스턴스를 생성하지 않기 때문에 깊은 복사보다 상대적으로 빠르다. reference type을 복사하는 경우 얕은 복사가 일어난다.

아래 예시를 보자.

class Address {
    var address : String
    
    init(_ string : String){
        self.address = string
    }
}

a1의 인스턴스를 a2에 할당한다. 이때 앝은 복사가 일어나기 때문에 같은 주소값을 가지게 된다.

만약 a1의 address에 값을 바꾼다면 a2의 address도 같이 바뀌게 된다. 같은 메모리를 차지하고 있기 때문이다.

Reference type의 깊은 복사

reference type의 객체를 복사하면 새로운 객체를 생성하지 않고 항상 주소값이 복사가 된다. 그렇다면 완전히 분리된 객체를 생성할 수는 없을까???

copy() 메소드를 사용하면 참조 타입이더라도 깊은 복사가 이뤄진다.
copy() 메소드는 객체를 반환한다. NSCopying 프로토콜을 채택한 객체는 copy() 메소드를 사용할 수 있다.

위에서 살펴본 Address 객체에 NScopying 프로토콜을 채택하여 깊은 복사를 수행해보자.

class Address : NSCopying {
    
    var address : String
    
    init(_ string : String){
        self.address = string
    }
    func copy(with zone: NSZone? = nil) -> Any {
        return Address(self.address)
    }
}


a1을 복사한 a2객체는 a1 과 독립적인 메모리 주소를 가진다. 새로운 인스턴스를 생성하여 할당하였기 때문이다.

reference type 내부에 reference type을 가지고 있는 경우

예시로 먼저 확인하자.

Person클래스는 내부에 Address 변수를 가지고 있다.

class Person : NSCopying {
   var name : String
   var city : Address
   
   init(name : String, city: Address){
       self.name = name
       self.city = city
   }
   func copy(with zone: NSZone? = nil) -> Any {
       return Person(name: self.name, city: self.city)
   }
}

person1 객체를 생성하고 copy() 한 값을 person2에 할당 해보자.

var a1 = Address("Seoul")
var person1 = Person(name: "elly", city: a1)
var person2 = person1.copy() as! Person

copy()는 깊은 복사를 하기 때문에 새로운 Person 인스턴스를 생성하여 person2에 할당한다. 그렇다면 내부에 Address는 깊은 복사를 할까? 아니면 얕은 복사를 할까?


신기하게도, Person 내부에 Address에 대해서는 얕은 복사가 이뤄졌다. 만약 person1의 address의 값을 바꾼다면 person2의 address 값도 바뀌게 된다.

reference type을 copy하는 경우 기본적으로 명시하지 않으면 얕은 복사가 일어난다. Person객체에 대해서 명시적으로 깊은 복사를 하도록 해주었지만, 내부에 Address에 대해서는 복사 방식을 명시하지 않았기 때문에 얕은 복사가 이뤄진 것이다.

내부 reference type의 변수에도 깊은 복사를 지정해 주어야 원하는대로 깊은 복사를 수행할 수 있다.

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

Value type 내부에 Reference type을 가지고 있는 경우

아까 궁금했던 질문에 대해 생각해보자.

value type을 안에 reference type을 가지고 있는 경우는 어떨까?

객체의 array, struct나 tuple 안에 reference type을 가지고 있는 경우가 이에 속한다. value type에 경우 copy()를 사용할 수 없다. copy() 메소드는 NSCopying 프로토콜을 채택하여 구현해야 하는데, NSCopyingNSObject의 서브 클래스에서만 동작한다. value type은 상속을 지원하지 않으며, 때문에 copy()를 사용할 수 없다.

array의 구조는 깊은 복사가 이뤄지지만, 여전히 address는 reference type이므로 얕은 복사가 이뤄진다.

arr1과 arr2는 같은 메모리를 가리키고 있다. 앞서 말했 듯이 reference type은 기본적으로(default) 얕은 복사가 이뤄지기 때문이다.

직렬화(Serializing)와 역직렬화(De-Serializing)

직렬화란?

직렬화(直列化) 또는 시리얼라이제이션(serialization)은 컴퓨터 과학의 데이터 스토리지 문맥에서 데이터 구조나 오브젝트 상태를 동일하거나 다른 컴퓨터 환경에 저장(이를테면 파일이나 메모리 버퍼에서, 또는 네트워크 연결 링크 간 전송)하고 나중에 재구성할 수 있는 포맷으로 변환하는 과정이다.

역직렬화란?

직렬화와 반대로, 일련의 바이트로부터 데이터 구조를 추출하는 일은 역직렬화 또는 디시리얼라이제이션(deserialization)이라고 한다.

간단히 말하자면, 직렬화와 역직렬화는 데이터를 저장하기 위해서 포맷을 변경하는 과정이다.

갑자기 직렬화/역직렬화가 깊은복사/얕은복사랑 무슨 상관이냐고??🤷🏻‍♀️

직렬화/역직렬화 과정에서 객체는 항상 새로운 객체를 생성한다. value type이든 reference type이든 모두 동일하게 깊은 복사를 수행한다.

swift에서 직렬화/역직렬화 과정은 아래 두 개의 API를 사용한다.

  • NSCoding : Archiving과 distribution을 위해 인코딩/디코딩을 가능하게 해주는 프로토콜이다. NSObject로 부터 상속을 받기 때문에 클레스에서만 사용할 수 있다.
  • Codable : 외부에서 표현하는 JSON과 같은 데이터 형식으로 인코딩/디코딩을 가능하게 해주는 프로토콜이다. value type이나 reference type에서 모두 가능하다. 하지만 상속을 통한 객체의 다형성이 동작하지 않는 한계를 가지고 있다.

Copy on write

copy on write는 value type 복사의 성능을 최적화 하기 위한 기술이다.

만약 하나의 String이나 Int를 복사한다고 했을때, 중요한 성능의 이슈가 없다. 하지만, 배열의 요소가 100만개 정도 되는 array의 경우는 어떨까? 단순히 복사만 하고 값을 변경하지 않는 경우에는 굳이 모든 원소들을 깊은 복사 할 필요가 없다. 이것은 메모리를 낭비할 뿐이다.

이를 해결하기 위한 방법이 Copy on write이다. 처음 복사를 했을 시, 단지 메모리의 주소를 복사하기만 한다. 즉, 얕은 복사가 이뤄지는 것이다. 이후 변화가 발생하면 뒤늦게 깊은 복사가 일어난다. 즉, 깊은 복사든 얕은 복사든 새로운 복사 객체가 생성되는 시점은 값이 변하는 시점이다.

과정 예시

  • arr1에 arr2를 할당한다.
  • arr1과 arr2는 같은 주소값을 가진다.
  • arr2에 변화가 생긴다.
  • arr1과 arr2는 그제서야 다른 메모리 주소를 가지게 된다.

참고

https://www.freecodecamp.org/news/deep-copy-vs-shallow-copy-and-how-you-can-use-them-in-swift-c623833f5ad3/

profile
 iOS Developer 좋아하는 것만 해도 부족한 시간

1개의 댓글

comment-user-thumbnail
2023년 4월 13일

감사합니다.

답글 달기