맨날 까먹는 Swift Sendable

shintwl·2025년 10월 16일

Swift Concurrency를 쓰다 보면 "이건 왜 Sendable 오류야?" 하는 순간이 자주 온다.
그래서 이번 글에서는 Sendable이 뭔지, 왜 필요한지, 그리고 언제 신경 써야 하는지를 한 번 정리해본다.

Sendable이란?

Sendable은 actor 간 안전하게 전달할 수 있는 타입임을 보장하는 마커 프로토콜(marker protocol) 이다.

protocol Sendable { }

구현해야 할 메서드는 없고, 단지 "이 타입은 여러 스레드 간 공유되어도 안전하다"는 의미만 가진다.
Swift의 actor, Task, async/await 구조에서 데이터 레이스(Data Race) 를 방지하기 위한 핵심 개념이다.

왜 필요한가?

Swift의 actor는 내부 상태를 하나의 스레드에서만 접근하도록 보장한다.
하지만 actor 간 메시지를 주고받을 때는 값이 다른 스레드로 이동하게 된다.
만약 그 값이 참조 타입(class)이라면, 여러 스레드가 동시에 같은 인스턴스를 접근할 수 있다.
→ 이게 바로 데이터 레이스의 근원이다.
그래서 Swift는 "스레드 간에 안전하게 전달 가능한 타입만 보내라"고 요구한다.
그 안전성을 판단하는 기준이 바로 Sendable.

값 타입은 기본적으로 안전하다

struct, enum 같이 값으로 복사되는 타입은 기본적으로 안전하다.
Swift는 이런 타입이 자동으로 Sendable을 채택하도록 한다.

struct User: Sendable {
    let id: Int
    let name: String
}

Int, String 같은 기본 타입은 이미 Sendable을 준수하기 때문에 별문제 없다.

참조 타입(class)은 기본적으로 Sendable 아님

class는 참조(reference)로 전달되기 때문에 여러 스레드에서 동시에 접근될 위험이 있다.

final class Logger {
    var message: String = ""
}

func log(_ logger: Logger) async {
    // ...
}

이럴 때 컴파일러는 다음과 같은 경고를 낸다.

“Reference type 'Logger' is not Sendable across actors”

예외: @unchecked Sendable

그래도 "이 클래스는 내가 안전하게 만들었어!" 라고 확신할 수 있다면,
컴파일러 검사를 우회할 수 있다.

final class ThreadSafeLogger: @unchecked Sendable {
    private let queue = DispatchQueue(label: "log.queue")
    private var messages: [String] = []

    func log(_ msg: String) {
        queue.async {
            self.messages.append(msg)
        }
    }
}

@unchecked Sendable은 "내가 책임질게" 라는 의미다.
즉, Swift가 안전성을 보장해주지 않으니, 개발자가 직접 스레드 동기화를 신경 써야 한다.

@Sendable 클로저

클로저가 여러 스레드에서 실행될 수 있다면, Swift는 그 클로저의 캡처 값이 모두 Sendable이어야 한다고 요구한다.

Task.detached { @Sendable in
    print("Run on another thread")
}

이 안에서 Sendable이 아닌 값을 캡처하면 컴파일러가 경고한다.

Sendable 검사 시점

Swift는 다음과 같은 순간에 Sendable 검사를 수행한다.

  1. actor 간 통신 — await otherActor.method(...)
  2. Task.detached — 클로저가 다른 스레드에서 실행될 때
  3. @Sendable closure — Sendable을 요구하는 API에 전달될 때

Swift 6에서는 더 엄격해진다

Swift 5에서는 Sendable 관련 경고가 많지만, 대부분 “경고(warning)”로 끝난다.
하지만 Swift 6부터는 엄격한 동시성(strict concurrency checking) 이 적용되어,
이제는 아예 컴파일 오류가 된다.
즉, 지금부터 Sendable을 신경 써 두면 나중에 Swift 6로 마이그레이션할 때 훨씬 수월하다.

마무리

"값이 스레드 간 이동할 때 안전한가?"
이 질문이 바로 Swift Concurrency의 핵심이자, Sendable의 존재 이유다.

0개의 댓글