@unchecked Sendable을 붙이면 무슨 일이 벌어지나요? 언제 정당화되나요?- 값 타입은 왜 대체로 안전한가요? 값 타입인데 Sendable이 아닌 경우는?
답변 ① — 검증을 끄는 각서, 그리고 그 대가 🔥 핵심
@unchecked Sendable은 컴파일러의 Sendable 안전성 검증을 끄고, "이 타입의 스레드 안전은 내가 직접 보장한다"고 약속하는 것입니다. 컴파일러는 더 이상 이 타입에 대해 데이터 레이스를 잡아주지 않습니다.
정당화되는 경우는 컴파일러가 볼 수 없는 수동 동기화로 상태를 보호하는 final 클래스입니다 — NSLock, os_unfair_lock, 직렬 DispatchQueue로 모든 접근을 감싼 레거시 코드가 대표적입니다. 컴파일러는 락의 의미를 이해하지 못하므로 안전한데도 경고를 냅니다.
대가는 컴파일 타임 안전이 사라진다는 것입니다. 접근 경로 하나에서 락을 빠뜨려도 아무도 잡아주지 않습니다. 그래서 불변식을 주석으로 문서화하고 마이그레이션 티켓을 남긴 뒤, 궁극적으로 actor로 옮기는 것이 정석입니다.
Mutex(Synchronization, iOS 18+)는 그 자체가 Sendable이라, Mutex만으로 보호되는 final 클래스는 @unchecked 없이 그냥 Sendable이 됩니다. @unchecked가 필요한 건 컴파일러가 못 보는 락들입니다. 이 구분을 하면 가산점입니다.
// ✅ @unchecked Sendable: 컴파일러가 못 보는 락으로 직접 보장 (최후의 수단)
final class LegacyCache: @unchecked Sendable {
private let lock = NSLock()
private var items: [String: Data] = [:]
func get(_ key: String) -> Data? {
lock.lock(); defer { lock.unlock() }
return items[key]
}
// ⚠️ 접근 경로 하나라도 lock을 빠뜨리면 아무도 안 잡아준다
}
// ✅ Mutex는 자체가 Sendable → @unchecked 불필요
import Synchronization
final class Counter: Sendable { // @unchecked 없음!
private let count = Mutex<Int>(0)
func increment() { count.withLock { $0 += 1 } }
}답변 ② — 값 시맨틱이 이유이고, 멤버가 예외를 만든다
값 타입(struct/enum)이 대체로 안전한 이유는 값 시맨틱 때문입니다. 경계를 넘길 때 복사본이 전달되므로 서로 공유하는 가변 상태가 없습니다. 조건은 모든 저장 프로퍼티가 Sendable일 때이며, 그러면 (public이 아니면) 암시적으로 Sendable이 됩니다. public 타입은 모듈 밖에서 내부를 볼 수 없으므로 명시적으로 : Sendable을 선언해야 합니다.
값 타입인데 Sendable이 아닌 경우는 멤버 중 non-Sendable이 하나라도 있을 때입니다.
- 가변 class 참조를 담은 struct — 가장 흔한 반례.
- non-
@Sendable클로저 프로퍼티 — 캡처한 것을 컴파일러가 보증할 수 없음.
"값 타입이면 무조건 안전"은 규칙이 아니라 휴리스틱입니다. struct 복사는 얕은 복사라, 안에 든 class 참조는 복사돼도 같은 객체를 가리켜 복사본끼리 그 객체에서 레이스가 납니다. Sendability는 struct냐 class냐가 아니라 멤버별로 결정됩니다. 이 반례를 못 대면 외운 수준으로 봅니다.
// ✅ 모든 멤버가 Sendable → (internal이면) 암시적 Sendable
struct UserProfile { let id: UUID; var name: String }
// ❌ 값 타입인데 non-Sendable: 가변 class 참조를 담음
final class Box { var value = 0 } // 가변 class → non-Sendable
struct Wrapper { var box: Box } // 복사해도 box는 같은 객체 → Wrapper도 non-Sendable
// 왜 위험한가: 복사본 둘이 같은 Box를 공유한다
let a = Wrapper(box: Box())
let b = a // 얕은 복사 — a.box 와 b.box 는 같은 객체
// a와 b를 서로 다른 스레드로 보내면 같은 Box에서 레이스반대로 actor와 @MainActor 격리 타입은 암시적으로 Sendable입니다. 격리가 이미 접근을 직렬화하기 때문입니다.
쉽게 이해하기
(질문 ②) 값 타입은 사진을 복사해서 나눠주는 것이고, 참조 타입은 원본 하나를 여럿이 돌려보는 것이에요. 사진은 각자 자기 복사본을 보니까 서로 안 부딪힙니다. 이게 값 타입이 안전한 이유예요.
그런데 만약 사진 안에 '우리 집 열쇠'가 찍혀 있으면 어떨까요? 사진을 아무리 복사해도 그 열쇠가 여는 집은 하나뿐이라, 여러 명이 동시에 우리 집을 어지럽힐 수 있어요. struct 안에 든 class가 딱 그 '열쇠'입니다. 그래서 안전한지는 안에 뭐가 들었는지를 봐야 알 수 있어요.
(질문 ①) @unchecked Sendable은 "검사원 없이 제가 알아서 안전하게 할게요"라는 각서예요. 이미 튼튼한 자물쇠(락)를 잘 채워놨는데 검사원이 그 자물쇠를 못 알아볼 때만 쓸 만합니다. 문제는 나중에 문을 하나 더 냈는데 거기 자물쇠 다는 걸 깜빡해도 아무도 안 잡아준다는 거예요. 그래서 되도록 actor로 바꾸는 게 좋습니다.
안전한지는 struct냐 class냐가 아니라 안에 뭐가 들었냐로 결정된다.