Swift에서 Actor vs Class: 언제 무엇을 사용해야 할까?

윤지하·2025년 8월 14일
0

swift

목록 보기
2/9

들어가며

Swift 5.5에서 도입된 Actor는 동시성 프로그래밍을 위한 강력한 도구입니다. 하지만 모든 상황에서 Actor가 정답은 아닙니다. 이 글에서는 Actor와 Class의 차이점을 알아보고, 각각을 언제 사용해야 하는지 실용적인 관점에서 살펴보겠습니다.

Actor와 Class의 핵심 차이점

1. 동시성 안전성 (Concurrency Safety)

Actor의 특징:

  • 자동으로 데이터 경쟁(data race)을 방지합니다
  • 한 번에 하나의 작업만 내부 상태에 접근할 수 있도록 보장합니다
  • Swift의 동시성 모델에 내장된 스레드 안전성을 제공합니다

Class의 특징:

  • 기본적으로 스레드 안전하지 않습니다
  • 여러 스레드가 동시에 접근할 수 있어 데이터 경쟁이 발생할 수 있습니다
  • 필요시 수동으로 동기화 메커니즘(lock, semaphore 등)을 구현해야 합니다

2. 접근 방식의 차이

Actor를 사용할 때는 await 키워드가 필수입니다:

// Actor 예제
actor BankAccount {
    private var balance = 0
    
    func deposit(amount: Int) {
        balance += amount
    }
    
    func getBalance() -> Int {
        return balance
    }
}

// 사용 시
let account = BankAccount()
await account.deposit(amount: 100)  // ⚠️ await 필요
let balance = await account.getBalance()

Class는 동기적으로 바로 접근 가능합니다:

// Class 예제
class BankAccount {
    private var balance = 0
    
    func deposit(amount: Int) {
        balance += amount
    }
    
    func getBalance() -> Int {
        return balance
    }
}

// 사용 시
let account = BankAccount()
account.deposit(amount: 100)  // ✅ 직접 호출
let balance = account.getBalance()

공통점과 차이점 정리

공통점

  • 둘 다 참조 타입(Reference Type)
  • 상속 가능
  • deinit 사용 가능
  • 프로퍼티와 메서드 보유 가능

Actor만의 고유 특징

  • 격리(isolation)를 통한 동시성 안전성 보장
  • 외부 접근 시 await 키워드 필수
  • nonisolated 키워드로 특정 멤버를 격리에서 제외 가능
  • @MainActor 같은 글로벌 액터 지원

Class만의 고유 특징

  • 동기적 즉시 접근 가능
  • Objective-C와의 상호 운용성
  • NSObject 상속 가능
  • UIKit/AppKit 등 기존 프레임워크와 자연스러운 통합

실전 가이드: 언제 무엇을 사용할까?

Actor를 사용해야 할 때 ✅

1. 여러 Task에서 동시 접근이 필요한 경우

actor ImageCache {
    private var cache: [URL: UIImage] = [:]
    
    func store(_ image: UIImage, for url: URL) {
        cache[url] = image
    }
    
    func retrieve(for url: URL) -> UIImage? {
        return cache[url]
    }
}

// 여러 곳에서 동시에 안전하게 접근
Task { await imageCache.store(image1, for: url1) }
Task { await imageCache.store(image2, for: url2) }

2. 공유 리소스를 관리할 때

actor DatabaseConnection {
    private var connection: SQLiteConnection?
    
    func execute(query: String) async throws -> [Row] {
        // 여러 스레드에서 동시 접근해도 안전
        return try await connection?.execute(query) ?? []
    }
}

3. 백그라운드 작업과 UI 업데이트가 공존할 때

actor DataProcessor {
    private var processedData: [ProcessedItem] = []
    
    func processInBackground(_ rawData: RawData) async {
        // 무거운 처리 작업
        let processed = await heavyProcessing(rawData)
        processedData.append(processed)
    }
    
    func getProcessedData() -> [ProcessedItem] {
        return processedData
    }
}

Class를 사용해야 할 때 ✅

1. 단순한 뷰 모델이나 데이터 모델

class UserProfileViewModel {
    var name: String = ""
    var age: Int = 0
    var email: String = ""
    
    func updateProfile(name: String, age: Int, email: String) {
        self.name = name
        self.age = age
        self.email = email
    }
}

2. UIKit/AppKit 컴포넌트와 통합

class CustomViewController: UIViewController {
    // UIKit은 Class 기반으로 설계됨
    override func viewDidLoad() {
        super.viewDidLoad()
        setupUI()
    }
}

3. 동시성이 필요 없는 일반적인 비즈니스 로직

class ShoppingCart {
    private var items: [Product] = []
    
    func addItem(_ product: Product) {
        items.append(product)
    }
    
    func calculateTotal() -> Double {
        return items.reduce(0) { $0 + $1.price }
    }
}

흔한 실수: 과도한 Actor 사용 ⚠️

❌ 나쁜 예: 불필요한 Actor 사용

// 동시성이 필요 없는데 Actor를 사용
actor SimpleCounter {
    var count = 0
    
    func increment() {
        count += 1
    }
}

// 사용할 때마다 불필요한 await
let counter = SimpleCounter()
await counter.increment()  // 왜 기다려야 하지?

✅ 좋은 예: 적절한 Class 사용

// 단순한 카운터는 Class로 충분
class SimpleCounter {
    var count = 0
    
    func increment() {
        count += 1
    }
}

// 깔끔하고 직관적인 사용
let counter = SimpleCounter()
counter.increment()  // 바로 실행!

Actor 사용의 단점 (단일 스레드 환경)

  1. 불필요한 비동기 처리: 모든 접근이 비동기가 되어 코드가 복잡해집니다
  2. 코드 복잡도 증가: await, Task 등의 추가 코드가 필요합니다
  3. 성능 오버헤드: 필요 없는 동기화 메커니즘으로 인한 성능 저하
  4. 디버깅 어려움: 비동기 코드는 동기 코드보다 디버깅이 어렵습니다

실용적인 결정 가이드 📋

Actor를 선택하는 명확한 신호들:

  • ✅ 여러 Task에서 동시 접근이 발생
  • ✅ 백그라운드 스레드와 메인 스레드 간 데이터 공유
  • ✅ 네트워크 요청 결과를 캐싱하는 매니저
  • ✅ 데이터베이스 연결 풀 관리
  • ✅ 파일 시스템 접근 조정

Class를 선택해야 할 때:

  • ✅ 단일 스레드에서만 사용
  • ✅ UI 컴포넌트와 직접 상호작용
  • ✅ 동기적 접근이 자연스러운 경우
  • ✅ 레거시 코드와의 통합
  • ✅ 간단한 데이터 모델

YAGNI 원칙을 기억하세요

"You Aren't Gonna Need It" - 미래에 필요할 수도 있다는 이유로 복잡한 설계를 미리 하지 마세요.

// ❌ 과도한 설계
// "나중에 멀티스레드가 필요할 수도 있으니까..."
actor UserSettings {
    var theme: Theme = .light
    var fontSize: Int = 14
}

// ✅ 적절한 설계
// 지금은 Class로 충분. 나중에 필요하면 리팩토링
class UserSettings {
    var theme: Theme = .light
    var fontSize: Int = 14
}

Swift의 강력한 타입 시스템 덕분에 Class를 Actor로 변경할 때 컴파일러가 수정이 필요한 모든 곳을 알려줍니다. 따라서 처음부터 과도하게 설계할 필요가 없습니다.

마무리

Actor는 Swift의 동시성 프로그래밍을 위한 훌륭한 도구이지만, 만능 해결책은 아닙니다. 각 상황에 맞는 적절한 도구를 선택하는 것이 중요합니다.

핵심 요약:

  • 동시성이 필요하다 → Actor 사용
  • 동시성이 필요 없다 → Class 사용
  • 확실하지 않다 → Class로 시작하고 필요시 리팩토링

올바른 도구를 선택하여 더 깔끔하고 유지보수하기 쉬운 코드를 작성하세요! 🚀


이 글이 도움이 되셨다면, 팀원들과 공유해주세요. Swift 동시성에 대한 더 많은 내용은 Apple의 공식 문서를 참고하세요.

profile
성장하고 싶은 개발자

0개의 댓글