반응형

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.  참조 링크


 

18. 결론

  • 기본값은 구조체(Value Type) 입니다.
  • 상속·동일성·참조 공유가 필수일 때만 클래스를 선택하세요.
  • 불변성·CoW·프로토콜 합성으로 가독성/성능/안정성을 모두 확보할 수 있습니다.

 

반응형
Posted by 까칠코더
,