Swift 클래스와 구조체 | Class, Struct, Enum

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

특징ClassStruct
타입참조 타입값 타입
상속가능불가능
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 APIclass프레임워크가 class 요구
상속·다형성이 필수classstruct는 상속 불가
큰 데이터를 자주 복사하면 부담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. initdeinit 활용

  • 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, didSetoldValue입니다.


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 ↔ 클로저 순환을 끊습니다.


정리

핵심 요약

  1. Class: 참조 타입, 상속 가능
  2. Struct: 값 타입, 복사됨
  3. Enum: 연관 값, 메서드 가능
  4. mutating: Struct 메서드에서 수정
  5. 선택 기준: 공유·상속·UIKit 연동은 class, 대부분의 모델은 struct
  6. 다형성: 프로토콜 타입과 override로 실제 구현 분기
  7. init / deinit: 초기화 규칙과 해제 시 정리
  8. willSet / didSet: 값 변경 시 부가 로직
  9. ARC: weak·unowned로 순환 참조 방지

다음 단계

  • Swift 프로토콜
  • Swift 제네릭
  • Swift 에러 처리

관련 글

  • Rust 구조체와 열거형 | Struct, Enum, Pattern Matching
  • C++ Aggregate Initialization
  • C++ Aggregate Initialization 완벽 가이드 집합 초기화
  • C++ 클래스와 객체
  • C++ struct vs class