본문 바로가기
감정일기(가칭)

🚀 Swift 6에서 바뀐 Concurrency 규칙 완전 정리

by 밤새는 탐험가89 2025. 10. 19.
728x90
SMALL

— “Call to main actor-isolated instance method…” 에러의 원인과 해결법


🎯 들어가며

Swift 6로 프로젝트를 올렸더니 갑자기 이런 에러가 떴다면 👇

Call to main actor-isolated instance method 'fetchDiaries(mode:)' 
in a synchronous nonisolated context; this is an error in the Swift 6 language mode

 

Swift 5까지는 잘 되던 코드인데 왜 이제는 안 되는 걸까?
이건 단순한 “문법 변경”이 아니라, Swift의 동시성(Concurrency) 모델이 진짜 완성되었기 때문이야.


⚙️ 1️⃣ Swift Concurrency의 핵심 개념 정리

개념 설명 예시
Actor 데이터를 안전하게 보호하는 실행 단위
(스레드 대신 데이터 중심 동기화)
@MainActor
Actor Isolation 각 Actor는 자신의 데이터만 접근 가능,
다른 Actor 접근 시 명시 필요
await, Task { @MainActor in ... }
Sendable 스레드 간 안전하게 전달 가능한 타입을 의미 Int, String, struct 등은 Sendable ✅
/ NSManagedObject는 ❌

💥 2️⃣ Swift 5에서는 왜 됐을까?

Swift 5에서는 async/await이 “실험적 기능” 이었어.
그래서 다음 같은 코드도 단순 경고로 끝났어 👇

@MainActor
func fetchDiaries() -> [Diary] { ... }

func fetchAsync() async {
    DispatchQueue.global().async {
        let result = self.fetchDiaries() // ⚠️ Swift 5에서는 경고
    }
}

 

⚠️ “메인 액터 격리 위반일 수 있음”
이 정도로 끝났지, 컴파일은 됐어.


🔒 3️⃣ 그런데 Swift 6부터는?

Swift 6에서는 동시성 모델이 언어 차원에서 완성됨
(즉, 더 이상 실험 단계가 아님)

 

그래서 같은 코드가 이렇게 인식돼 👇

 

❌ “너 지금 메인 액터(@MainActor)에 속한 함수(fetchDiaries)를
백그라운드(DispatchQueue.global)에서 호출했네?
이건 데이터 레이스 위험이 있으니까 컴파일 자체를 막을게.”


🧠 4️⃣ Swift 5 → 6 변화 도식

구분 Swift 5  Swift 6
Actor 격리 위반 ⚠️ 경고만 표시 🚫 컴파일 에러
Sendable 위반 일부만 체크 모든 async 경로에서 검사
Core Data (NSManagedObjectContext) Thread-safe 체크 X Sendable 아님 → 오류 처리 필요
해결 필요 여부 무시 가능 필수 수정

🧩 5️⃣ 실제 사례 (Core Data 예시)

@MainActor
final class DiaryCoreDataManager {
    func fetchDiaries(mode: FetchMode) -> [EmotionDiaryModel] { ... }

    func fetchDiariesAsync(mode: FetchMode) async -> [EmotionDiaryModel] {
        await withCheckedContinuation { continuation in
            DispatchQueue.global(qos: .userInitiated).async {
                // ❌ Swift 6에서는 여기서 에러 발생
                let result = self.fetchDiaries(mode: mode)
                continuation.resume(returning: result)
            }
        }
    }
}

 

 

- fetchDiaries(mode:)는 @MainActor에 묶여 있음

- DispatchQueue.global()은 백그라운드 큐 (actor 격리 깨짐)


✅ 6️⃣ 해결 방법 3가지 

방법 설명 사용 조건 안정성
🟩 Task { @MainActor in … } 메인 액터 컨텍스트로 다시 실행 viewContext 사용 시 ✅✅✅
🟦 nonisolated func 액터 격리에서 제외 (thread-safe 전제) backgroundContext 사용 시 ⚠️ 주의
🟨 backgroundContext.perform {} Core Data 전용 백그라운드 작업 대량 처리, 이미지 포함 ✅✅

 

🟩 방법 1 — Task로 메인 액터 지정 (가장 안전)

func fetchDiariesAsync(mode: FetchMode) async -> [EmotionDiaryModel] {
    await withCheckedContinuation { continuation in
        DispatchQueue.global(qos: .userInitiated).async {
            Task { @MainActor in
                let result = self.fetchDiaries(mode: mode)
                continuation.resume(returning: result)
            }
        }
    }
}

 

➡️ Core Data의 viewContext와 100% 호환
➡️ Swift 6의 actor 규칙도 준수

 

🟦 방법 2 — nonisolated로 격리 해제

@MainActor
final class DiaryCoreDataManager {
    nonisolated func fetchDiaries(mode: FetchMode) -> [EmotionDiaryModel] { ... }
}

 

⚠️ 단, 내부에서 viewContext를 접근하면 여전히 위험함
→ 반드시 backgroundContext 사용해야 함

 

🟨 방법 3 — Core Data Background Context

func fetchDiariesInBackground(mode: FetchMode) async -> [EmotionDiaryModel] {
    await withCheckedContinuation { continuation in
        backgroundContext.perform {
            let request = NSFetchRequest<EmotionDiaryEntity>(entityName: "EmotionDiaryEntity")
            let entities = try? backgroundContext.fetch(request)
            continuation.resume(returning: entities?.compactMap { $0.toModel() } ?? [])
        }
    }
}

🧭 7️⃣ Swift 6 Actor 격리 — 한눈에 정리 도식

                         ┌────────────────────────────────────────────┐
                         │        Swift 5 (경고 수준)                   │
                         │────────────────────────────────────────────│
                         │ ✅ @MainActor 함수                          │
                         │ ⚠️ 백그라운드 호출 허용 (주의만 표시)              │
                         │                                            │
                         │     DispatchQueue.global() → 가능           │
                         └────────────────────────────────────────────┘
                                        ↓ ↓ ↓
                         ┌────────────────────────────────────────────┐
                         │        Swift 6 (엄격한 규칙)                  │
                         │────────────────────────────────────────────│
                         │ 🚫 @MainActor 함수는 Actor 외부 호출 불가       │
                         │ ✅ 반드시 Task { @MainActor in … } 로 래핑     │
                         │                                            │
                         │     DispatchQueue.global() → ❌ 불가        │
                         └────────────────────────────────────────────┘

📘 결론

Swift 6부터는 동시성 규칙이 “선택”이 아니라 “의무”다.

 

- @MainActor는 반드시 메인 스레드에서만 호출해야 하고,

- 백그라운드 접근은 Task { @MainActor in … }로 안전하게 감싸야 한다.

 

✅ 즉, Thread-safe한 경계선이 코드로 명확히 보이는 시대가 온 거다.

728x90
LIST