Swift 클래스와 구조체 | Class, Struct, Enum
이 글의 핵심
Swift 클래스와 구조체에 대한 실전 가이드입니다. Class, Struct, Enum 등을 예제와 함께 상세히 설명합니다.
들어가며
클래스는 참조 의미론(힙·공유), 구조체는 복사 의미론(스택 위주)에 가깝습니다. UI 상태·도메인 모델을 어디에 둘지 팀 규칙과 함께 고르는 것이 좋습니다.
1. 클래스 (Class)
기본 클래스
class Person {
var name: String
var age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
func introduce() {
print("안녕하세요, \(name)입니다.")
}
}
let person = Person(name: "홍길동", age: 25)
person.introduce()
참조 타입
class Counter {
var count = 0
func increment() {
count += 1
}
}
let counter1 = Counter()
let counter2 = counter1 // 같은 인스턴스 참조
counter1.increment()
print(counter2.count) // 1 (같은 객체)
상속
class Vehicle {
var speed: Int = 0
func describe() {
print("속도: \(speed)km/h")
}
}
class Car: Vehicle {
var brand: String
init(brand: String) {
self.brand = brand
super.init()
}
override func describe() {
print("\(brand) 자동차, 속도: \(speed)km/h")
}
}
let car = Car(brand: "현대")
car.speed = 100
car.describe()
2. 구조체 (Struct)
기본 구조체
struct Point {
var x: Int
var y: Int
mutating func moveBy(x: Int, y: Int) {
self.x += x
self.y += y
}
func distance() -> Double {
return sqrt(Double(x * x + y * y))
}
}
var point = Point(x: 0, y: 0)
point.moveBy(x: 10, y: 20)
print("(\(point.x), \(point.y))")
값 타입
struct Rectangle {
var width: Int
var height: Int
}
var rect1 = Rectangle(width: 10, height: 20)
var rect2 = rect1 // 복사됨
rect1.width = 30
print(rect1.width) // 30
print(rect2.width) // 10 (독립적)
계산 프로퍼티
struct Circle {
var radius: Double
var area: Double {
return Double.pi * radius * radius
}
var circumference: Double {
return 2 * Double.pi * radius
}
}
let circle = Circle(radius: 5)
print("넓이: \(circle.area)")
print("둘레: \(circle.circumference)")
3. 열거형 (Enum)
기본 열거형
enum Status {
case active
case inactive
case pending
}
var status = Status.active
switch status {
case .active:
print("활성")
case .inactive:
print("비활성")
case .pending:
print("대기")
}
연관 값
enum Result {
case success(String)
case failure(String)
}
func processResult(_ result: Result) {
switch result {
case .success(let message):
print("성공: \(message)")
case .failure(let error):
print("실패: \(error)")
}
}
processResult(.success("데이터 로드 완료"))
processResult(.failure("네트워크 에러"))
원시 값
enum Direction: Int {
case north = 0
case south = 1
case east = 2
case west = 3
}
let direction = Direction.north
print(direction.rawValue) // 0
if let dir = Direction(rawValue: 2) {
print(dir) // east
}
열거형 메서드
enum TrafficLight {
case red
case yellow
case green
func duration() -> Int {
switch self {
case .red: return 60
case .yellow: return 5
case .green: return 45
}
}
}
let light = TrafficLight.red
print("신호 시간: \(light.duration())초")
4. Class vs Struct
| 특징 | Class | Struct |
|---|---|---|
| 타입 | 참조 타입 | 값 타입 |
| 상속 | 가능 | 불가능 |
| Deinit | 있음 | 없음 |
| 복사 | 참조 복사 | 값 복사 |
| 사용 예 | 복잡한 객체 | 간단한 데이터 |
5. 실전 예제
예제: 은행 계좌 시스템
class BankAccount {
let accountNumber: String
private(set) var balance: Double
init(accountNumber: String, initialBalance: Double) {
self.accountNumber = accountNumber
self.balance = initialBalance
}
func deposit(_ amount: Double) {
guard amount > 0 else { return }
balance += amount
print("입금: \(amount)원, 잔액: \(balance)원")
}
func withdraw(_ amount: Double) -> Bool {
guard amount > 0 && amount <= balance else {
print("출금 실패")
return false
}
balance -= amount
print("출금: \(amount)원, 잔액: \(balance)원")
return true
}
}
struct Transaction {
let type: TransactionType
let amount: Double
let date: Date
enum TransactionType {
case deposit
case withdrawal
}
}
let account = BankAccount(accountNumber: "123-456", initialBalance: 10000)
account.deposit(5000)
account.withdraw(3000)
6. 클래스 vs 구조체 선택 기준 (참조 vs 값)
기본 원칙: 모델은 struct로 시작하고, 정말 필요할 때만 class를 씁니다.
| 상황 | 추천 | 이유 |
|---|---|---|
| 좌표, DTO, 설정 값 묶음 | struct | 복사가 안전하고, 스레드 간 공유 이슈가 적음 |
| 동일 인스턴스를 여러 곳에서 공유해야 함 | class | 참조가 하나로 유지됨 |
UIKit UIViewController 등 Cocoa API | class | 프레임워크가 class 요구 |
| 상속·다형성이 필수 | class | struct는 상속 불가 |
| 큰 데이터를 자주 복사하면 부담 | class 또는 struct + Copy-on-Write 타입 활용 | 배열·딕셔너리 등은 이미 COW |
값 타입: 대입·전달 시 복사되어 서로 독립적입니다. 참조 타입: 같은 객체를 가리키므로 한쪽 수정이 다른 참조에도 반영됩니다. SwiftUI에서 @State로 다루는 모델은 보통 값 타입(struct)에 두고, 공유 상태는 ObservableObject 등으로 참조를 관리합니다.
7. 상속과 다형성 실전 예제
프로토콜 타입으로 배열을 다루면 런타임에 실제 타입에 맞는 동작을 선택할 수 있습니다(다형성).
protocol Drawable {
func draw()
}
class CircleModel: Drawable {
var radius: Double
init(radius: Double) { self.radius = radius }
func draw() { print("원 그리기 r=\(radius)") }
}
class RectangleModel: Drawable {
var width: Double, height: Double
init(width: Double, height: Double) {
self.width = width
self.height = height
}
func draw() { print("사각형 그리기 \(width)x\(height)") }
}
func renderAll(_ items: [Drawable]) {
for item in items {
item.draw() // 실제 타입에 따라 다른 구현 호출
}
}
let shapes: [Drawable] = [CircleModel(radius: 3), RectangleModel(width: 2, height: 4)]
renderAll(shapes)
상속은 기본 동작 공유와 override에 쓰고, 역할(역할 기반 설계)은 프로토콜로 나누는 조합이 흔합니다.
8. init과 deinit 활용
init: 모든 저장 프로퍼티가 초기화된 뒤에만self사용 가능. 지정 이니셜라이저 체인(super.init등) 규칙을 지켜야 합니다.- 편의 이니셜라이저(
convenience init): 같은 클래스 안의 다른init을 호출해 중복 코드를 줄입니다. deinit: class 전용, 참조 카운트가 0이 될 때 호출. 파일 닫기, 타이머 해제,NotificationCenter구독 해제 등 정리 작업에 사용합니다.
class Resource {
let name: String
init(name: String) {
self.name = name
print("\(name) 열림")
}
convenience init?(named: String?) {
guard let n = named, !n.isEmpty else { return nil }
self.init(name: n)
}
deinit {
print("\(name) 정리(deinit)")
}
}
struct에는 deinit이 없습니다. 값이 스코프를 벗어나면 멤버가 순서대로 해제됩니다.
9. 프로퍼티 옵저버 (willSet, didSet)
저장 프로퍼티에 값이 바뀔 때 추가 로직을 넣을 수 있습니다. 초기화 직후 첫 할당에는 옵저버가 호출되지 않는 경우가 있어(특히 상속 초기화 흐름), UI 동기화·로깅·유효성 검사에 많이 씁니다.
struct Temperature {
var celsius: Double {
willSet {
print("곧 \(celsius) → \(newValue)")
}
didSet {
if celsius < -273.15 {
celsius = -273.15 // 한 번 더 옵저버 호출 후 안정화
}
}
}
}
willSet의 암시 매개변수 이름은 newValue, didSet은 oldValue입니다.
10. 메모리 관리 (ARC, weak, unowned)
Swift는 ARC(Automatic Reference Counting)로 참조 횟수를 관리합니다. 강한 참조 순환이 있으면 두 인스턴스가 서로를 잡고 있어 해제되지 않을 수 있습니다.
weak: 옵셔널 참조, 카운트에 포함하지 않음. 상대가 먼저 없어지면 자동으로nil. 델리게이트, 클로저 캡처, 부모–자식 중 한쪽만 소유할 때 자주 사용합니다.unowned: 약한 참조이지만 옵션이 아님. 상대 수명이 항상 자기보다 길거나 같다고 보장될 때만 사용. 잘못 쓰면 댕글링 참조로 크래시 가능.
class Parent {
var child: Child?
deinit { print("Parent deinit") }
}
class Child {
weak var parent: Parent?
init(parent: Parent) {
self.parent = parent
}
deinit { print("Child deinit") }
}
클로저에서는 [weak self] 또는 [unowned self]로 캡처해 self ↔ 클로저 순환을 끊습니다.
정리
핵심 요약
- Class: 참조 타입, 상속 가능
- Struct: 값 타입, 복사됨
- Enum: 연관 값, 메서드 가능
- mutating: Struct 메서드에서 수정
- 선택 기준: 공유·상속·UIKit 연동은 class, 대부분의 모델은 struct
- 다형성: 프로토콜 타입과
override로 실제 구현 분기 - init / deinit: 초기화 규칙과 해제 시 정리
- willSet / didSet: 값 변경 시 부가 로직
- ARC:
weak·unowned로 순환 참조 방지
다음 단계
- Swift 프로토콜
- Swift 제네릭
- Swift 에러 처리
관련 글
- Rust 구조체와 열거형 | Struct, Enum, Pattern Matching
- C++ Aggregate Initialization
- C++ Aggregate Initialization 완벽 가이드 집합 초기화
- C++ 클래스와 객체
- C++ struct vs class