디스패치 편에서 “호출이 어떻게 번역되는가”를 봤다면, 이번엔 “값이 어떻게 놓이는가”입니다.
이전 글 Swift 심화 #5에서 이어지는 내용입니다.
struct의 크기는 프로퍼티 크기의 합일까요? 아닙니다.
프로퍼티 선언 순서를 바꾸면 struct 크기가 달라질 수 있다면 믿으시겠어요? 심화 시리즈, 메모리 레이아웃 편입니다.
이 지식이 필요한 순간은 분명합니다. 수십만 개를 배열에 담는 모델을 설계할 때, 또 Instruments에서 메모리 그래프가 예상보다 뚱뚱할 때죠.
그리고 some·any 편에서 미뤄둔 질문, “그 상자는 실제로 얼마나 큰가”에 답할 때입니다.
세 가지 치수 — size, stride, alignment
Swift는 타입의 메모리 치수를 물어볼 수 있는 도구를 표준으로 제공합니다. MemoryLayout입니다.
MemoryLayout<Int>.size // 8
MemoryLayout<Bool>.size // 1
struct Point { var x: Double; var y: Double }
MemoryLayout<Point>.size // 16
치수는 세 개고, 각각 의미가 다릅니다.
size는 값 하나의 연속된 메모리 footprint입니다. 타입 끝의 정렬을 위한 tail padding은 포함하지 않습니다.
**alignment(정렬)**는 그 타입이 놓일 수 있는 주소의 규칙이에요. CPU는 8바이트 값을 8의 배수 주소에서 읽는 게 효율적입니다(아키텍처에 따라 필수이기도 하고요).
그래서 타입마다 “n의 배수 주소에 놓인다”는 제약이 붙습니다. 아래 수치는 현재 64비트 Apple 플랫폼 기준이며, Double의 alignment는 8, Bool은 1입니다.
**stride(보폭)**는 배열에서 다음 원소까지의 간격입니다. 정렬 제약 때문에 원소 사이에 빈 공간(패딩)이 생길 수 있어서 stride ≥ size예요.
세 치수의 관계를 보여주는 고전 예제가 이겁니다.
struct Bad {
var flag: Bool // 1바이트
var value: Double // 8바이트 — 8의 배수 주소에 놓여야 함
var count: Int32 // 4바이트
}
MemoryLayout<Bad>.stride // 24
struct Good {
var value: Double // 8
var count: Int32 // 4
var flag: Bool // 1
}
MemoryLayout<Good>.stride // 16
내용물은 같은데 순서만 바꿨더니 배열 기준 33%가 절약됩니다. Bad에서는 flag 뒤의 value를 정렬하려고 7바이트 패딩이 필요합니다(Swift ABI Type Layout).
실무에서는 alignment가 큰 프로퍼티부터 묶는 것이 패딩을 줄이는 흔한 휴리스틱입니다. 다만 모든 타입에서 최적을 보장하는 규칙은 아니므로 최종 결과는 MemoryLayout으로 재야 합니다.
현재 Swift의 fragile struct 레이아웃 알고리즘은 저장 프로퍼티를 선언 순서대로 배치합니다. 하지만 필드 순서는 언어와 ABI의 보장이 아닙니다(Swift ABI · SE-0260). 정확한 오프셋이 필요한 코드에서는 관찰값에 의존하면 안 됩니다.
둘째, 이 최적화가 의미 있는 건 인스턴스가 대량일 때뿐이에요. 설정 객체 하나의 8바이트와 달리, 100만 원소 배열의 원소당 8바이트는 총 8MB입니다(Swift ABI Type Layout).
늘 그렇듯 계측이 먼저입니다.
enum의 마법 — Optional 태그가 공짜일 때
enum의 레이아웃에는 Swift 컴파일러의 영리함이 잘 드러납니다. 케이스 구분값(태그)을 저장해야 하는데, 페이로드 타입의 쓰이지 않는 비트 패턴(extra inhabitants)에 태그를 숨기거든요.
대표 사례가 Bool?입니다. 현재 64비트 Apple 플랫폼에서 MemoryLayout<Bool?>.size는 1입니다.
Bool은 1바이트 중 두 패턴(0, 1)만 쓰니, 남는 패턴 하나를 nil 표현에 쓰는 거예요.
참조 타입 옵셔널도 같습니다. Swift 클래스 포인터는 null을 객체로 사용하지 않으므로 nil을 0으로 표현합니다. 현재 64비트에서 AnyObject?의 size는 8입니다.
하지만 모든 Optional이 공짜인 건 아닙니다. Int는 8바이트 비트 패턴을 값 표현에 사용합니다. 따라서 현재 환경에서 Int?의 size는 9이고 stride는 16입니다(Swift ABI).
옵셔널 편에서 말한 “안전은 공짜에 가깝다”는 표현은 이처럼 여분의 표현을 재활용할 수 있는 타입에서 정확합니다. Optional이라는 문법 자체가 모든 타입에서 메모리 비용 제로를 보장하는 것은 아닙니다.
상자의 실측 — 단일 프로토콜 existential은 5워드
some·any 편에서 “any는 상자고 비용이 있다”고만 했는데, 이제 실측할 수 있습니다.
protocol Shape {}
MemoryLayout<any Shape>.size // 40 — 현재 64비트 환경
클래스 제약이 없는 단일 프로토콜 any Shape는 현재 64비트 환경에서 40바이트, 즉 5워드입니다. 구성은 값 버퍼 3워드 + 타입 메타데이터 포인터 + witness table 포인터 하나예요.
여기서 중요한 갈림길이 생깁니다. 담는 값의 size와 alignment가 모두 3워드 인라인 버퍼의 조건을 만족하면 버퍼에 직접 들어갑니다.
조건을 넘으면 값을 별도 저장 공간에 할당하고 버퍼에는 포인터를 둡니다. 이때 힙 할당, 참조 카운팅, 간접 참조 비용이 추가될 수 있습니다(Swift ABI · SE-0335).
5워드라는 수치는 모든 any 타입의 고정 크기가 아닙니다. 프로토콜 조합은 witness table 포인터가 늘 수 있습니다.
클래스 전용 프로토콜 existential은 값 버퍼와 별도 메타데이터 포인터가 필요 없는 더 작은 레이아웃을 씁니다.
프로토콜 지향 코드가 예상 밖으로 느리다면 some·제네릭으로 상자를 제거하거나 담는 타입을 줄이는 방법을 검토할 수 있습니다. 실제 효과는 Instruments와 벤치마크로 확인해야 합니다(SE-0335 Existential any).
클래스 인스턴스의 치수도 짚어볼게요. 현재 64비트 Apple 플랫폼에서 클래스 타입 값의 stride는 8이며, 객체가 아니라 참조의 크기입니다.
네이티브 Swift 힙 객체는 일반적으로 메타데이터 포인터와 인라인 참조 카운트 정보로 구성된 헤더를 두며, 64비트에서는 16바이트입니다. 실제 할당 크기는 프로퍼티 정렬과 할당기의 반올림 때문에 더 커질 수 있습니다(Swift HeapObject · Swift RefCount).
ARC 편에서 말한 참조 타입의 숨은 비용은 이 헤더와 할당·해제·카운팅 연산입니다. 작은 데이터 조각 수십만 개를 클래스로 만들면 데이터보다 관리 비용이 커질 수 있습니다.
실무 도구함 — 재보고, 확인하고, 의심하기
이 층을 다루는 실무 도구를 정리합니다.
MemoryLayout으로 가설 검증. 대량 생산되는 모델 struct를 설계할 때 stride를 한 번 재보는 습관이면 충분합니다.
유닛 테스트에 #expect(MemoryLayout<Tick>.stride <= 32)처럼 예산을 넣어둘 수도 있습니다. 나중에 프로퍼티가 추가돼 예산을 넘기면 바로 드러납니다.
Instruments Allocations로 힙 확인. “struct 위주로 짰는데 힙 할당이 많다”면 용의자는 정해져 있습니다.
any 상자, 클로저 캡처(클로저 편), CoW(copy-on-write) 저장소, 그리고 클래스 인스턴스. Allocations의 타입별 집계가 어느 쪽인지 알려줍니다.
copy-on-write 컬렉션의 착시: 현재 64비트 Apple 플랫폼에서 MemoryLayout<[Int]>.stride는 8입니다. Array 값은 힙 저장소를 간접 참조하는 작은 struct지만, 이 수치를 고정 ABI로 가정하면 안 됩니다.
배열이 담긴 struct의 stride가 작다고 “가볍다”고 결론 내면 안 되는 이유입니다.
실제 무게는 힙 저장소에 있고, 그건 Allocations가 보여줍니다. CoW 원리는 별도 글에서 다룬 대로고요.
마지막으로 균형 감각입니다. 이 편의 지식이 매일 쓰이는 건 아닙니다.
하지만 “왜”를 아는 것의 가치는 문제 상황에서 드러납니다. 메모리 그래프가 이상할 때 추측이 아니라 용의자 목록에서 시작할 수 있다는 것, 그게 이 층을 한 번 내려가본 사람의 이점입니다.
정리
- 타입의 치수는 셋입니다. size는 tail padding을 제외한 footprint, alignment는 주소 정렬 규칙, stride는 연속 배치 간격입니다. 대량 생산 struct는 alignment가 큰 프로퍼티를 묶으면 패딩이 줄어드는 경우가 많지만 반드시 실측해야 합니다.
- enum은 페이로드의 extra inhabitant에 태그를 숨길 수 있습니다. Bool?과 참조 옵셔널은 추가 공간이 없지만 Int?처럼 별도 태그 공간이 필요한 타입도 있습니다.
- 64비트의 단일 비클래스 프로토콜 existential은 5워드입니다. 값의 size나 alignment가 3워드 버퍼 조건을 넘으면 별도 저장 공간과 간접 비용이 생길 수 있습니다.
- 현재 64비트 네이티브 Swift 객체의 기본 헤더는 일반적으로 16바이트입니다. 실제 할당 크기는 프로퍼티와 allocator 정렬에 따라 더 커질 수 있습니다.
- 도구는 MemoryLayout(설계 검증)과 Instruments Allocations(실측)입니다. 계측 없는 레이아웃 최적화는 조기 최적화입니다.
다음 편은 컴파일 타임의 마법을 다룹니다. #Preview와 @Observable 뒤에서 코드가 코드를 쓰는 장치, Swift 매크로입니다.
출처 및 확인 기준
- MemoryLayout — Apple Developer Documentation · 확인 2026-08-19 · 근거: size·alignment·stride의 API 정의
- Swift ABI Type Layout — Swift 프로젝트 · 공식 문서 · 확인 2026-08-19 · 근거: struct·enum·existential container 레이아웃
- SE-0260 Library Evolution — Swift Evolution · 확인 2026-08-19 · 근거: struct 필드 순서의 언어·ABI 보장 범위
- SE-0335 Existential any — Swift Evolution · 확인 2026-08-19 · 근거: existential의 3워드 인라인 버퍼와 동적 메모리 비용
- Swift HeapObject · Swift RefCount — Swift 런타임 · 확인 2026-08-19 · 근거: 네이티브 힙 객체 헤더와 인라인 참조 카운트

![[Swift 심화 #6] Swift 메모리 레이아웃, 프로퍼티 순서가 크기를 바꾼다 대표 이미지](/assets/images/posts/dfd50aa0-6496-4f00-a59f-21b04c18ed87/swift-memory-layout-size-stride-alignment.jpg)