Swift에서 구조체(Value Type) 기반 설계
Swift는 Value Semantics(값 의미론) 을 강력히 지원하는 언어입니다. 구조체(struct)는 값 타입의 대표로서 명확한 소유·복사 모델, ARC 부하 감소, 스레드 안전성 향상 등의 이점을 제공합니다. 이 문서는 iOS 실무 관점에서 “언제 struct를 우선 선택해야 하는지”와 “어떻게 설계해야 성능·안정성을 확보하는지”를 사례와 함께 정리합니다.
1. 핵심 요약
- 우선 선택: struct → 참조 공유가 필수이거나 상속이 필요할 때만 class 고려
- 이점: ARC 감소(참조 카운팅 없음), 불변(immutability) 지향, 예측 가능한 동작, 테스트 용이성, 동시성 친화성
- 주의: 큰 값 복사 비용, 참조 공유가 필요한 그래프 모델(예: 뷰 계층, NS* 프레임워크)에는 부적합
- 실무 팁: 불변 프로퍼티 + mutating 메서드, Codable/Equatable/Hashable 합성, Copy‑on‑Write(CoW)로 성능 최적화
2. 값 타입 vs 참조 타입: 동작 차이
| 구분 | 값 타입(struct, enum) | 참조 타입(class) |
| 할당/대입/파라미터 전달 | 복사(copy) | 참조(reference) 공유 |
| 동일성 | 값이 같으면 동일 의미 | 동일성(identity) 개념(===) |
| 메모리 관리 | ARC 대상 아님 | ARC(+ 순환 참조 위험) |
| 동시성 | 공유 없는 설계가 쉬움 | 공유 상태 보호(락, actor 등) 필요 |
| 상속 | 불가 | 가능 |
struct Point { var x = 0, y = 0 }
var a = Point(x: 1, y: 2)
var b = a // 복사
b.x = 10
print(a.x, b.x) // 1, 10 (독립)
3. 언제 struct를 우선 선택할까? (체크리스트)
- 모델이 작고 독립적이며 불변성을 지향
- 동등성(==) 만 중요하고 동일성(===) 이 중요하지 않음
- 상속이 불필요하고 프로토콜로 행위 조합 가능
- 스냅샷/히스토리(undo)/diff 등 복사 기반 흐름에 적합
- SwiftUI/Redux/TCA 등 상태 불변 아키텍처
UIKit/NS* 객체, Core Data NSManagedObject, AVFoundation 등은 클래스 기반이므로 래핑(wrap)하거나 그대로 사용합니다.
4. 불변(immutability) 설계와 mutating
값 타입은 불변을 기본값으로 잡고, 필요할 때만 명시적 변경 지점을 두는 게 가독성이 높습니다.
struct User {
let id: Int
var name: String
var point: Int
mutating func earn(_ p: Int) {
point += p
}
}
var u = User(id: 1, name: "Kim", point: 0)
u.earn(10) // 명시적 변경 지점
- let 인스턴스에서는 mutating 메서드 호출 불가 → 의도치 않은 변경 방지
- 값 타입은 변경 시 새 복사본이 생성되므로, 외부 공유 상태가 암묵적으로 변하지 않음
5. 프로토콜 합성과 합성적 설계
상속 대신 프로토콜 조합으로 행위를 첨가합니다.
struct Product: Identifiable, Codable, Equatable, Hashable {
let id: UUID
var title: String
var price: Decimal
}
- Swift는 위와 같은 단순 모델에 대해 합성 구현(Equatable/Hashable/Codable 자동 생성) 을 제공합니다.
- 컬렉션 키/집합 원소로 쓰려면 Hashable 채택이 유리합니다.
6. 성능: Copy‑on‑Write(CoW)와 큰 구조의 복사 비용
Swift 표준 컬렉션(Array/Dictionary/Set)은 CoW 를 사용합니다. 복사 시 버퍼를 공유하고, 수정 시점에만 실제 복사합니다.
var a = [1,2,3]
var b = a // 버퍼 공유
b.append(4) // 여기서 실제 복사 발생 (a, b 분리)
6.1 사용자 정의 타입에 CoW 적용하기
큰 내부 버퍼를 가진 값 타입이라면 래퍼 + private class 저장소로 CoW 구현을 고려합니다.
final class _Storage {
var buffer: [UInt8]
init(_ b: [UInt8]) { buffer = b }
func makeUnique() -> _Storage {
isKnownUniquelyReferenced(&self) ? self : _Storage(buffer)
}
}
struct Blob {
private var storage: _Storage
init(_ data: [UInt8]) { storage = _Storage(data) }
// 쓰기 전 고유화
private mutating func ensureUnique() {
storage = storage.makeUnique()
}
var count: Int { storage.buffer.count }
mutating func append(_ byte: UInt8) {
ensureUnique()
storage.buffer.append(byte)
}
}
- isKnownUniquelyReferenced로 참조가 유일할 때만 제자리 수정(in‑place) → 불필요한 복사 방지
- 읽기 전용 연산은 복사 없음
7. 스레드 안전성과 동시성 친화성
값 타입은 공유가 아닌 전달을 전제로 하므로, 데이터 레이스 위험이 낮습니다. Swift Concurrency(actors, Sendable)와도 잘 맞습니다.
struct Counter: Sendable {
private var value: Int = 0
// 값 타입 자체는 Sendable 자동 합성 가능 (모든 저장 프로퍼티가 Sendable이면)
}
- 클래스는 공유 가변 상태이므로 액터/락 보호가 필요합니다.
- 값 타입은 스냅샷 전달이 기본이므로 병렬 처리 시 안전한 경향이 큼.
8. 메모리/ARC 부하 감소
클래스는 생성·대입·소멸 시 retain/release 비용이 누적됩니다. 값 타입은 ARC 대상이 아니므로 핫 루프/대량 컬렉션에서 유리합니다.
// 단순 이벤트 로그 모델 → struct 배열 권장
struct Event: Codable, Hashable { let id: Int; let message: String }
let feed: [Event] = ...
물론 거대한 값복사는 비싸므로 CoW/참조 저장소 등으로 균형을 잡습니다.
9. SwiftUI/TCA와 값 의미론
SwiftUI의 View는 값 타입이며, 상태 모델 또한 불변 구조 + 의도적 변경 지점(Reducer/Action)으로 관리하는 패턴이 일반적입니다.
struct ProfileState: Equatable {
var name = ""
var age = 0
}
- 값 의미론 덕분에 diff 계산, 스냅샷 비교, 테스트가 단순해집니다.
10. Equatable/Hashable/Codable 설계 팁
- Equatable: 사용자 기준 동등성(예: id만 비교)을 명시적으로 정의
- Hashable: 동등성 기준과 일치하도록 해시 구성
- Codable: JSON 바인딩에 활용, 앱 경계(저장/네트워크)에서 불변 스냅샷 처리
struct User: Codable, Equatable, Hashable {
let id: Int
var name: String
static func == (l: Self, r: Self) -> Bool { l.id == r.id } // 사용자 정의 가능
func hash(into h: inout Hasher) { h.combine(id) }
}
11. API 표면 설계: inout, 비가변 + 새 인스턴스 반환
값 타입은 inout을 이용해 호출자 관점에서 명시적 수정 지점을 만들거나, 새 인스턴스 반환으로 불변성을 유지할 수 있습니다.
// inout 사용
func applyDiscount(_ rate: Double, to price: inout Decimal) {
price -= price * Decimal(rate)
}
// 불변 + 새 인스턴스 반환
extension User {
func renamed(_ new: String) -> User {
var copy = self
copy.name = new
return copy
}
}
12. 브리징 및 프레임워크 상호 운용
- struct ↔ NS* 클래스를 오갈 때 값 복사 비용을 고려
- Foundation의 많은 타입(String, Data, URL 등)은 값 의미론을 가지지만, 내부적으로 CoW + 클래스 저장소를 활용
let d1 = Data([0,1,2])
var d2 = d1 // CoW 공유
d2.append(3) // 이 시점에 복사
13. 테스트·디버깅에서의 장점
- 순수성: 입력 → 출력이 명확, 부수 효과가 적음
- 스냅샷 테스트: Codable + 정렬된 출력(JSON)으로 손쉬운 비교
- 디핑(Deep copy): 값 타입 자체가 스냅샷 역할
14. 안티 패턴과 교정
1) 무분별한 클래스 사용
- 교정: 상속/동일성/참조 공유가 정말 필요한지 점검. 아니면 struct.
2) 큰 struct에 자주 쓰기 변경
- 교정: CoW 저장소 도입, 변경 배치, inout로 국소 변경
3) 값 타입 내부에서 외부 참조를 강하게 보관
- 교정: 참조는 의미적으로 불변이어야 하며, 동시성/수명 주기를 명확히()
4) 동등성 규칙 불일치
- 교정: Equatable과 Hashable 규칙을 일치시키고 문서화
15. 실무 예제 모음
15.1 장바구니 모델(Value 타입 + 집계)
struct Line: Hashable, Codable {
let sku: String
var qty: Int
var price: Decimal
var amount: Decimal { price * Decimal(qty) }
}
struct Cart: Hashable, Codable {
private(set) var lines: [Line] = []
mutating func add(_ line: Line) {
if let i = lines.firstIndex(where: { $0.sku == line.sku }) {
lines[i].qty += line.qty
} else {
lines.append(line)
}
}
var total: Decimal { lines.reduce(0) { $0 + $1.amount } }
}
15.2 네트워크 응답 스냅샷
struct Feed: Codable, Equatable {
let items: [Item]
let nextCursor: String?
}
15.3 도메인 이벤트(불변 레코드)
struct Audit: Codable, Hashable {
let id: UUID
let actor: String
let action: String
let at: Date
}
16. 선택 가이드
| 질문 | struct 우선? |
| 상속이 필요한가? | 아니오 → struct |
| 동일성 추적이 필요한가? | 아니오 → struct |
| 외부 공유 없이 스냅샷/복사가 자연스러운가? | 예 → struct |
| 대량 컬렉션/핫 루프에서 ARC 부하가 문제인가? | 예 → struct |
| UIKit/NS*와 강결합인가? | 예 → class 또는 wrapper |
17. 참조 링크
- The Swift Programming Language — Structures and Classes
https://docs.swift.org/swift-book/documentation/the-swift-programming-language/classesandstructures/ - Structures — Methods / Mutating Methods
https://docs.swift.org/swift-book/documentation/the-swift-programming-language/methods/#Modifying-Value-Types-from-Within-Instance-Methods - Protocols / Generics / Standard Library (Codable, Hashable, Equatable)
https://docs.swift.org/swift-book/ - Swift Concurrency — Sendable, Actors
https://docs.swift.org/swift-book/documentation/the-swift-programming-language/concurrency/
18. 결론
- 기본값은 구조체(Value Type) 입니다.
- 상속·동일성·참조 공유가 필수일 때만 클래스를 선택하세요.
- 불변성·CoW·프로토콜 합성으로 가독성/성능/안정성을 모두 확보할 수 있습니다.
'Dev Study > Swift' 카테고리의 다른 글
| Swift에서 배열 초기화: 반복 append vs Array(repeating:count:) (0) | 2025.11.11 |
|---|---|
| Swift에서 문자열 비교 (0) | 2025.11.11 |
| Swift에서 ARC 최적화: weak vs unowned (0) | 2025.11.11 |
| Swift에서 @inlinable / @inline(__always) (0) | 2025.11.11 |
| Swift에서 switch문의 pattern matching (0) | 2025.11.11 |
| Swift에서 문자열 결합을 위해 joined(separator:) 사용하기 (0) | 2025.11.11 |
| Swift에서 guard let vs 중첩 if let (0) | 2025.11.11 |
| Swift에서 Set를 사용해서 포함(contains) 검사 (0) | 2025.11.11 |


