LODY/정리

Swift 언어 기본 / 값 타입과 참조 타입

값 타입과 참조 타입

🟣 같은 걸 복사했는데 결과가 다르다

입문기에 가장 크게 데였던 지점이다. 데이터 묶음을 만들 때 struct로 만들지 class로 만들지 감이 없었다. 둘 다 프로퍼티와 메서드를 담을 수 있으니 겉보기엔 비슷했다. 그런데 복사해서 한쪽을 고쳤더니, 구조체는 다른 쪽이 멀쩡한데 클래스는 다른 쪽도 같이 바뀌었다. 같은 문법으로 만든 것 같은데 결과가 정반대라 한참을 헤맸다.

원인은 하나였다. 구조체는 값 타입이고 클래스는 참조 타입이며, 이 둘은 복사의 의미가 다르다. 이 장은 그 "복사"라는 단어의 두 가지 뜻을 갈라 놓는다.


🟣 값 타입: 복사하면 남남이 된다

값 타입은 변수에 대입하거나 함수에 넘길 때 내용 전체가 통째로 복사된다. 복사가 끝나면 원본과 사본은 완전히 남남이다. 한쪽을 고쳐도 다른 쪽은 그대로다. Swift에서 struct, enum, 그리고 튜플이 값 타입이다.

struct Point {
    var x: Int
    var y: Int
}
 
var a = Point(x: 1, y: 2)
var b = a          // 내용이 통째로 복사된다
b.x = 99
 
print(a.x)  // 1  (a는 영향 없음)
print(b.x)  // 99

b = a 시점에 a의 내용이 그대로 복제돼 별개의 b가 된다. 이후 b.x를 바꿔도 a는 자기 값을 지킨다.

ax:1 y:2b (복사본)x:99 y:2복사 후 서로 독립. 한쪽을 고쳐도 다른 쪽은 그대로내용 통째 복사

놀랍게도 Swift 표준 라이브러리의 핵심 타입 상당수가 값 타입이다. Int, Double, String, Array, Dictionary가 전부 구조체다. 그래서 배열을 함수에 넘기고 함수 안에서 바꿔도 바깥 배열은 안전하다. 값 타입이 기본값이라는 사실이 Swift 코드를 예측 가능하게 만드는 큰 이유다. 넘긴 값이 나 모르게 바뀔 걱정이 없다.


🟣 참조 타입: 복사해도 같은 것을 가리킨다

참조 타입은 대입하거나 넘길 때 내용을 복사하지 않는다. 대신 그 인스턴스를 가리키는 참조만 복사된다. 그래서 원본과 사본은 이름만 둘일 뿐, 실제로는 같은 하나의 인스턴스를 함께 가리킨다. 한쪽을 통해 고치면 다른 쪽에도 그대로 보인다. Swift에서 class가 참조 타입이다.

class Box {
    var value: Int
    init(value: Int) { self.value = value }
}
 
var a = Box(value: 1)
var b = a          // 참조만 복사된다. 인스턴스는 하나
b.value = 99
 
print(a.value)  // 99  (a로 봐도 바뀌어 있다)
print(b.value)  // 99

b = a는 새 Box를 만들지 않는다. a가 가리키던 그 인스턴스를 b도 함께 가리킬 뿐이다. 그래서 b.value를 바꾸면 a.value로 봐도 바뀌어 있다.

abBox 인스턴스value: 99가리킴같은 것을 가리킴

처음에 나를 괴롭혔던 버그가 정확히 이 그림이었다. 화면 모델을 class로 만들어 두 곳에 넘겼는데, 한 화면에서 고친 값이 다른 화면에도 새어 나갔다. 참조 타입이니 둘이 같은 인스턴스를 보고 있었던 것이다.


🟣 정체성과 동등성

참조 타입에는 값 타입에 없는 개념이 하나 더 있다. 두 참조가 같은 인스턴스를 가리키는가를 묻는 정체성이다. Swift는 이걸 === 연산자로 확인한다. 값이 같은지를 묻는 ==와는 다르다.

let a = Box(value: 1)
let b = Box(value: 1)   // 값은 같지만 별개의 인스턴스
let c = a               // a와 같은 인스턴스
 
a === b   // false  (다른 인스턴스)
a === c   // true   (같은 인스턴스)

===는 값 타입에는 아예 쓸 수 없다. 구조체는 복사되는 순간 별개가 되므로 "같은 인스턴스인가"라는 질문 자체가 성립하지 않기 때문이다. 정체성이라는 개념이 있느냐 없느냐가 두 타입을 가르는 또 하나의 선이다.


🟣 그래서 언제 무엇을 쓰나

애플의 가이드도, 실무 경험도 결론은 비슷하게 모였다. 기본은 구조체로 두고, 참조 의미가 꼭 필요할 때만 클래스를 쓴다.

구조체가 맞는 경우는 데이터를 담아 옮기는 대부분의 상황이다. 좌표, 색상, API 응답을 담는 모델처럼 "값 자체"가 중요하고 복사돼도 문제없는 것들이다. 값 타입은 복사되면 독립하므로 예상치 못한 공유로 인한 버그가 원천적으로 없다.

클래스가 필요한 경우는 좁다. 여러 곳이 같은 하나의 인스턴스를 공유하며 상태를 함께 봐야 할 때, 또는 상속이 필요할 때다. 화면 하나에 대응하는 뷰 컨트롤러, 앱 전체가 공유하는 설정 객체 같은 것들이 그렇다. 이런 것들은 복사되면 오히려 곤란하다. 앱 어디서 보든 같은 설정이어야 하기 때문이다.

판단이 설 때는 질문 하나로 정리됐다. "이걸 복사했을 때 둘이 남남이 되는 게 맞나, 아니면 같은 것을 계속 봐야 하나." 남남이 맞으면 구조체, 같은 것을 봐야 하면 클래스다.


🟣 정리

  • 값 타입(struct, enum, 튜플)은 대입하고 넘길 때 내용이 통째로 복사되어 원본과 사본이 남남이 된다. 한쪽을 고쳐도 다른 쪽은 그대로다.
  • 참조 타입(class)은 참조만 복사되어 여러 이름이 같은 하나의 인스턴스를 가리킨다. 한쪽으로 고치면 다른 쪽에도 보인다. 입문기 버그의 상당수가 이 공유에서 나왔다.
  • Int, String, Array, Dictionary 등 Swift 표준 타입은 값 타입이다. 값 타입이 기본이라는 점이 코드를 예측 가능하게 만든다.
  • ===는 두 참조가 같은 인스턴스인지 묻는 정체성 비교로, 참조 타입에만 쓴다. 값 타입은 복사되면 별개이므로 정체성이라는 개념이 없다.
  • 선택 기준은 "복사했을 때 남남이 되는 게 맞나"이다. 남남이 맞으면 구조체, 같은 것을 계속 봐야 하면 클래스다. 면접에서 자주 나오는 질문이므로, 복사 의미와 정체성 두 축으로 답하면 깔끔하다.

참조 타입은 여러 곳에서 같은 인스턴스를 가리킨다고 했다. 그러면 아무도 그 인스턴스를 안 볼 때가 언제인지, 즉 언제 메모리에서 지워도 되는지가 문제가 된다. Swift가 이걸 자동으로 세어서 관리하는 방식이 다음 장의 ARC다.