-
Swift 동시성 모델Apple🍎/Swift 2025. 4. 18. 20:14
실행 컨텍스트 (Execution Context)
실행 컨텍스트는 코드가 실행되는 독립적인 환경을 의미합니다. Swift의 동시성 모델에서 각 컨텍스트는 자체적인 격리 특성을 가지고 있으며, 이를 통해 안전한 동시성을 구현합니다.
주요 실행 컨텍스트 유형
- 메인 액터 컨텍스트 (Main Actor Context)
- UI 작업을 위한 특별한 컨텍스트
- 항상 메인 스레드에서 실행됨
- @MainActor 속성이나 MainActor.run { } 블록 내부 코드가 이 컨텍스트에서 실행됨
- 액터 컨텍스트 (Actor Context)
- 각 액터 인스턴스마다 고유한 컨텍스트 존재
- 액터 내부의 메서드와 프로퍼티에 접근할 때 활성화됨
- 자체 직렬 큐를 통해 상태 접근을 동기화하여 데이터 레이스 방지
- 분리된 태스크 컨텍스트 (Detached Task Context)
- Task.detached { } 블록으로 생성된 독립적인 태스크의 실행 환경
- 부모 태스크로부터 상속받는 컨텍스트 정보가 없음
- 구조화된 태스크 컨텍스트 (Structured Task Context)
- Task { } 블록으로 생성된 실행 환경
- 생성된 위치의 액터 컨텍스트나 다른 정보를 상속받음
- 비액터 컨텍스트 (Non-actor Context)
- 어떤 액터에도 속하지 않은 일반 코드 실행 환경
- 전역 함수, 일반 클래스의 메서드 등이 여기에 해당
격리(Isolation)의 의미
각 컨텍스트는 자체적인 격리 특성을 가지며, 이는 해당 컨텍스트 내에서 작업이 순차적으로 처리된다는 것을 의미합니다. 예를 들어, 액터 컨텍스트는 내부적으로 직렬 큐를 사용하여 액터의 상태에 대한 접근을 동기화합니다. 이를 통해 여러 태스크가 동시에 액터의 상태를 변경하려 할 때 데이터 레이스를 방지합니다.
- 액터 격리(Actor isolation)
- 각 액터는 자체 직렬 큐(serial queue)를 가지고 있습니다.
- 액터의 모든 상태(프로퍼티) 접근과 메서드 호출은 이 큐에서 순차적으로 처리됩니다.
- 이를 통해 여러 태스크가 동시에 액터의 상태를 변경하려 할 때 데이터 레이스를 방지합니다.
- 메인 액터 격리(Main Actor isolation)
- 메인 액터는 메인 스레드의 직렬 큐를 사용합니다.
- 메인 액터에 격리된 모든 코드는 메인 스레드에서 순차적으로 실행됩니다.
- UI 관련 작업을 안전하게 처리할 수 있게 해줍니다.
- 태스크 격리(Task isolation)
- 각 태스크는 자체 실행 컨텍스트를 가집니다.
- 태스크 내의 작업은 논리적으로 순차적으로 실행됩니다(비동기 지점에서 중단될 수 있지만).
이러한 격리는 내부적으로 GCD(Grand Central Dispatch)나 다른 저수준 동시성 메커니즘을 사용하여 구현됩니다. 실제로는 직렬 큐, 작업 스케줄링, 우선순위 관리 등이 Swift 런타임에 의해 처리됩니다.
비동기 경계 (Async Boundary)
비동기 경계는 한 실행 컨텍스트에서 다른 실행 컨텍스트로 전환되는 지점을 의미합니다. 이 지점에서는 비동기적 중단과 재개가 발생합니다.
비동기 경계의 특징
- 실행 중단점(Suspension Point)
- await 키워드가 있는 곳은 함수의 실행이 일시적으로 중단될 수 있는 지점
- 현재 스레드는 다른 작업을 수행할 수 있고, 결과가 준비되면 실행이 재개됨
- 컨텍스트 전환
- 한 실행 컨텍스트에서 다른 실행 컨텍스트로 전환됨
- 예: 비액터 컨텍스트 → 액터 컨텍스트, 액터 A → 액터 B
- 비동기적 대기
- 호출부는 결과를 기다리며 일시 중단됨
- 시스템은 그 동안 해당 스레드를 다른 작업에 활용할 수 있음
코드에서 비동기 경계가 나타나는 형태
- await 키워드 사용 지점
- 액터 메서드나 프로퍼티 접근 (다른 액터 컨텍스트에서 접근할 때)
- MainActor.run { } 호출
- Task { } 또는 Task.detached { } 생성 지점
Swift 동시성에서의 컨텍스트 전환 예시
1. 비액터 컨텍스트 → 액터 컨텍스트
// 비액터 컨텍스트에서 시작 func fetchUserProfile(id: String) async throws -> UserProfile { print("시작: 비액터 컨텍스트") // 비액터 컨텍스트 → UserManager 액터 컨텍스트로 전환 let user = await UserManager.shared.getUser(id: id) // UserManager 액터 컨텍스트 → 비액터 컨텍스트로 돌아옴 print("사용자 정보 받음: 비액터 컨텍스트") // 비액터 컨텍스트 → ProfileService 액터 컨텍스트로 전환 let profile = await ProfileService.shared.getProfile(for: user) // ProfileService 액터 컨텍스트 → 비액터 컨텍스트로 돌아옴 print("프로필 정보 받음: 비액터 컨텍스트") return profile } // 액터 정의 actor UserManager { static let shared = UserManager() private var users: [String: User] = [:] // UserManager 액터 컨텍스트에서 실행 func getUser(id: String) -> User { print(" UserManager 액터 컨텍스트에서 사용자 조회 중") return users[id] ?? User(id: id, name: "Unknown") } } actor ProfileService { static let shared = ProfileService() // ProfileService 액터 컨텍스트에서 실행 func getProfile(for user: User) -> UserProfile { print(" ProfileService 액터 컨텍스트에서 프로필 생성 중") return UserProfile(user: user, bio: "Bio for \(user.name)") } }
2. 액터 컨텍스트 → 다른 액터 컨텍스트
actor DataManager { // DataManager 액터 컨텍스트 func processData(id: String) async { print("DataManager 액터 컨텍스트: 데이터 처리 시작") // DataManager 액터 컨텍스트 → StorageManager 액터 컨텍스트로 전환 let rawData = await StorageManager.shared.fetchData(id: id) // StorageManager 액터 컨텍스트 → DataManager 액터 컨텍스트로 돌아옴 print("DataManager 액터 컨텍스트: 원시 데이터 받음") // DataManager 액터 컨텍스트 → AnalyticsManager 액터 컨텍스트로 전환 await AnalyticsManager.shared.logDataAccess(dataId: id) // AnalyticsManager 액터 컨텍스트 → DataManager 액터 컨텍스트로 돌아옴 print("DataManager 액터 컨텍스트: 데이터 처리 완료") } } actor StorageManager { static let shared = StorageManager() // StorageManager 액터 컨텍스트에서 실행 func fetchData(id: String) -> Data { print(" StorageManager 액터 컨텍스트: 데이터 조회 중") return Data() // 간략화된 예시 } } actor AnalyticsManager { static let shared = AnalyticsManager() // AnalyticsManager 액터 컨텍스트에서 실행 func logDataAccess(dataId: String) { print(" AnalyticsManager 액터 컨텍스트: 접근 로깅 중") } }
3. 비액터 컨텍스트 → 메인 액터 컨텍스트
// 비액터 컨텍스트에서 시작 func loadAndDisplayData() async { print("비액터 컨텍스트: 데이터 로딩 시작") // 비액터 컨텍스트에서 데이터 로드 let data = await fetchDataFromNetwork() print("비액터 컨텍스트: 네트워크 데이터 로드 완료") // 비액터 컨텍스트 → 메인 액터 컨텍스트로 전환 await MainActor.run { print(" 메인 액터 컨텍스트: UI 업데이트 시작") updateUI(with: data) print(" 메인 액터 컨텍스트: UI 업데이트 완료") } // 메인 액터 컨텍스트 → 비액터 컨텍스트로 돌아옴 print("비액터 컨텍스트: 작업 완료") } // 비액터 컨텍스트에서 실행 func fetchDataFromNetwork() async -> [String] { print(" 비액터 컨텍스트: 네트워크 요청 수행 중") return ["Item 1", "Item 2", "Item 3"] } // 메인 액터 컨텍스트에서만 호출 가능 @MainActor func updateUI(with data: [String]) { print(" 메인 액터 컨텍스트: UI 컴포넌트 업데이트 중") }
4. 메인 액터 컨텍스트 → 비액터 컨텍스트
// 메인 액터 컨텍스트에서 시작 @MainActor func handleButtonTap() async { print("메인 액터 컨텍스트: 버튼 탭 처리 시작") // UI 관련 작업 showLoadingIndicator() print("메인 액터 컨텍스트: 로딩 인디케이터 표시") // 메인 액터 컨텍스트 → 비액터 컨텍스트로 전환 let result = await performHeavyComputation() // 비액터 컨텍스트 → 메인 액터 컨텍스트로 돌아옴 print("메인 액터 컨텍스트: 계산 결과 받음") // 메인 액터 컨텍스트에서 UI 업데이트 updateResultView(with: result) hideLoadingIndicator() print("메인 액터 컨텍스트: 작업 완료") } // 메인 액터 컨텍스트에서 실행 @MainActor func showLoadingIndicator() { print(" 메인 액터 컨텍스트: 로딩 인디케이터 표시 중") } // 메인 액터 컨텍스트에서 실행 @MainActor func hideLoadingIndicator() { print(" 메인 액터 컨텍스트: 로딩 인디케이터 숨김") } // 메인 액터 컨텍스트에서 실행 @MainActor func updateResultView(with result: Int) { print(" 메인 액터 컨텍스트: 결과 뷰 업데이트 중") } // 비액터 컨텍스트에서 실행 (무거운 계산은 메인 스레드 외부에서 수행) func performHeavyComputation() async -> Int { print(" 비액터 컨텍스트: 무거운 계산 수행 중") // 시간이 걸리는 작업 시뮬레이션 await Task.sleep(for: .milliseconds(100)) return 42 }
5. 구조화된 태스크로의 전환
// 비액터 컨텍스트에서 시작 func processMultipleItems() async { print("비액터 컨텍스트: 다중 아이템 처리 시작") // 비액터 컨텍스트 → 구조화된 태스크 컨텍스트로 전환 let task = Task { print(" 구조화된 태스크 컨텍스트: 태스크 시작") // 구조화된 태스크 내에서 작업 수행 let items = ["A", "B", "C"] var results: [String] = [] for item in items { // 구조화된 태스크 컨텍스트 → 액터 컨텍스트로 전환 let processed = await ItemProcessor.shared.process(item: item) // 액터 컨텍스트 → 구조화된 태스크 컨텍스트로 돌아옴 results.append(processed) } print(" 구조화된 태스크 컨텍스트: 모든 아이템 처리 완료") return results } // 비액터 컨텍스트 → 구조화된 태스크 컨텍스트의 결과 대기 let results = await task.value // 구조화된 태스크 컨텍스트 → 비액터 컨텍스트로 돌아옴 print("비액터 컨텍스트: 결과 받음 - \(results)") } actor ItemProcessor { static let shared = ItemProcessor() // ItemProcessor 액터 컨텍스트에서 실행 func process(item: String) -> String { print(" ItemProcessor 액터 컨텍스트: 아이템 \(item) 처리 중") return "Processed \(item)" } }
6. 분리된 태스크로의 전환
// 메인 액터 컨텍스트에서 시작 @MainActor func startBackgroundWork() { print("메인 액터 컨텍스트: 백그라운드 작업 시작") // 메인 액터 컨텍스트 → 분리된 태스크 컨텍스트로 전환 let task = Task.detached { print(" 분리된 태스크 컨텍스트: 백그라운드 작업 실행 중") // 분리된 태스크 컨텍스트 내에서 작업 수행 let result = await performLongRunningOperation() // 분리된 태스크 컨텍스트 → 메인 액터 컨텍스트로 전환 await MainActor.run { print(" 메인 액터 컨텍스트: 백그라운드 결과로 UI 업데이트") updateUIWithResult(result) } // 메인 액터 컨텍스트 → 분리된 태스크 컨텍스트로 돌아옴 print(" 분리된 태스크 컨텍스트: 백그라운드 작업 완료") } print("메인 액터 컨텍스트: 백그라운드 작업 시작됨 (비동기적으로 진행 중)") } // 비액터 컨텍스트에서 실행 func performLongRunningOperation() async -> String { print(" 비액터 컨텍스트: 장시간 실행 작업 수행 중") // 시간이 걸리는 작업 시뮬레이션 await Task.sleep(for: .milliseconds(100)) return "Operation Result" } // 메인 액터 컨텍스트에서 실행 @MainActor func updateUIWithResult(_ result: String) { print(" 메인 액터 컨텍스트: UI 업데이트 - \(result)") }
7. 액터 컨텍스트 → 동일 액터 내 비동기 메서드
actor DatabaseManager { // DatabaseManager 액터 컨텍스트 func saveUserData(user: User) async throws { print("DatabaseManager 액터 컨텍스트: 사용자 데이터 저장 시작") // 같은 액터 컨텍스트 내에서 비동기 메서드 호출 (컨텍스트 전환 없음) try await validateUser(user) print("DatabaseManager 액터 컨텍스트: 사용자 유효성 검증 완료") // DatabaseManager 액터 컨텍스트 → 비액터 컨텍스트(외부 API)로 전환 try await writeToDatabase(user) // 비액터 컨텍스트 → DatabaseManager 액터 컨텍스트로 돌아옴 print("DatabaseManager 액터 컨텍스트: 데이터베이스 저장 완료") // DatabaseManager 액터 컨텍스트 → CacheManager 액터 컨텍스트로 전환 await CacheManager.shared.updateCache(user: user) // CacheManager 액터 컨텍스트 → DatabaseManager 액터 컨텍스트로 돌아옴 print("DatabaseManager 액터 컨텍스트: 캐시 업데이트 완료") } // DatabaseManager 액터 컨텍스트에서 실행 private func validateUser(_ user: User) async throws { print(" DatabaseManager 액터 컨텍스트: 사용자 유효성 검증 중") // 유효성 검증 로직 } // 비액터 컨텍스트로 전환 (외부 API 호출) private func writeToDatabase(_ user: User) async throws { print(" 비액터 컨텍스트: 데이터베이스 쓰기 작업 수행 중") // 데이터베이스 쓰기 작업 시뮬레이션 await Task.sleep(for: .milliseconds(50)) } } actor CacheManager { static let shared = CacheManager() // CacheManager 액터 컨텍스트에서 실행 func updateCache(user: User) { print(" CacheManager 액터 컨텍스트: 캐시 업데이트 중") } }
8. 중첩된 컨텍스트 전환 (복합 예제)
// 비액터 컨텍스트에서 시작 func complexOperation() async { print("비액터 컨텍스트: 복합 작업 시작") // 비액터 컨텍스트 → 메인 액터 컨텍스트로 전환 await MainActor.run { print(" 메인 액터 컨텍스트: UI 준비 중") // 메인 액터 컨텍스트 → 구조화된 태스크 컨텍스트로 전환 Task { print(" 구조화된 태스크 컨텍스트 (메인 액터 상속): 작업 시작") // 구조화된 태스크 컨텍스트 → 비액터 컨텍스트로 전환 // (nonisolated를 사용해 액터 격리에서 벗어남) let data = await nonisolated_fetchData() print(" 구조화된 태스크 컨텍스트 (메인 액터 상속): 데이터 받음") // 구조화된 태스크 컨텍스트 → 액터 컨텍스트로 전환 let processedData = await DataProcessor.shared.process(data: data) print(" 구조화된 태스크 컨텍스트 (메인 액터 상속): 처리된 데이터 받음") // 구조화된 태스크 컨텍스트 → 메인 액터 컨텍스트로 전환 (이미 메인 액터에 있기 때문에 실제 전환은 없음) await MainActor.run { print(" 메인 액터 컨텍스트: 최종 UI 업데이트") updateUI(with: processedData) } } print(" 메인 액터 컨텍스트: 백그라운드 작업 시작됨") } print("비액터 컨텍스트: 복합 작업 설정 완료") } // 비액터 컨텍스트에서 실행 func nonisolated_fetchData() async -> [String] { print(" 비액터 컨텍스트: 데이터 가져오는 중") return ["Raw 1", "Raw 2", "Raw 3"] } actor DataProcessor { static let shared = DataProcessor() // DataProcessor 액터 컨텍스트에서 실행 func process(data: [String]) -> [String] { print(" DataProcessor 액터 컨텍스트: 데이터 처리 중") return data.map { "Processed \($0)" } } } // 메인 액터 컨텍스트에서 실행 @MainActor func updateUI(with data: [String]) { print(" 메인 액터 컨텍스트: UI 업데이트 중 - \(data)") }
동시성 컨텍스트(Concurrent Context)의 이해
동시성 컨텍스트(Concurrent Context)는 Swift의 동시성 모델에서 코드가 실행되는 논리적 환경을 의미합니다. 이는 코드가 어떻게 스케줄링되고, 어떤 격리 규칙을 따르며, 어떤 방식으로 다른 코드와 상호작용하는지를 정의합니다.
동시성 컨텍스트의 핵심 특성
- 실행 순서 보장: 하나의 동시성 컨텍스트 내에서는 코드가 순차적으로 실행됩니다.
- 격리 특성: 각 컨텍스트는 특정 격리 도메인(isolation domain)에 속할 수 있으며, 이는 해당 컨텍스트 내에서 안전하게 접근할 수 있는 상태를 정의합니다.
- 중단 및 재개: 동시성 컨텍스트는 await 지점에서 일시 중단되고 나중에 재개될 수 있습니다.
- 독립적 진행: 여러 동시성 컨텍스트는 서로 독립적으로 진행될 수 있습니다.
Swift에서의 동시성 컨텍스트 유형
1. 비동기 함수 컨텍스트
async 키워드로 표시된 함수는 자체 동시성 컨텍스트를 생성합니다.
func processData() async { // 이 함수 내부 전체가 하나의 동시성 컨텍스트입니다 // 코드는 순차적으로 실행되지만, await 지점에서 중단될 수 있습니다 }
2. 태스크 컨텍스트
Task 또는 Task.detached로 생성된 태스크는 새로운 동시성 컨텍스트를 형성합니다.
// 이 Task 블록은 새로운 동시성 컨텍스트를 생성합니다 Task { // 이 블록 내부의 코드는 자체 동시성 컨텍스트에서 실행됩니다 }
3. 액터 컨텍스트
액터는 자신만의 고유한 동시성 컨텍스트를 가집니다.
actor DataManager { // 이 액터의 프로퍼티와 메서드는 모두 DataManager 액터의 // 동시성 컨텍스트에서 실행됩니다 func process() { // 이 코드는 DataManager 액터 컨텍스트에서 실행됩니다 } }
동시성 컨텍스트와 실행 모델의 관계
Swift의 동시성 모델에서 동시성 컨텍스트는 운영체제 수준의 스레드와 직접적으로 매핑되지 않습니다. 대신, Swift 런타임은 여러 동시성 컨텍스트를 효율적으로 관리하고 스케줄링합니다:
- 협력적 양보(Cooperative Yielding): await 지점에서 한 컨텍스트는 실행을 양보하고 다른 컨텍스트가 실행될 수 있게 합니다.
- 논리적 스레드: 동시성 컨텍스트는 물리적 스레드가 아닌 "논리적 스레드"로 생각할 수 있습니다. 여러 컨텍스트가 같은 물리적 스레드에서 실행될 수 있고, 한 컨텍스트가 다른 스레드로 이동할 수도 있습니다.
- Continuation Passing: 동시성 컨텍스트가 await 지점에서 중단되면, Swift 런타임은 해당 컨텍스트의 상태를 저장하고 나중에 복원하여 실행을 재개할 수 있습니다.
동시성 컨텍스트와 메모리 모델
동시성 컨텍스트는 메모리 접근 규칙과 밀접한 관련이 있습니다.
- 액터 격리(Actor Isolation): 액터 컨텍스트 내에서는 해당 액터의 가변 상태에 독점적으로 접근할 수 있습니다.
- 상호 배제(Mutual Exclusion): 두 개의 서로 다른 액터 컨텍스트는 동시에 실행될 수 있지만, 각자의 상태에만 직접 접근할 수 있습니다.
- 컨텍스트 간 데이터 전달: 한 동시성 컨텍스트에서 다른 컨텍스트로 데이터를 전달할 때는 해당 데이터가 Sendable을 준수해야 합니다.
예시: 다양한 동시성 컨텍스트 간의 상호작용
// 비동기 함수 컨텍스트 func processUserData(userId: String) async throws { print("비동기 함수 컨텍스트 시작") // 새로운 태스크 컨텍스트 생성 let userTask = Task { print("태스크 컨텍스트 시작") // UserManager 액터 컨텍스트로 전환 let user = await UserManager.shared.getUser(id: userId) // 다시 태스크 컨텍스트로 돌아옴 print("태스크 컨텍스트 계속 실행") return user } // 비동기 함수 컨텍스트에서 태스크 결과 대기 let user = try await userTask.value // ProfileManager 액터 컨텍스트로 전환 let profile = await ProfileManager.shared.getProfile(for: user) // 메인 액터 컨텍스트로 전환 await MainActor.run { print("메인 액터 컨텍스트에서 UI 업데이트") updateUI(with: profile) } print("비동기 함수 컨텍스트 종료") } // 각각 고유한 액터 컨텍스트를 가짐 actor UserManager { static let shared = UserManager() // UserManager 액터 컨텍스트에서 실행 func getUser(id: String) async -> User { /* ... */ } } actor ProfileManager { static let shared = ProfileManager() // ProfileManager 액터 컨텍스트에서 실행 func getProfile(for user: User) async -> Profile { /* ... */ } } // 메인 액터 컨텍스트에서 실행 @MainActor func updateUI(with profile: Profile) { /* ... */ }
이 예시에서는 여러 동시성 컨텍스트가 상호작용하며, 각 컨텍스트는 자체적인 실행 규칙을 따릅니다.
구조화된 태스크 vs 분리된 태스크
구조화된 태스크 (Structured Task)
Task { } 생성자로 만들어지는 일반 태스크입니다. 이것이 Swift에서 기본적으로 사용하는 태스크 유형입니다.
Task { // 이것은 구조화된 태스크입니다 }
분리된 태스크 (Detached Task)
Task.detached { } 생성자로 만들어지는 태스크입니다.
Task.detached { // 이것은 분리된 태스크입니다 }
컨텍스트 상속이란?
"컨텍스트 상속"은 태스크가 생성될 때 생성 환경의 특정 속성들을 가져오는 것을 의미합니다. 상속되는 주요 속성들은
- 액터 컨텍스트: 태스크가 어떤 액터 내부에서 생성되었는지
- 우선순위: 태스크의 실행 우선순위
- 작업 로컬 값(Task-local values): 태스크 간에 공유되는 로컬 값
- 취소 상태: 부모 태스크가 취소되면 자식도 취소됨
구조화된 태스크 vs 분리된 태스크: 주요 차이점
구조화된 태스크 (Task)
- 컨텍스트 상속: 생성된 환경의 컨텍스트를 상속받습니다.
- 메인 액터에서 생성된 Task는 기본적으로 메인 액터 컨텍스트를 상속
- 액터 내부에서 생성된 Task는 해당 액터의 컨텍스트를 상속
- 수명 주기 연결: 부모 태스크나 범위가 종료되면 자동으로 취소됩니다.
- 취소 전파: 부모 태스크가 취소되면 모든 자식 태스크도 자동으로 취소됩니다.
분리된 태스크 (Task.detached)
- 컨텍스트 독립: 생성 환경의 컨텍스트를 상속받지 않습니다. 항상 비액터 컨텍스트(어떤 액터에도 속하지 않음)에서 시작합니다.
- 독립적 수명 주기: 생성 범위나 부모 태스크와 관계없이 독립적으로 실행됩니다.
- 취소 독립: 부모 태스크가 취소되어도 자동으로 취소되지 않습니다.
컨텍스트 상속의 실제 예
// 메인 액터에서 실행 중인 코드 @MainActor func handleButtonTap() { // 구조화된 태스크 - 메인 액터 컨텍스트를 상속 let task1 = Task { print("Task 1") // 이 코드는 메인 액터 컨텍스트를 상속받았으므로 // UI 업데이트가 안전함 (MainActor.run 불필요) updateLabel("Processing...") // 비동기 작업은 여전히 await 필요 let result = await performCalculation() updateLabel(result) } // 분리된 태스크 - 컨텍스트 상속 없음 let task2 = Task.detached { print("Task 2") // 이 코드는 메인 액터 컨텍스트를 상속받지 않았으므로 // UI 업데이트를 위해 명시적 전환 필요 await MainActor.run { updateLabel("Detached processing...") } let result = await performCalculation() // UI 업데이트에 다시 명시적 전환 필요 await MainActor.run { updateLabel(result) } } } // 메인 액터에서 실행되는 함수 @MainActor func updateLabel(_ text: String) { // UI 업데이트 코드 } // 비액터 컨텍스트에서 실행되는 함수 func performCalculation() async -> String { // 무거운 계산 return "Result" }
이 예제에서 task1은 메인 액터 컨텍스트에서 생성되었기 때문에 그 컨텍스트를 상속받습니다. 따라서 updateLabel 함수를 직접 호출할 수 있습니다.
반면 task2는 분리된 태스크이므로 메인 액터 컨텍스트를 상속받지 않습니다. 그래서 UI를 업데이트하려면 MainActor.run을 통해 명시적으로 메인 액터 컨텍스트로 전환해야 합니다.
액터 내부에서의 예
actor DataManager { func processData() async { // 구조화된 태스크 - DataManager 액터 컨텍스트 상속 let task1 = Task { // 이 코드는 DataManager 액터 컨텍스트에서 실행됨 // 액터의 상태에 직접 접근 가능 updateInternalState() } // 분리된 태스크 - 컨텍스트 상속 없음 let task2 = Task.detached { // 이 코드는 DataManager 액터 컨텍스트를 상속받지 않음 // 액터 상태에 접근하려면 명시적으로 액터 메서드 호출 필요 await self.updateInternalState() } } func updateInternalState() { // 액터 상태 업데이트 } }
이 예제에서 task1은 DataManager 액터 컨텍스트를 상속받아서 updateInternalState()를 직접 호출할 수 있습니다. 하지만 task2는 액터 컨텍스트를 상속받지 않기 때문에 await self.updateInternalState()와 같이 명시적으로 액터 메서드를 호출해야 합니다.
이렇게 컨텍스트 상속은 코드가 어떤 격리 환경에서 실행되는지를 결정하며, 이는 상태 접근 방법과 안전성에 직접적인 영향을 미칩니다.
Swift의 nonisolated 키워드 이해하기
nonisolated 키워드는 Swift의 액터 모델에서 격리(isolation)와 관련된 중요한 개념입니다. 이 키워드를 사용하면 액터 내부의 특정 멤버가 액터의 격리 특성을 갖지 않도록 할 수 있습니다.
액터 격리의 기본 원칙
먼저, 액터의 기본 동작을 이해해야 합니다.
- 액터의 모든 프로퍼티와 메서드는 기본적으로 격리(isolated) 상태입니다.
- 격리된 코드는 액터의 고유한 직렬 큐에서만 실행됩니다.
- 외부에서 격리된 멤버에 접근하려면 await을 사용해야 합니다.
nonisolated 키워드의 목적
nonisolated 키워드는 액터 내에서 특정 멤버가 액터의 격리 특성을 갖지 않도록 표시합니다. 이렇게 하면
- await 없이 해당 멤버에 접근할 수 있습니다.
- 해당 멤버는 액터의 가변 상태에 직접 접근할 수 없습니다.
- 해당 멤버는 액터의 직렬 큐가 아닌 호출자의 컨텍스트에서 실행됩니다.
nonisolated 사용 예시
1. 액터 내 메서드에 nonisolated 사용
actor DataManager { private var data: [String: Any] = [:] // 격리된 메서드 (기본값) func updateData(key: String, value: Any) { data[key] = value // 액터 상태에 직접 접근 가능 } // 격리되지 않은 메서드 nonisolated func calculateHash(for value: String) -> Int { // data[value] = true // 컴파일 오류! 액터 상태에 직접 접근 불가 return value.hashValue // 액터 상태를 사용하지 않는 순수 계산 } } // 사용 예시 func example() async { let manager = DataManager() // 격리된 메서드는 await 필요 await manager.updateData(key: "name", value: "John") // 격리되지 않은 메서드는 await 불필요 let hash = manager.calculateHash(for: "John") // await 없이 호출 가능 }
2. 읽기 전용 계산 프로퍼티에 nonisolated 사용
actor UserManager { private var users: [User] = [] // 격리된 프로퍼티 (기본값) var userCount: Int { users.count // 액터 상태에 접근 } // 격리되지 않은 프로퍼티 nonisolated var isConfigured: Bool { // users.isEmpty // 컴파일 오류! 액터 상태에 직접 접근 불가 return true // 액터 상태와 무관한 정보 } } // 사용 예시 func checkUserManager() async { let manager = UserManager() // 격리된 프로퍼티는 await 필요 let count = await manager.userCount // 격리되지 않은 프로퍼티는 await 불필요 if manager.isConfigured { // await 없이 접근 가능 print("Manager is configured") } }
3. 액터 프로토콜 요구사항 구현에 nonisolated 사용
protocol DataProvider { func provideIdentifier() -> String } actor CloudStorage: DataProvider { private var storageId: String = UUID().uuidString // 프로토콜 요구사항을 nonisolated로 구현 nonisolated func provideIdentifier() -> String { // storageId // 컴파일 오류! 액터 상태에 직접 접근 불가 return "cloud-storage" // 상수 반환 } // 액터 내부용 격리 메서드 func internalIdentifier() -> String { return "internal-\(storageId)" // 액터 상태에 접근 가능 } } // 사용 예시 func useProvider(provider: DataProvider) { // 프로토콜 메서드는 await 없이 호출 가능 let id = provider.provideIdentifier() print("ID: \(id)") } func useCloudStorage() async { let storage = CloudStorage() // DataProvider로 사용 시 await 불필요 useProvider(provider: storage) // 액터 메서드 직접 호출 시 await 필요 let internalId = await storage.internalIdentifier() print("Internal ID: \(internalId)") }
nonisolated 사용의 일반적인 시나리오
- 순수 계산 함수: 액터 상태에 접근하지 않는 순수 함수
- 읽기 전용 상수 값 반환: 변경되지 않는 값을 제공하는 경우
- 프로토콜 적합성: 비동기적이지 않은 프로토콜 요구사항 구현
- 성능 최적화: 불필요한 액터 큐 전환을 피하기 위해
- 유틸리티 함수: 액터 상태와 무관한 헬퍼 메서드
nonisolated를 사용할 때 제약사항
- 액터 상태 접근 불가: nonisolated 멤버는 액터의 가변 상태에 직접 접근할 수 없습니다.
- 다른 격리된 멤버 호출 제한: nonisolated 메서드에서 같은 액터의 격리된 메서드를 호출하려면 await self.method()와 같이 명시적으로 호출해야 합니다.
- self 캡처 주의: 클로저 내에서 self를 캡처할 때 액터 격리 규칙을 고려해야 합니다.
실제 사용 사례
예: 데이터베이스 액터
actor Database { private var connection: Connection private var isConnected: Bool = false init(connectionString: String) { self.connection = Connection(string: connectionString) } // 격리된 메서드 - 데이터베이스 상태를 변경 func connect() async throws { isConnected = true try await connection.open() } // 격리된 메서드 - 데이터베이스 쿼리 실행 func query(_ sql: String) async throws -> [Record] { guard isConnected else { throw DatabaseError.notConnected } return try await connection.execute(sql) } // 격리되지 않은 메서드 - SQL 검증만 수행 nonisolated func validateSQL(_ sql: String) -> Bool { // SQL 유효성 검사 로직 (데이터베이스 상태 접근 안 함) return sql.contains("SELECT") || sql.contains("INSERT") || sql.contains("UPDATE") } // 격리되지 않은 프로퍼티 - 설정 정보만 반환 nonisolated var connectionType: String { return "SQLite" } } // 사용 예 func exampleUsage() async throws { let db = Database(connectionString: "path/to/db") // SQL 검증은 await 없이 호출 가능 if db.validateSQL("SELECT * FROM users") { print("SQL is valid") } // 연결 타입도 await 없이 접근 가능 print("Connection type: \(db.connectionType)") // 실제 데이터베이스 작업은 await 필요 try await db.connect() let records = try await db.query("SELECT * FROM users") print("Found \(records.count) records") }
정리
nonisolated 키워드는 액터 내에서 격리가 필요하지 않은 멤버를 표시하는 중요한 도구입니다. 이를 통해
- 액터 상태에 접근하지 않는 기능은 더 효율적으로 사용할 수 있습니다 (await 없이 호출 가능).
- 액터가 비동기적이지 않은 프로토콜을 준수할 수 있습니다.
- 불필요한 동기화 오버헤드를 줄일 수 있습니다.
'Apple🍎 > Swift' 카테고리의 다른 글
좀 더 Low 하게 가보자~ Swift 저수준 메모리 관리 (0) 2025.04.25 DispatchQueue와 DispatchWorkItem 비교하기 (0) 2025.04.17 Swift에서의 데이터 레이스 방지를 위한 동기화 기법 (0) 2025.04.16 Swift 6 : Typed Throws ( 에러도 타입을 줘서 더 명확히 처리하자.) (0) 2025.04.14 inout 파라미터 작동 방식 파해치기 (0) 2025.03.11 - 메인 액터 컨텍스트 (Main Actor Context)