🟣 같은 걸 복사했는데 결과가 다르다
입문기에 가장 크게 데였던 지점이다. 데이터 묶음을 만들 때 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) // 99b = a 시점에 a의 내용이 그대로 복제돼 별개의 b가 된다. 이후 b.x를 바꿔도 a는 자기 값을 지킨다.
놀랍게도 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) // 99b = a는 새 Box를 만들지 않는다. a가 가리키던 그 인스턴스를 b도 함께 가리킬 뿐이다. 그래서 b.value를 바꾸면 a.value로 봐도 바뀌어 있다.
처음에 나를 괴롭혔던 버그가 정확히 이 그림이었다. 화면 모델을 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다.