[Swift] Value Type vs Reference Type

JISU LEE·2021년 12월 12일
1

Swift

목록 보기
2/3
post-thumbnail

이런건 공식 자료가 어디인걸까..? 잘 모르겠어서 우선 Wikipedia로 들어갔다.

컴퓨터 프로그래밍에서 데이터 타입은 Value type과 Reference type으로 나눌 수 있다고 한다. 간단하게 요약하자면 Value type의 값은 실제 값이고, Reference type의 값은 다른 값의 참조 값이다.

이렇게만 말하면 뭔말인지 모를테니 천천히 훑어봐보자.

Classification per language

언어마다 타입별 자료형들이 다른데 그 자료는 이 테이블을 참고하면 될 것 같다.
다 외울 필요는 전혀 없을 것 같고, 인상 깊었던 부분은 Python은 전부 다 Reference type이라는 것 정도? 이다.

Boxing and unboxing

그 다음은 Boxing? Unboxing? 유튜브에서나 본 언박싱을 여기서 볼 줄은 몰랐다..
용어에 대해 조금 더 알아봐야 겠다.

Boxing

값 타입과 참조 타입을 구별하는 프로그래밍 언어는 일반적으로 값 타입의 일부나 전체를 참조유형으로 감싸기 위해 boxing이라는 메커니즘을 제공.
Stack 변수를 Heap 변수로 만듦.

Unboxing

참조 유형으로 감싸진 값 유형을 풀기 위한 과정.
Heap 변수를 Stack 변수로 만듦.

Boxing과 Unboxing은 반대 개념이라고 보면 될 것 같다.
Value type —-boxing—-> Reference(Value Type) —-unboxing-—> Value Type

Boxing의 예로 Objective-C의 NSNumber가 나와있는데, 이 부분을 살펴보면 Boxing의 개념을 알 수 있을 것 같다.

NSArray에는 reference type만 넣을 수 있는데, NSArray에 Int 타입을 넣어주기 위해 NSNumber로 boxing을 해준다는 것이다.

// NSArray의 특징으로 인해 실제로 값이 세팅되지 않음
NSArray *array = [[NSArray alloc] initWithObjects: 1, 2, nil]

// NSArray에 정상적으로 값이 세팅되려면
NSArray *array = [[NSArray alloc] initWithObjects:
				  [NSNumber numberWithInt: 1],
		                  [NSNumber numberWithINt: 2], nil];

저렇게 세팅된 NSArray에서 1, 2를 꺼내 value type으로 다시 사용하려면 unboxing해주는 과정이 필요하게 되는 것이다.

NSNumber가 Swift에서도 boxing의 대표적인 예시로 쓰이는 것 같은데, boxing 시 자료형의 불일치로 값이 손실될 수 있다는 것을 유의해야 한다고 한다.

let num = NSNumber(floatLiteral: 12.345)
num.intValue // 소수 유실

[Objective C] boxing (NSArray 에 int 형 변수 넣기) | [Swift] Boxing

위 두 사이트를 참고하였는데, 위와 같은 방식으로 boxing하는 것 보다 class로 감싸서 넘기는 것을 추천하고 있다.

Swift에서도 주로 이런 방식으로 boxing을 하는 것 같다. (Boxing technique in Swift 참고)

또한 wikipedia에서는 반복적인 boxing & unboxing은 성능에 심각한 영향을 줄 수 있다고 명시하고 있다. 그 이유는 boxing은 새 객체를 동적으로 할당하고, unboxing될 경우 boxed되었던 값은 참조가 없어지기 때문에 다시 deinit돼야 하는 것이다.

나는 이를 수명이 짧은 객체가 여러개 생성(boxing)되었다가 쓸모 없어지는 상황(unboxing)이 많아지면 많아질수록, garvage collector가 돌기 전까진 쓸데없이 메모리를 차지하고 있게 된다는 것으로 해석했다.
(그래서 Java HotSpot 같이 수명이 짧은 객체를 더 효율적으로 수집할 수 있는 최신 garvage collector의 경우에는 큰 영향이 없다고 말하고 있다.)

요약하자면 Value type과 Reference type 간의 이동을 Boxing, Unboxing이라고 부르며, 과도한 Boxing & Unboxing은 성능 상 좋지 않다.

Value and Reference Types in Swift

Value and Reference Types - Swift Blog 참고

이제는 Swift에서의 값 타입과 참조 타입을 알아보려고 한다.
Value type은 복사를 한다는 것, Reference type은 공유 한다는 것이 가장 큰 차이점인데 코드로 설명해보겠다.

// value type
struct Book { var page: Int = -1 }
var book1 = Book()
var book2 = book1
book1.page = 200
print("\(book1.page), \(book2.page)") // prints "200, -1"
1. var book1 = Book()2. var book2 = book13. book1.page = 200

book1의 page 값이 바꼈음에도 book2는 기존 초기값을 그대로 가지고 있다.

// reference type
class Book { var writer: String = "tmfrlrkvlek" }
var book1 = Book()
var book2 = book1
book1.writer = "jisu"
print("\(book1.writer), \(book2.writer)") // prints "jisu, jisu"
1. var book1 = Book()2. var book2 = book13. book1.writer = "jisu"

이번에는 book1의 writer가 jisu로 변하자 book2의 writer도 jisu로 변한 것을 확인할 수 있다.

The Role of Mutation in Safety

Swift에서는 Value type을 더 권장하곤 하는데, 그 이유에 대해서도 설명하고 있다.

코드에 대한 추론이 쉬워진다.
항상 고유한, 복사된 객체를 얻는다면 앱의 다른 부분이 내부적으로 데이터를 변경하지 않는다는 것을 신뢰할 수 있다.
debug하기 힘든 버그를 생성할 수 있는 다중 스레드 환경에서 특히 유용하다.

How to Choose?

Cocoa로 작업한다면 많은 API들이 NSObject의 상속을 기대하기 때문에 class를 사용해야 한다.

하지만 다른 경우에는 몇가지 가이드 라인이 있다.

Value type을 쓰는 경우

  1. 객체끼리 비교(==, < 등)하는 것이 의미가 있을 경우
  2. 복사본이 독립적인 상태를 갖기를 원할 경우
  3. 데이터가 여러 스레드에서 사용될 경우

Reference type을 쓰는 경우

  1. 인스턴스 ID를 === 로 비교하는 것이 의미 있을 경우
    === 는 뒤에서 설명
  2. 변경 가능한 공유 상태를 만들고 싶을 경우

Swift에서는 Array, String, Dictionary가 모두 값 유형이다. 이것들은 C의 int 값처럼 쓰이며 해당 데이터의 고유한 인스턴스 역할을 한다. 동기화 없이 스레드 간 값 복사본을 안전하게 전달할 수 있다. 안전성의 형샹으로 Swift에서 예측 가능한 코드를 작성하는 데 더 도움이 된다.

위 가이드라인은 애플 공식 blog에 나와있는 부분을 발췌했다. 이게 2014년도 글이니 애플은 생각보다오래 전부터 value type을 밀어왔었나보다😳

=== & !==

Structures and Classes - The Swift Programming Language (Swift 5.5) 참고

위 Reference type을 쓰는 경우에서 ===을 보고 뭔가 싶었던 분들도 있을 것이다. == 같은 비교 연산자와는 달리 얘는 식별 연산자라고 한다.

우리가 일반적으로 사용하는 ==Equatable protocol을 채택했을 경우 사용 가능한 것으로, 일정한 조건에 부합하는지를 확인하는 것이다. 반면 ===는 내부에서 이미 구현되어있어 따로 선언해 줄 필요가 없는 것으로, 동일한 객체를 참조하는지 확인하는 것이다.

참조를 확인하는 것이니 Reference type에서 쓰이는 것으로, 객체의 원본이 정확히 동일한지를 비교한다.

Swift 타입에 따른 메모리 할당

주로 Value type은 Stack에, Reference type은 Heap에 할당된다.

[iOS] Swift의 Type과 메모리 저장 공간 참고

하지만 위 글에 따르면 Value type이 Heap에, Reference type이 Stack에 생성되는 몇몇 경우도 있다고 한다. 이에 대한 자세한 내용은 추후에 더 학습해보는 것으로 해야겠다.

궁금한 점 : Reference type이 Stack에 할당되는 경우 === 연산자는 Heap 주소가 아니라 Stack의 주소를 비교할까?

Summary

구분Value typeReference type
저장실제 값참조 값
할당(일반적으로) Stack(일반적으로) Heap
= 사용복사본 전달참조 전달
수정 시 원본 변경XO
=== 사용불가능가능
사용하는 경우데이터가 여러 스레드에서 사용될 경우
복사본이 독립적인 상태를 갖기를 원할 경우
NSObject 채택이 필요한 경우
변경 가능한 공유 상태를 만들고 싶을 경우
profile
iOS / 🌊

0개의 댓글