iOS Development Guide

값 타입과 참조 타입이 메모리에서 다뤄지는 방식

이 주제의 핵심은 스택/힙 암기가 아니라 동작 의미(semantics)다. 값 타입은 "복사된 값"처럼, 참조 타입은 "같은 실체 공유"처럼 동작한다. 메모리 위치는 이 동작을 구현하기 위한 결과다.

Value semantics Reference semantics Knowledge item #16
학습 날짜

기록 없음

핵심 비교

  • 값 타입(`struct`, `enum`, `tuple`): 대입/전달 시 값이 복사된 것처럼 독립 상태를 가진다.
  • 참조 타입(`class`, `closure`): 대입/전달 시 동일 인스턴스를 공유한다.
  • 값 타입은 공유 가변 상태를 줄이기 쉽고, 참조 타입은 생명주기와 동기화 관리가 중요하다.

자주 나오는 오해

  • `struct = 항상 스택`, `class = 항상 힙`은 과한 단순화다.
  • 실제 저장 위치는 캡처, 수명, 컴파일러 최적화 등으로 달라질 수 있다.
  • 실무에서 예측해야 할 것은 위치보다 "복사되는가/공유되는가"다.

가장 기본 예시: 대입 후 수정

struct CounterValue {
    var n: Int
}

final class CounterRef {
    var n: Int
    init(n: Int) { self.n = n }
}

var a = CounterValue(n: 1)
var b = a
b.n = 10
// a.n == 1, b.n == 10 (독립 상태)

let c = CounterRef(n: 1)
let d = c
d.n = 10
// c.n == 10, d.n == 10 (동일 인스턴스 공유)
면접/실무 설명 포인트: "값 타입은 변경 영향 범위가 지역적이고, 참조 타입은 전파 범위를 추적해야 한다."

Copy-on-Write (CoW)

`Array`, `Dictionary`, `String`은 값 타입이지만 내부 저장 버퍼를 참조로 최적화한다. 읽기만 할 때는 버퍼를 공유하고, 쓰기 시점에만 복사해서 값 의미를 유지한다.

var x = [1, 2, 3]
var y = x   // 내부 버퍼 공유 가능
y.append(4) // 쓰기 시점에 분리(copy)
// x == [1, 2, 3]
// y == [1, 2, 3, 4]

클로저 캡처와 수명

값 타입도 escaping 클로저에 캡처되면 저장 수명 때문에 힙이 관여할 수 있다. 그래도 외부 관찰 동작은 값 의미를 따르는지(스냅샷/변수 캡처)로 판단해야 한다.

struct S { var n: Int }
var s = S(n: 1)

let snap = { [s] in print("snap:", s.n) }
let live = { print("live:", s.n) }

s.n = 9
snap() // 1 (캡처 시점 스냅샷)
live() // 9 (현재 변수 값 참조)

언제 struct, 언제 class인가

상황 권장 이유
도메인 값(좌표, 금액, 응답 모델) `struct` 불변/독립 상태 설계가 쉽고 테스트가 단순해진다.
공유 identity가 필요한 객체(세션, 코디네이터, 캐시) `class` 하나의 실체를 여러 곳에서 참조하고 수명을 제어해야 한다.
상태 변경 전파가 빈번한 UI 바인딩 대상 상황별 선택 값 스냅샷 기반 갱신인지, 참조 공유 기반 관찰인지 먼저 결정한다.

실무 체크리스트

  • "공유해야 하는 상태"인지 먼저 정의했는가?
  • 불필요하게 `class`를 써서 전역 영향 범위를 키우지 않았는가?
  • 참조 타입이면 `deinit` 확인, retain cycle 점검, 동시성 보호 전략이 있는가?
  • 컬렉션 대량 복사 우려가 있으면 CoW 동작을 전제로 성능 측정했는가?

한 줄 결론

메모리 주소를 설명하는 것보다 상태 전파 범위를 설명할 수 있어야 한다.

"복사되는 모델인지, 공유되는 객체인지"를 먼저 설계하면, ARC, 성능, 동시성 이슈까지 훨씬 일관되게 다룰 수 있다.