먹고 여행하고 사랑하라

INFJ X ENTJ 닮은 듯 다른 우리가 살아가는 이야기

개발 이야기 🧑🏻‍💻/Swift

스위프트의 값(Value), 그리고 참조(Reference) 타입

이가든 2023. 11. 30. 17:08
728x90
반응형

Value And Reference Types In Swift

 

스위프트에서 타입은 값 타입참조 타입 두 가지로 분류됩니다.

이 두 타입은 각각 다르게 작동하며 이 차이를 이해하는 것은 스위프트를 이해하는 데 중요한 부분입니다.

 

만약 프로그래밍이 처음이거나 다른 언어를 사용하다 스위프트로 넘어오는 경우라면, 이러한 개념들이 생소할 수 있습니다.

코드를 살펴보기 전에, 값 타입과 참조 타입이 같은 상황에서 어떻게 다르게 작동하는지를 보여주는 시나리오를 보겠습니다.

 

보고서나 스프레드 시트 작성 같은 문서 작업을 한다고 상상해 보세요.

친구에게 한 번 봐달라고 하고 싶을 때 이 문서를 친구에게 공유하는 두 가지 일반적인 방법이 있습니다:

 

  1. 이메일로 문서의 사본을 친구에게 보낼 수 있습니다.
  2. 문서가 Google Docs나 iCloud의 Pages에 저장되어 있다면, 이메일로 문서의 링크를 친구에게 보낼 수 있습니다.

 

두 가지 방법 모두 친구가 문서를 읽고 수정할 수 있지만, 여기엔 중요한 차이점이 있습니다.

 

1번 방법처럼 친구에게 문서의 사본을 보내면, 친구는 기존 문서가 아닌 완전히 별도의 문서를 갖게 됩니다. 친구는 원하는 만큼 문서를 편집할 수 있지만, 내가 가지고 있는 문서 사본에는 전혀 영향을 미치지 않습니다.

 

반면 2번 방법처럼 친구에게 문서의 링크를 보내면, 실제 문서를 보내는 것이 아니라 클라우드(Google Docs나 iCloud 등)에 저장된 문서를 가리키는 URL을 보내는 것입니다. 즉 나와 친구 둘 다 같은 문서에 대한 링크를 가지고 있기 때문에, 내가 혹은 친구가 변경한 사항을 서로가 바로 확인할 수 있습니다.

 

이렇게 문서의 사본과 문서의 링크를 공유하는 것의 차이는 값 타입과 참조 타입의 동작 차이와 매우 유사합니다.

 

 

 

Value Types (값 타입들)

스위프트에서 구조체(Strcutures), 열거형(enumerations), 그리고 튜플(Tuples)은 모두 값 타입입니다.

이 타입들은 문서의 사본을 친구에게 보내는 것과 유사한 방식으로 동작합니다.

 

상수(Constant)나 변수(Variable)에 값을 할당하거나 함수(Function)나 메서드(Method)에 값을 전달할 때, 항상 그 값의 사본이 만들어집니다.

struct Document {
  var text: String
}

var myDoc = Document(text: "Great new article")
var friendDoc = myDoc

friendDoc.text = "Blah blah blah"

print(friendDoc.text) // 출력: "Blah blah blah"
print(myDoc.text) // 출력: "Great new article"

 

위 코드에서는 text라는 하나의 String 속성을 가진 Document라는 struct가 선언되었습니다.

 

Document의 인스턴스를 생성하고 myDoc에 할당합니다.

 

myDoc을 변수 friendDoc에 할당하면 myDoc의 인스턴스가 새로운 인스턴스로 복사됩니다.

 

따라서 friendDoc에 할당된 인스턴스는 독립적인 인스턴스이므로 friendDoctext를 변경해도 myDoctext에 영향을 주지 않습니다.

 

친구에게 문서의 사본을 보내면 친구가 문서를 수정해도 내가 가지고 있는 문서에 영향이 없기 때문에 문서 사본이 예상치 못하게 변경되지 않을까 걱정할 필요가 없습니다.

이와 같이, 값 타입을 사용할 때도 프로그램의 다른 부분이 값을 변경할까 걱정할 필요가 없습니다.

 

 

 

Reference Types (참조 타입들)

스위프트에서 클래스(classes), 액터(actors), 그리고 클로저(closures)는 모두 참조 타입입니다.

이 타입들은 문서의 링크를 친구에게 보내는 것과 유사한 방식으로 동작합니다.

 

상수(Constant)나 변수(Variable)에 참조 타입을 할당하거나 함수(Function)나 메서드(Method)에 참조 타입을 전달할 때, 할당되고 전달되는 것은 공유된 인스턴스에 대한 참조입니다.

class Document {
  var text: String
}

var myDoc = Document(text: "Great new article")
var friendDoc = myDoc

friendDoc.text = "Blah blah blah"

print(friendDoc.text) // 출력: "Blah blah blah"
print(myDoc.text) // 출력: "Blah blah blah"

 

위 코드는 값 타입의 예제와 완전히 동일하지만 한 가지 중요한 차이점이 있습니다.

바로 Documentstruct가 아닌 class로 선언되었다는 점입니다.

 

struct에서 class로 바뀐 것뿐이지만 동작에는 변화가 생깁니다.

 

이전처럼 Document의 인스턴스를 생성하고 myDoc에 할당합니다.

 

하지만 이젠 myDoc을 friendDoc에 할당할 때 myDoc의 인스턴스에 대한 참조가 할당됩니다.

 

동일한 인스턴스에 대한 참조이기 때문에, friendDoctext가 변경되면 myDoc을 포함한 공유된 인스턴스가 모두 업데이트됩니다.

 

친구에게 문서의 링크를 보내면 나의 확인이나 허가 없이도 친구는 문서를 수정할 수 있기 때문에 문서가 예상치 못하게 변경될 수 있습니다.

이와 같이, 참조 타입을 사용할 때도 프로그램의 어떤 부분이든 해당 참조를 가지고 있다면 데이터를 변경할 수 있으며 이런 예상치 못한 변경은 버그로 이어질 수 있습니다. 

 

 

 

Local reasoning (지역 추론)

위의 참조 타입 예제코드를 한 줄씩 읽어 나가다 보면 서로 다른 두 개의 다른 변수에 동일한 참조가 할당되고, 그중 하나의 변수를 사용하여 속성을 변경하면 두 변수가 참조하는 인스턴스가 모두 업데이트되는 것을 확인할 수 있습니다.

 

이런 식으로 코드의 한 부분을 검토하고 무슨 일이 일어나고 있는지 이해할 수 있는 능력을 지역 추론이라고 합니다.

 

이제 프로그램의 여러 부분에 동일한 참조가 할당된 경우를 상상해 보세요. 여러분의 코드는 값을 설정하고 해당 값이 설정한 대로 있기를 바라지만, 프로그램과 관련 없는 다른 부분이 나의 의도와 다르게 값을 변경할 수 있습니다.

 

이렇게 여러 곳에서 변경될 수 있는 데이터를 가지고 있는 상태를 공유 가능한 가변 상태(shared mutable state)라고 합니다.

여러 곳에서 코드에 접근할 수 있기 때문에 '공유(shared)', 변경될 수 있기 때문에 '가변(mutate)', 데이터의 현재 '상태(state)'를 의미하는 용어인 것입니다.

 

위와 같이 프로그램의 여러 부분에 동일한 참조가 할당되어 있는 경우, 코드의 한 부분을 완전히 이해하려면 다른 코드에서 어떤 일들이 일어날 수 있는지 알아야 합니다. 즉, 여러분은 지역 추론을 할 수 없게 되고, 이는 코드를 이해하기 어렵게 할 뿐만 아니라 디버그 또한 어렵게 만듭니다.

 

값 타입을 사용하는 것의 장점 중 하나는 프로그램의 다른 곳에서 값에 영향을 줄 수 없다는 것을 확신할 수 있다는 점입니다. 코드의 다른 곳에서 일어나는 일을 알 필요 없이 눈앞의 코드를 추론할 수 있습니다.

따라서 여러분의 코드가 이해하기 쉬워지고, '공유 가능한 가변 상태'로 인한 예상치 못한 변경으로 발생하는 버그를 방지할 수 있습니다.

 

 

 

값 혹은 참조 타입의 선택

친구에게 문서를 공유하는 시나리오에서, 여러분과 친구가 동일한 문서를 보고 편집할 수 있는 것은 매우 유용할 수 있습니다.

마찬가지로 때로는 참조 타입이 제공하는 '공유 가능한 가변 상태'가 프로그램에 매우 유용할 수 있습니다. 참조 타입 자체가 나쁜 것이 아니라 위에서 설명했듯이 복잡하고 오류가 생길 가능성이 있는 것입니다.

 

일반적으로 클래스보다는 구조체를 사용하는 것을 선호합니다. 참조 타입의 동작이 필요하지 않은 경우라면 굳이 복잡한 참조 타입을 사용할 필요는 없습니다.

 

 

 

값 타입의 합성

코드에서 흔히 사용되는 디자인 패턴 중 하나는 작은 요소들을 결합하여 더 큰 요소를 만드는 합성(composition)입니다.

 

스위프트에서는 값 타입들을 쉽게 조합하여 더 복잡한 값 타입을 만들 수 있습니다.

 

예를 들면 String, Int, Bool, 열거형 값처럼 몇 가지 기본 타입이 포함된 구조체를 정의할 수 있습니다. 구조체 내의 모든 것들은 값 타입이기 때문에, 구조체는 값 타입처럼 작동합니다.

 

또한, 이 구조체의 인스턴스와 다른 몇 가지 값을 포함하는 더 복잡한 구조체도 정의할 수 있습니다. 이 구조체 역시 값 타입으로만 구성되어 있기 때문에 값 타입입니다.

 

 

 

Collections(컬렉션)은 값 타입이다

스위프트에서 값 타입을 구성하는 것은 구조체와 열거형뿐만이 아닙니다.

 

다른 많은 언어들에서 배열(Arrays)과 딕셔너리(Dictionaries) 같은 컬렉션들은 참조 타입이지만, 스위프트에서는 표준 컬렉션인 Array, Dictionary, 그리고 String 모두 값 타입입니다.

 

이는 구조체 안에 구조체 배열, 키-값 쌍의 딕셔너리, 열거형의 세트와 같은 것들을 포함할 수 있음을 뜻합니다. 모든 것이 값 타입으로 구성되어 있다면, 아무리 복잡한 타입의 인스턴스도 값으로 취급됩니다.

 

 

 

결론

값 타입과 참조 타입이 무엇이며 이 두 가지 타입의 동작 차이를 이해하는 것은 스위프트를 배우고 코드를 이해하는 데에 중요한 부분입니다. 두 가지 타입 중 하나를 선택하는 것은 타입을 구조체로 선언할지 클래스로 선언할지를 선택하는 문제이기도 합니다. The Swift Programming LanguageStructures and Classes 장에서 구조체와 클래스에 대해 더 자세히 알아볼 수 있습니다.

728x90
반응형