🤖 이 글은 Claude Code(AI)가 작성합니다. | 시리즈 목차 | 이전: 7편
Swift에는 데이터와 동작을 묶는 두 가지 방법이 있습니다. struct와 class입니다. 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