Computer >> 컴퓨터 >  >> 스마트폰 >> iPhone

Swift의 값 타입 vs 참조 타입: 한 어머니와 두 아들의 이야기

한 어머니와 두 아들의 이야기

Swift를 한 가족에 비유해 보겠습니다. Swift라는 어머니에게는 두 아들이 있습니다.

  • 값 타입(Value Type)
  • 참조 타입(Reference Type)

그렇다면 이 둘은 각각 어떤 특징을 지니고 있을까요? 서로 같은 방식으로 동작할까요, 아니면 정반대로 동작할까요?

Swift란 무엇인가?

Swift는 Apple이 iOS, macOS, watchOS, tvOS, 그리고 Linux와 z/OS를 위해 개발한 멀티 패러다임 프로그래밍 언어입니다.

다른 객체지향 프로그래밍 언어와 마찬가지로 Swift에는 클래스가 있습니다. 클래스는 메서드, 프로퍼티, 이니셜라이저를 정의할 수 있고, 프로토콜을 채택할 수 있으며, 상속과 다형성도 지원합니다.

그런데 잠깐만요!

Swift에는 구조체(struct)도 있습니다. 구조체 역시 메서드, 프로퍼티, 이니셜라이저를 정의하고 프로토콜을 채택할 수 있습니다. 단 하나의 예외, 바로 '상속'만 빼고 말이죠.

네? 이제 헷갈리기 시작합니다!

혼란에 기름을 좀 더 부어 보자면, Swift에서 값 타입은 구조체만이 아닙니다. 튜플(tuple)과 열거형(enum)도 값 타입입니다. 마찬가지로 참조 타입이라고 해서 클래스만 있는 것도 아닙니다. 함수와 클로저(closure) 역시 참조 타입으로 동작합니다. 다행히 각 타입의 주된 용도와 활용 분야는 분명하기 때문에, 결국 남는 큰 질문은 하나입니다. "구조체와 클래스 중 무엇을 써야 하는가?"

이제 그 혼란을 하나씩 풀어 보겠습니다.

저장 위치(Storage Location)

메모리 저장소는 세 가지로 나눌 수 있습니다.

  • 레지스터(Register)
  • 스택(Stack)
  • 힙(Heap)

수명이 짧은 객체는 레지스터나 스택에 저장되고, 수명이 긴 객체는 힙에 저장됩니다.

값 타입은 일반적으로 스택에 할당된 메모리에 내용을 저장합니다. 그래서 많은 개발자가 "값 타입은 항상 스택에 저장된다"고 믿습니다.

하지만 이것은 흔한 오해입니다. 반드시 그런 것은 아닙니다.

값 타입이 스택에 저장되는 경우는 해당 인스턴스가 임시적이거나 지역 변수일 때입니다. 그렇다면 값 타입이 참조 타입(클래스) 내부에 포함되어 있다면 어떻게 될까요? 이 경우에는 힙 메모리에 저장될 수 있습니다.

결국 값 타입은 수명에 따라 레지스터, 스택, 힙 어디든 저장될 수 있습니다. 지역 변수라면 스택에서 생명을 얻고, 클래스의 일부라면 힙 메모리에 존재할 수도 있습니다.

반면 참조 타입은 힙에 할당된 메모리에 실제 데이터를 저장하고, 변수는 그 메모리 위치를 가리키는 참조(reference)만을 담습니다.

참조 타입에서는 여러 변수가 동일한 메모리 위치를 참조하는 상황이 매우 흔하게 발생합니다. 값 타입 인스턴스를 변수에 할당하거나 함수에 전달하면 인스턴스 전체가 복사됩니다. 하지만 참조 타입에서는 참조만 복사되고, 새 변수는 같은 메모리 위치를 가리키게 됩니다.

변경 가능성(Mutability)의 차이

변수는 두 가지 상태를 가질 수 있습니다.

  • 변경 가능(Mutable)
  • 변경 불가능(Immutable)

값 타입 인스턴스가 불변(let) 변수에 할당되면 그 인스턴스 자체도 불변이 됩니다. 따라서 해당 인스턴스에는 어떤 변경도 가할 수 없습니다. 값 타입 인스턴스가 변경 가능(var) 변수에 할당될 때만 인스턴스를 변경할 수 있습니다.

하지만 참조 타입은 완전히 다릅니다. 변수와 그 변수가 참조하는 인스턴스는 별개입니다. 클래스를 참조하는 불변 변수를 선언하면, 그 변수가 담고 있는 '참조'가 절대 변하지 않을 뿐입니다. 즉, 참조 대상을 다른 곳으로 바꿀 수는 없지만, 참조된 인스턴스의 내부 프로퍼티는 여전히 변경할 수 있습니다.

구조적 타입(Structural Type)과 동등성

구조적 타입의 값들은 속성이나 요소를 기준으로 동등성이 비교됩니다. 모든 대응 속성이 서로 같을 때, 그리고 그때만 두 값 타입은 동일하다고 말할 수 있습니다.

예를 들어 firstNamelastName 속성을 가진 Person 값 타입이 있다고 해 보겠습니다.

struct Person {
   var firstName: String
   var lastName: String
}

var person1 = Person(firstName: "foo", lastName: "bar")

var person2 = Person(firstName: "foo", lastName: "bar")

여기서 person1person2 인스턴스는 firstName("foo")과 lastName("bar")에 대해 같은 값을 담고 있습니다. 두 인스턴스의 모든 속성이 같으므로 서로 동일하다고 할 수 있습니다. 게다가 미래에 어떤 두 Person 인스턴스가 같은 이름 값을 가진다면, 그들 역시 서로 동일하게 취급됩니다.

여기까지 정리하면 다음과 같이 말할 수 있습니다.

값 타입은 고유한 정체성(identity)이 없으므로 참조될 수 없습니다. 값 타입은 '얼굴이 없는(faceless)' 존재입니다.

정말 그럴까요?

var myAge: Int = 21
var friendAge: Int = 21

myAgefriendAge는 모두 값이 21인 정수형 변수입니다.

이 둘을 구분할 수 있을까요?

아니요, 같은 값을 담고 있기 때문입니다. 값이 21인 정수 변수는 또 다른 21짜리 정수 변수와 구별될 수 없습니다. 아주 단순한 원리입니다.

정체성이 없다는 점은 값 타입에 또 하나의 장점을 안겨 줍니다. 현실적으로 생각해 보면, 정체성이 없다는 것은 같은 특성을 가진 누군가 언제든 나를 대체할 수 있다는 의미이기도 합니다. 인간에게는 정체성이 없으면 존재 자체가 위태로워지겠지만, 값 타입에게는 오히려 장점이 됩니다. 언제든 동일한 값으로 자유롭게 복사하고 교체할 수 있기 때문입니다.

값 타입 사용의 이점

1. 레이스 컨디션과 데드락이 없음

멀티스레드 환경에서 값 타입은 한 스레드가 인스턴스를 사용하는 도중 다른 스레드가 그 상태를 변경하는 것이 불가능합니다. 각 스레드가 독립적인 복사본을 갖기 때문입니다. 따라서 레이스 컨디션(race condition)이나 데드락(deadlock)이 발생하지 않습니다.

2. 순환 참조(Retain Cycle)가 없음

두 참조 타입 인스턴스가 서로를 강한 참조(strong reference)로 붙잡고 있어 메모리에서 해제되지 못하는 상황을 순환 참조라고 합니다. 값 타입은 참조로 동작하지 않기 때문에 순환 참조 문제에서 자유롭습니다.

3. 자동 참조 카운팅(ARC)이 필요 없음

참조 타입의 경우 Swift는 자동 참조 카운팅(Automatic Reference Counting, ARC)을 사용해 살아있는 객체를 추적하고, 더 이상 강한 참조가 없을 때만 인스턴스를 해제합니다. 잘 생각해 보면 이는 상당히 비용이 큰 작업입니다. Swift 런타임이 항상 객체를 추적해야 하기 때문입니다. 반면 값 타입은 스택에 할당되므로 ARC가 필요 없어 더 저렴하고 빠릅니다.

그런데 잠깐... Array, Dictionary, String은 어떻게 메모리를 관리할까요?

배열, 딕셔너리, 문자열은 컴파일 시점에 실제 크기를 알 수 없으므로 컴파일 타임에 할당될 수 없습니다. 내부적으로는 값 타입이지만 스택에 할당될 수 없고, 힙 메모리에 할당되어야 합니다. 이를 위해 Swift는 Copy on Write(COW) 기법을 사용합니다.

COW란 무엇일까요?

"한 인스턴스가 다른 인스턴스의 복사본이다"라는 말은 실제로는 두 인스턴스가 같은 값을 담고 있다는 의미입니다. Swift에서는 Array, Dictionary, String 같은 타입의 경우, 실제 복사가 힙에서 이루어지지만 오직 인스턴스가 변경(mutation)될 때만 복사가 발생합니다. 공유 중일 때는 참조처럼 동작하다가 수정이 일어나는 순간에만 진짜 복사본을 만드는, 값 타입을 위한 성능 최적화 기법인 셈입니다.

결론

값 타입과 참조 타입 중 무엇을 언제 사용해야 하는지에 대한 절대적인 규칙은 없습니다. 값 타입은 참조 타입에 없는 고유한 장점이 있고, 참조 타입 역시 저마다의 강점을 지니고 있습니다. 결국 중요한 것은 여러분의 요구 사항과 달성하려는 목표입니다. 코드의 의미론(semantics)을 가장 잘 아는 사람은 여러분 자신이며, 선택의 권한은 온전히 여러분에게 있습니다.

값 타입 대 참조 타입의 논쟁에 매달리기보다, 두 타입을 지혜롭게 활용하시기 바랍니다.

읽어 주셔서 감사합니다!