반응형

Swift 6 컴파일러는 기본 설정으로 완전한 동시성 검사(Complete Concurrency Checking)를 강제합니다.
기존 Swift 5 스타일의 비동기 코드나 DispatchQueue.main.async에 의존하던 로직은 컴파일 시점에 Call to main actor-isolated instance method in a synchronous nonisolated context 또는 Capture of non-sendable type in a concurrent closure 에러를 발생시킵니다. 데이터 레이스(Data Race) 문제를 해결하는 실무 예제와 패턴을 정리합니다.

 

1. MainActor 격리(Isolation) 및 UI 업데이트 실무 패턴

Swift 6에서는 UI 렌더링에 관여하는 모든 타입과 함수가 메인 스레드에서 안전하게 실행됨을 보장해야 합니다.

 

@MainActor 클래스 및 ViewModel 격리

ObservableObject 또는 @Observable 마크업을 사용하는 ViewModel 클래스 전체에 @MainActor를 선언하면 내부 상태 변경이 메인 스레드에서 캡슐화됩니다.

import SwiftUI

@MainActor
final class UserProfileViewModel: ObservableObject {
    @Published private(set) var userName: String = ""
    @Published private(set) var isLoading: Bool = false
    
    private let networkService: NetworkServiceProtocol
    
    init(networkService: NetworkServiceProtocol) {
        self.networkService = networkService
    }
    
    func fetchUserProfile() async {
        isLoading = true
        defer { isLoading = false }
        
        do {
            let profile = try await networkService.loadProfile()
            self.userName = profile.name
        } catch {
            print("프로필 로드 실패: \(error)")
        }
    }
}

nonisolated 키워드 활용

@MainActor가 지정된 클래스 내부에서도 UI 업데이트와 상관없는 무거운 순수 연산 함수는 nonisolated 키워드를 붙여 메인 스레드 병목 현상을 방지합니다.

@MainActor
final class DataProcessor {
    // UI 업데이트용 프로퍼티
    var statusText: String = ""
    
    // 메인 스레드를 블로킹하지 않는 순수 대용량 데이터 계산 함수
    nonisolated func processLargeDataSet(_ data: [Int]) -> Int {
        return data.reduce(0, +)
    }
}

 

2. Sendable 프로토콜과 동시성 상태 전달 규칙

Sendable 프로토콜은 서로 다른 스레드/태스크 간에 안전하게 전달될 수 있는 타입임을 컴파일러에 증명하는 키워드입니다.

 

Sendable 적용 기준 비교

구분 Struct / Enum Class Actor
Sendable 자동 채택 내부 프로퍼티가 모두 Sendable인 경우 자동 자동 채택 안 됨 (명시 필요) 기본적으로 Sendable 준수
가변 상태 (Mutable State) 불변 구조체 및 값 타입 안전 불변(final & let) 전제하에만 제약적 허용 internal state 보호를 통해 안전
권장 가이드 DTO, 모델 객체는 값 타입 적극 활용 불필요한 레퍼런스 타입 최소화 상태 관리는 Class 대신 Actor 사용

 

클래스에 @unchecked Sendable 적용 시 주의사항

내부적으로 커스텀 락(NSLock 등)을 사용하여 스레드 안전성을 스스로 보장하는 경우에만 @unchecked Sendable을 채택합니다.

// 커스텀 스레드 세이프 클래스 예시
final class CustomSafeCache: @unchecked Sendable {
    private var storage: [String: Any] = [:]
    private let lock = NSLock()

    func getValue(forKey key: String) -> Any? {
        lock.lock()
        defer { lock.unlock() }
        return storage[key]
    }
}

 

3. 기존 콜백 기반 코드를 async/await로 마이그레이션

기존 Completion Handler 및 DispatchQueue 기반 비동기 코드는 withCheckedThrowingContinuation을 사용해 Swift Concurrency로 안전하게 전환할 수 있습니다.

func fetchLegacyData() async throws -> Data {
    try await withCheckedThrowingContinuation { continuation in
        legacySDKFetch { result, error in
            if let error = error {
                continuation.resume(throwing: error)
            } else if let data = result {
                continuation.resume(returning: data)
            } else {
                continuation.resume(throwing: URLError(.badServerResponse))
            }
        }
    }
}

 

요약

Swift 6 데이터 레이스 에러는 UI 조작부에 @MainActor를 명시하고, 태스크 간 전송되는 모델 데이터에 Sendable 프로토콜을 채택함으로써 컴파일 시점에 스레드 안전성을 확보할 수 있습니다.

반응형
Posted by 까칠코더
,