본문 바로가기
Swift

깊은 복사 & 얕은 복사 (Swift)

by SeoB-P 2022. 5. 8.

값타입(ValueType) & 참조타입(ReferenceType)

Swift에서는 모든 데이터 타입이 'ValueType'이거나 'ReferenceType'으로 이루어져있다.

대표적인 ValueType으로는 Struct,Enum 등등 이 있고 대표적인 ReferenceType으로는 Class를 들 수 있을 것이다.

이 두 Type은 다른 차이점들이 많지만, 가장 큰 차이점은 'Copy를 하는 방식'이다.

 

얕은 복사 (Shallow Copy)

얕은 복사는 '주소값을 복사'하는 복사 방식이다.

따라서, 값을 복사할때 새로운 메모리 공간을 할애하여 복사하는 것이 아니라 주소값을 복사한다.

우리가 주로 Class를 새로 선언하고 다른 변수에 복사를 할때 일어나는 일과 같다.

따라서, A클래스의 Property(love)의 값을 바꾸면 b의 값도 같이 바뀐다.

 

Class

깊은 복사 (Deep Copy)

  • Deep copy를 이용하면 원본이 가르키는 모든 Object가 복사가 되고 그 복사본을 가르키게 되며,  완전히 분리된 두개의 Object가 됨.
  • Deep copy가 이루어진 collection들(array,Dictionary..)의 모든 원소는 원본을 중복해서 가지고 있다.
  • 멀티 쓰레드 환경에서 안전하다. - object의 변경사항이 다른 object에 영향을 끼치지 않는다.
  • Value Type은 Deep copy를 한다.

Struct

얕은 복사와는 'b'만 값이 바뀌었음을 확인 할 수있다.

참조타입의 Deep Copy

참조타입을 Deep하게 Copy할 순 없을까?

`CopyMethod` 를 사용하면 가능하다!

이 메서드는 NSCopying프로토콜의 메서드이다.

이 프로토콜을 채택해야 Object들은 functional copy들을 제공 할 수 있다.

NSObject는 NSCopying protocol을 자체적으로 지원하고 있지 않기 때문에, subClass들은 반드시 이 protocol을 지원하고 copy(with:)메서드를 실행해야 한다.

 

functional copy? 기능적 복사?

문서에 따르면 Copy의 의미는 Class마다 다를 수 있지만 Copy는 무조건 Copy가 만들어질 시기에 

원본과 똑같은 Value를 가지고 기능적으로 독립적이어야 한다. 고 되어있다.

원본의 값을 가지고 독립적으로  기능을 사용 할 수 있는 copy를 제공 하는 것으로 이해했다.

따라서, 독립적으로 값을 바꿀 수 있기 때문에 이는 원본과는 상관이 없는 작업이 되는 것이고 이를 위해선 새로운 메모리를 할당 해야하므로 Deep Copy가 된다!

 

ReferenceTyped에서 Deep Copy를 사용하기 위해 위의 예제에서 Class에 NSCopying을 채택 & 구현해 보았다.

이번에는 Class타입을 복사했음에도 두개의 Property가 다르게 찍힘을 확인 할 수 있다!

게다가, 메모리 주소도 서로 다름을 확인 할 수 있다. (V 옆의 메모리 주소를 잘 확인 해보자!)

NSZone? : 시스템에서 정한 메모리 공간이라고 하는데 이제 사용하지 않는다고  무시된다고 한다.

참조 타입 안의 참조 타입 (Nested ReferenceType)

참조타입안에 Property로 참조 타입이 있을때 Deep Copy를 시도한다면 어떻게 될까?

예를 들어 위 코드에서 Class Property를 추가한다고 해보자.

 

간단하게 Name이라는 Class를 만들고

기존 Class C에 프로퍼티로 추가를 해보았다.

 

이 경우 Name은 Deep Copy가 될까 얕은 Copy가 될까?

 

c 와 d는 분명 101130b0과  1011306e0이라는 다른 메모리 주소를 가지고 있는 것으로 보아 Deep copy가 잘 되었으나, c와 d안에 있는 name은 여전히 같은 주소를 가지고 있음을 확인 할 수 있다.

 

따라서, referenceType을 copy하는 경우 Deep Copy를 원할때에는 반드시 명시적으로 copy를 해주어야 한다.

referenceType의 copy방식의 default값은 얕은 복사이기 때문이다.

 

이번 예제에서 만약 Name까지 DeepCopy를 하고 싶다면 밑과 같은 코드가 필요할 것이다.

이제 name프로퍼티 마저 다른 메모리값으로 찍힘을 확인 할 수 있다.

Reference Type in Value Type 

만약, ValueType안에 referenceType이 있다면 어떻게 될까?

ValueType은 subclass가 되지 않으므로 NSCopy를 상속 받을 수 없다.

 

돌아온 Struct A에 ReferenceType인 Name을 넣고 복사를 한 후 b name의 값을 바꾸어 보았다.

결과는... 잘된다?!

a의 Name과 b의 Name이 각각 다른 것으로 보아 Deep Copy가 잘 된것 처럼 보인다.

하지만..

여기서 b의 name을 바꾸지 않는다면?

name 자체는 얕은 복사를 하여 같은 메모리 주소를 가지고 있음을 알 수 있다.

Copy on Wirte(Cow)

값에 변화가 없으면 복사를 할때 그 값을 참조만 하고 있다가 수정이 일어나면 그때 복사를 한다는 개념이다.

Swift에서는 주로 Collection들(Array, Dictionary..) 에 사용이 되고 있다.

근데 위의 예시에서는 Struct가 아니었나??

그렇다.. Array는 Struct이며 ValueType이다.

ValueType임에도 불구하고 성능상 이점을 위해 수정이 일어나기 전까지는 얕은 복사를 하고 있다가 깊은 복사를 하게 되는 것.

(만약 100만개의 값이 있는 배열을 그저 복사만 하고싶은데 그 것을 다~ 메모리에 복사를 해버리면 큰 손해이므로.)

 

Array안에 ReferenceType이 들어간다면..? 여기를 참고

Serializing & deserializing

우리가 주로 API와 통신하기 위해서 하는 Serializing 과 deserializing을 할때에는 항상 새로운 object를 만든다.

이때에는 ReferenceType이든 ValueType이든 상관 없이 항상 Deep Copy를 지원한다.

 

Swift는 밑 두가지 API를 이용해서 Data를 Serializing 과 deserializing을 한다.

1. NSCoding: Archiving과 distribution을 위해 object를 endcode하고 decode할 수 있게 하는 protocol.

NSObject를 상속받기 때문에 Class에서만 쓸 수 있다.

secure 지원

 

2. Codable: 우리가 만든 Data Type들을 JSON과 같은 형식으로 적절하게 encode하고 decode할 수 있게 해주는 protocol

Value Type이나 ReferenceType 모두 사용이 가능. 

하지만, 상속관계가 유지되지 않는다.

secure 지원안함

 

추가.

키 - 아카이브

그냥 copy메서드를 사용했다고 해서  항상 완전한 Deep Copy를 했다고는 하기 힘들다. 그안에 nested된 모~든 타입들을 뒤져서 복사해야하기 때문에..

실제로 우리가 자주 사용하는  계층 구조를 보면. ImageView, Button, View 등등 많은 계층을 이루고 있다.

이 계층 구조를 Object - Graph라고 한다.

이러한 Object - Graph를 통째로 저장하고 싶을 때 사용하는 방식 Keyed - Archiving 이다.

Keyed - Archiving은 몇가지 특징을 가진다.

1. 하위 / 상위 호환성을 가질 수 있다.

2. 인코딩 / 디코딩 과정에서 대체 가능.

3. Coding Protocol을 구현해야한다. !중요!

NSCoding을 채택을 하면 encode와 decode를 반드시 구현을 해야한다.

encode란 archiving하고 싶은 데이터를 key로 archiving하는 것. 메모리 -> 파일, 서버, .. 

archiving 하고 싶지 않은 프로퍼티는 넣지 않아도 된다.   

decode는 그의 반대.    파일,서버 -> 메모리

특히 required init?은 우리가 UIView를 만들때 자주 보게됨!

 

지금처럼 서로 참조하고 있는 상황에서 Archiving할 경우 person0만 archiving하더라도 참조하고 있는 person1까지 됨.

 

이런걸 쓸일이 있을까??

Of Course.

ViewController에서 ViewDidLoad()에서 View를 Load를 할때 우리는 주로 StoryBoard나 xib파일에서 만든 View들을ViewController의 Self.View에다가 Loading한다는 표현을 사용하곤 한다.

사실 StoryBoard나 xib파일들은 Interface Builder에서 Build를 해서 App에다 적용 할 때 Archiving한 파일이 만들어짐.

그리고 ViewController는 이러한 Archiving파일을 decode해서 self.View에다가 loading하는 것이다.

 

UIView를 상속받은 아이들은 항상 required init(coder:) 가 있는 이유가 바로 이것이다.

StoryBoard나 xib파일로 만들어 진것들을 decode해야 하므로..

 

 

참고: https://woozzang.tistory.com/m/128

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

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

https://babbab2.tistory.com/18

'Swift' 카테고리의 다른 글

Generic을 이용한 CompileTime에 Type 검사하기.  (0) 2022.05.12
Memory Leak Case When ReferenceType in Dictionary  (0) 2022.05.11
MetaType_iOS  (0) 2022.05.05
동시성 프로그래밍 가이드 읽어보기.  (0) 2022.01.27
메모리와 Array  (1) 2022.01.12

댓글