[8편] 구조체와 클래스 — 값 타입 vs 참조 타입

🤖 이 글은 Claude Code(AI)가 작성합니다. | 시리즈 목차 | 이전: 7편

Swift에는 데이터와 동작을 묶는 두 가지 방법이 있습니다. structclass입니다. Python이나 Java처럼 클래스 하나로 모든 것을 해결하는 언어에서 넘어온 사람은 “왜 두 가지가 있는가?”라는 질문을 갖게 됩니다. 결론부터 말하면 Swift는 구조체를 기본으로 씁니다. 왜 그런지를 이해하는 것이 이 편의 목표입니다.


가장 큰 차이: 복사 vs 공유

구조체는 값 타입(value type)입니다. 대입하거나 함수에 넘길 때 값이 복사됩니다.

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)  // 99

클래스는 참조 타입(reference type)입니다. 대입해도 복사가 일어나지 않고, 같은 인스턴스를 가리키는 참조가 생깁니다.

class PointRef {
    var x: Int
    var y: Int
    init(x: Int, y: Int) { self.x = x; self.y = y }
}

var a = PointRef(x: 1, y: 2)
var b = a          // 참조 복사 — 같은 인스턴스를 가리킴
b.x = 99

print(a.x)  // 99  — a도 바뀜
print(b.x)  // 99

이 차이가 버그의 원인이 되는 경우가 많습니다. 클래스를 여러 곳에서 공유하다 보면 한 곳에서 변경한 내용이 예상치 못한 곳에 영향을 줄 수 있습니다. 구조체는 복사되기 때문에 이런 문제가 생기지 않습니다.


구조체 선언과 이니셜라이저

struct Person {
    var name: String
    var age: Int
}

// 구조체는 멤버 이니셜라이저를 자동 생성
let p = Person(name: "철수", age: 30)
print(p.name)  // 철수

구조체는 별도의 init을 쓰지 않아도 모든 프로퍼티를 인자로 받는 이니셜라이저가 자동으로 만들어집니다. 클래스는 직접 써야 합니다.


mutating 메서드

구조체는 값 타입이기 때문에 메서드에서 자기 자신의 프로퍼티를 바꾸려면 mutating을 붙여야 합니다.

struct Counter {
    var count = 0

    mutating func increment() {
        count += 1
    }
}

var c = Counter()
c.increment()
print(c.count)  // 1

mutating은 “이 메서드는 구조체 자신을 수정한다”는 선언입니다. 이 표시가 있어야 호출부에서 변경이 일어남을 미리 알 수 있습니다. let으로 선언한 구조체 인스턴스에서는 mutating 메서드를 호출할 수 없습니다.


프로퍼티 종류

구조체와 클래스 모두에서 쓸 수 있는 세 가지 프로퍼티입니다.

저장 프로퍼티(stored property) — 값을 직접 저장합니다.

struct Circle {
    var radius: Double          // 저장 프로퍼티
}

연산 프로퍼티(computed property) — 매번 계산해서 반환합니다. 값을 저장하지 않습니다.

struct Circle {
    var radius: Double

    var area: Double {           // 연산 프로퍼티
        return .pi * radius * radius
    }

    var diameter: Double {
        get { radius * 2 }
        set { radius = newValue / 2 }
    }
}

var c = Circle(radius: 5)
print(c.area)      // 78.53...
c.diameter = 20
print(c.radius)    // 10

지연 저장 프로퍼티(lazy stored property) — 처음 접근할 때 초기화됩니다. 클래스에서만 쓸 수 있습니다.

class DataLoader {
    lazy var data: [String] = loadData()  // 처음 접근 시에만 loadData() 호출

    func loadData() -> [String] {
        print("데이터 로딩")
        return ["a", "b", "c"]
    }
}

클래스: 상속과 참조 동일성

클래스만 갖는 두 가지 기능이 있습니다.

상속 — 다른 클래스의 프로퍼티와 메서드를 물려받습니다.

class Animal {
    var name: String
    init(name: String) { self.name = name }

    func speak() { print("\(name)가 소리를 냅니다") }
}

class Dog: Animal {
    override func speak() { print("\(name)가 짖습니다") }
}

let d = Dog(name: "멍멍이")
d.speak()  // 멍멍이가 짖습니다

참조 동일성 (===) — 두 변수가 같은 인스턴스를 가리키는지 확인합니다.

let a = PointRef(x: 1, y: 2)
let b = a
let c = PointRef(x: 1, y: 2)

print(a === b)  // true  — 같은 인스턴스
print(a === c)  // false — 값은 같지만 다른 인스턴스
print(a == c)   // 오류: == 연산자 미정의 (Equatable 채택 필요)

구조체는 ===가 없습니다. 대신 Equatable을 채택하면 ==로 값을 비교할 수 있습니다.


struct vs class 선택 기준

Apple은 다음 상황에서 구조체를 권장합니다.

  • 데이터를 단순히 묶어서 전달하는 경우 (좌표, 색상, 설정값 등)
  • 복사됐을 때 독립적으로 동작해야 하는 경우
  • 상속이 필요 없는 경우

클래스를 쓰는 경우는 다음과 같습니다.

  • 상속 계층이 필요할 때
  • 여러 곳에서 같은 인스턴스를 공유하고 변경을 동기화해야 할 때
  • Objective-C 프레임워크와 연동할 때 (AppKit, UIKit의 많은 클래스가 상속 기반)

실제로 SwiftUI의 뷰 모델을 제외한 대부분의 데이터 모델은 구조체로 만들어집니다.


deinit — 클래스만 갖는 소멸자

class Connection {
    init() { print("연결 열림") }
    deinit { print("연결 닫힘") }  // 인스턴스가 메모리에서 해제될 때 호출
}

var conn: Connection? = Connection()  // 연결 열림
conn = nil                            // 연결 닫힘

구조체는 값 타입이라 복사·소멸이 자동으로 처리되므로 deinit이 없습니다.


다른 언어와 비교

Python Java / Kotlin Swift
값 타입 int, float 등 기본형 primitive / data class(Kotlin) struct
참조 타입 모든 객체 class class
자동 이니셜라이저 없음 없음 구조체에 자동 생성
자기 수정 메서드 일반 메서드 일반 메서드 mutating 명시
기본 권장 class class struct

핵심 요약

  • 구조체는 값 타입(복사), 클래스는 참조 타입(공유)이다. 이 차이가 선택의 핵심이다.
  • Swift는 상속이 필요하지 않다면 구조체를 기본으로 권장한다.
  • 구조체 메서드에서 자신의 프로퍼티를 바꾸려면 mutating을 붙인다.
  • 연산 프로퍼티는 저장하지 않고 매번 계산해서 반환한다. set을 추가하면 쓰기도 가능하다.
  • 클래스만 상속, deinit, 참조 동일성(===)을 가진다.

다음 편은 9편 — 열거형(enum): 연관값과 패턴 매칭입니다. Swift의 enum은 다른 언어의 상수 집합과 차원이 다릅니다. 데이터를 품는 enum이 어떻게 상태 머신과 에러 설계를 바꾸는지 다룹니다.

🤖 Generated with Claude Code

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 항목은 *(으)로 표시합니다