Swift Concurrency를 쓰다 보면 "이건 왜 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는 참조(reference)로 전달되기 때문에 여러 스레드에서 동시에 접근될 위험이 있다.
final class Logger {
var message: String = ""
}
func log(_ logger: Logger) async {
// ...
}
이럴 때 컴파일러는 다음과 같은 경고를 낸다.
“Reference type 'Logger' is not Sendable across actors”
그래도 "이 클래스는 내가 안전하게 만들었어!" 라고 확신할 수 있다면,
컴파일러 검사를 우회할 수 있다.
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가 안전성을 보장해주지 않으니, 개발자가 직접 스레드 동기화를 신경 써야 한다.
클로저가 여러 스레드에서 실행될 수 있다면, Swift는 그 클로저의 캡처 값이 모두 Sendable이어야 한다고 요구한다.
Task.detached { @Sendable in
print("Run on another thread")
}
이 안에서 Sendable이 아닌 값을 캡처하면 컴파일러가 경고한다.
Swift는 다음과 같은 순간에 Sendable 검사를 수행한다.
Swift 5에서는 Sendable 관련 경고가 많지만, 대부분 “경고(warning)”로 끝난다.
하지만 Swift 6부터는 엄격한 동시성(strict concurrency checking) 이 적용되어,
이제는 아예 컴파일 오류가 된다.
즉, 지금부터 Sendable을 신경 써 두면 나중에 Swift 6로 마이그레이션할 때 훨씬 수월하다.
"값이 스레드 간 이동할 때 안전한가?"
이 질문이 바로 Swift Concurrency의 핵심이자, Sendable의 존재 이유다.