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

🧠 Swift Concurrency — “Capture of 'self' with non-sendable type” 완전 정리

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

Core Data + async/await 사용 시 흔히 발생하는 Sendable 오류 해결법


🚨 1️⃣ 에러 상황

Core Data에서 async/await을 적용하려고
다음과 같은 코드를 작성했다고 해보자 👇

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

 

그런데 빌드하자마자 이런 에러가 발생한다 👇

Capture of 'self' with non-sendable type 'DiaryCoreDataManager' in a '@Sendable' closure

💡 2️⃣ 이게 왜 생기는 걸까?

Swift의 Concurrency 모델(= async/await)
스레드 간 안전성(thread safety) 을 매우 중요하게 생각해.

 

그래서 다른 스레드(Task) 로 객체를 전달할 때,
그 객체가 "안전하게 전송 가능한 타입"인지 확인해.
이걸 바로 Sendable 검사라고 해.

 

🔍 핵심 포인트

 

- @Sendable 클로저 = “다른 스레드로 넘길 수 있는 클로저”

- Swift는 이 클로저 안에서 캡처한 객체가
스레드 간 안전하게 공유될 수 있는지 검사함

- 그런데 Core Data의 주요 타입(NSManagedObjectContext, NSPersistentContainer)은
thread-safe하지 않음 ⚠️

 

즉, Swift 입장에서 보면

 

“Core Data 객체는 스레드 간 넘기면 안 되는데,

너 async 안에서 self 썼잖아?”
라는 의미야.


🧩 3️⃣ 해결 방법 3가지

방법 설명 안전성 추천도
✅ @MainActor 클래스 선언 Core Data는 기본적으로 메인 스레드 전용 → 가장 자연스러움 매우 높음 ⭐️⭐️⭐️
✅ Task.detached 사용 Swift Concurrency 친화적 백그라운드 Task 중간 ⭐️⭐️
⚠️ [weak self] 캡처 일시적인 회피용, Sendable은 해결 안 됨 낮음 ⚠️
🚫 @unchecked Sendable 선언 강제로 검사 무시, 위험 매우 낮음

🧩 4️⃣ 추천 해결책 — @MainActor 클래스 선언

Core Data의 viewContext는 원래 메인 스레드 전용이라
이 객체를 다루는 매니저 전체를 @MainActor로 선언하면 완벽해요.

@MainActor
final class DiaryCoreDataManager {
    static let shared = DiaryCoreDataManager()
    private init() {}

    func fetchDiaries(mode: FetchMode) -> [EmotionDiaryModel] {
        let request: NSFetchRequest<EmotionDiaryEntity> = EmotionDiaryEntity.fetchRequest()
        ...
        return try! context.fetch(request).compactMap { $0.toModel() }
    }

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

 

이렇게 하면 self가 non-Sendable이어도
Swift가 “어차피 메인 액터에서만 접근하니까 괜찮다”라고 판단해. ✅


⚙️ 5️⃣ 다른 방법 — Task.detached 활용

만약 정말로 백그라운드에서 완전히 분리된 fetch를 원한다면,
DispatchQueue.global() 대신 Swift의 Task.detached를 사용할 수도 있어 👇

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

하지만 이 경우에는
Core Data의 컨텍스트를 thread-safe하게 다뤄야 하므로
전용 backgroundContext를 따로 만들어서 써야 해.
(그건 v3.0 이후 Firebase 병합할 때 다룰 부분 😉)


💬 6️⃣ 잠깐, [weak self]는 안 되나?

DispatchQueue.global().async { [weak self] in
    guard let self else { return }
    let result = self.fetchDiaries(mode: mode)
    continuation.resume(returning: result)
}

 

이건 컴파일은 통과하지만,
Swift Concurrency의 타입 안전성 자체는 유지되지 않아.
즉, "그냥 경고를 없애는 임시 방편" 이지
진짜 해결은 아냐 ⚠️


📘 7️⃣ 정리 요약

항목 설명
에러 메시지 Capture of 'self' with non-sendable type ...
원인 Core Data 객체는 thread-safe하지 않음
해결 핵심 Sendable 타입이 아닌 객체를 async 클로저로 넘기지 말 것
실전 해결책 Core Data Manager를 @MainActor로 선언
부가 팁 backgroundContext를 쓰려면 Task.detached + privateQueueContext 조합

🚀 결론

Core Data는 기본적으로 메인 스레드 전용 구조다.
따라서 Core Data Manager 전체를 @MainActor로 선언하는 것이
Swift Concurrency 환경에서 가장 안전하고 자연스러운 설계 방식이다.

728x90
LIST