value semantic type이 뭐야?
변수에 담거나 전달할 때 복사본이 넘어가, 한쪽을 바꿔도 다른 쪽에 영향이 없는 타입. struct·enum·Int·String·Array 등. 반대는 reference semantics(class) — 참조(주소)를 공유해 한쪽 변경이 모두에 반영된다. 값 타입은 공유 상태가 없어 Sendable 조건을 만족하기 쉽다.
Apple 공식 Sendable 문서를 한 줄씩 읽으며 나온 질문들을 그대로 정리한 문서다. value semantics → Sendable 4종 → class 3조건 → @unchecked → 암묵적(implicit) conformance → @MainActor → @Sendable 클로저까지, "왜 그런가"를 추론 과정 그대로 보존했다.
Apple Developer Documentation — Sendable protocol
https://developer.apple.com/documentation/Swift/Sendable
기록 없음
Sendable = "이 타입의 값은 동시성 도메인(concurrency domain) 경계를 넘어가도 데이터 레이스가 없다"는 컴파일러와의 약속이다. 안전을 확보하는 방법은 단 두 가지: ① 상태를 불변으로 만들거나(값 복사·let), ② 상태 접근을 한 곳으로 격리(actor·lock·@MainActor)한다.
동시성 경계로 격리된 실행 영역. 각 actor, Task, @MainActor(메인 스레드), 백그라운드 스레드 등이 서로 다른 도메인이다. 서로 다른 도메인 사이에서 값을 주고받을 때 데이터 레이스가 생길 수 있고, Sendable이 "이 경계를 넘어도 안전하다"를 보장한다.
| 종류 | 조건 | 안전 확보 원리 |
|---|---|---|
| Value types (struct, enum) | 모든 저장 프로퍼티 / associated value의 타입이 Sendable | 전달 시 복사되어 각자 독립 → 공유 mutable 상태 없음 |
| Reference types — mutable 상태 없음 | 저장 프로퍼티가 전부 불변(let) + 타입도 Sendable, final | 아무도 못 바꿈 → 공유해도 안전 |
| Reference types — 내부에서 접근 관리 | lock/queue로 직접 동기화 + @unchecked Sendable | 모든 접근을 직렬화 → 동시 접근 차단 |
| Functions & closures | @Sendable 표시 + by-value 캡처 + 캡처값이 Sendable | 캡처가 값 복사라 외부와 단절 |
핵심 타입은 : Sendable로 conform하지만, 함수·클로저는 값이라 conform 대상이 없어 @Sendable 속성으로 표시한다.
value semantic type이 뭐야?
변수에 담거나 전달할 때 복사본이 넘어가, 한쪽을 바꿔도 다른 쪽에 영향이 없는 타입. struct·enum·Int·String·Array 등. 반대는 reference semantics(class) — 참조(주소)를 공유해 한쪽 변경이 모두에 반영된다. 값 타입은 공유 상태가 없어 Sendable 조건을 만족하기 쉽다.
class도 protocol을 conform해서 써?
그렇다. UIKit의 delegate / dataSource가 대표 사례다. Swift는 단일 상속이지만 protocol은 여러 개 채택 가능. 상속 부모는 맨 앞에, 그 뒤에 protocol들을 쓴다. weak delegate를 잡으려면 protocol을 AnyObject로 제한(class 전용)해야 한다.
associated value가 뭐야?
enum의 각 case에 딸려 저장되는 데이터. case qrCode(String)의 String이 associated value다. enum이 Sendable이려면 모든 case의 associated value 타입이 전부 Sendable이어야 한다. struct의 "저장 프로퍼티"에 해당하는 enum 버전이다. (case에 고정 상수를 부여하는 raw value와는 다른 개념)
"내부에서 상태 접근을 관리하는 참조 타입"이란?
var mutable 상태가 있어도, class 스스로 lock(NSLock)이나 serial queue로 모든 접근을 직렬화하면 안전하다. 단 컴파일러가 검증 못 하므로 @unchecked Sendable과 함께 쓴다. 이것을 언어 차원에서 자동화한 것이 actor다.
Apple 문서: "a class must: Be marked final; Contain only stored properties that are immutable and sendable; Have no superclass or have NSObject as the superclass."
| 조건 | 이유 — 무엇을 막나 |
|---|---|
① final | 하위 클래스가 var mutable 상태를 추가하는 구멍을 막음 |
② let + Sendable 타입 | 자기 자신의 mutable 상태 제거. (let만으론 부족 — 가리키는 객체 내부가 변할 수 있어 타입도 Sendable이어야) |
| ③ 부모 없음 / NSObject만 | 상위 클래스가 물려주는 var 유입을 막음 |
class에 let만 있으면 되는 거 아냐?
부족하다. ① let의 타입도 Sendable이어야 하고(let box: Box는 참조만 고정, box.n은 변경 가능), ② final이어야 한다. 셋 다 충족해야 Sendable.
let으로 된 class는 어차피 내부를 못 바꾸잖아? 왜 final이 필요해?
그 class 자체는 못 바꾸는 게 맞다. 하지만 class는 상속된다. 누군가 상속해 var를 추가하고, 그 하위 인스턴스를 부모 타입으로 위장해 넘기면 mutable 상태가 도메인을 넘는다. final이 상속 자체를 막아 "이 타입의 모든 객체는 불변"을 컴파일러가 확정하게 한다. struct/enum은 상속이 없어 이 문제가 원천적으로 없다.
"no superclass or NSObject as superclass"가 뭐야?
부모가 아예 없거나, 있어도 NSObject만 허용. 일반 class를 상속하면 그 부모의 var를 물려받아 안전이 깨진다. NSObject는 ObjC 루트 클래스로 동시성 안전을 깨는 mutable 저장 프로퍼티를 노출하지 않고, iOS의 수많은 API가 NSObject 상속을 요구하므로 실용성을 위해 예외로 열어둔 것.
non-final class에 : Sendable 붙이면 컴파일 안 돼, 아니면 그냥 조심하라는 경고야?
컴파일 에러로 막힌다. Non-final class 'X' cannot conform to 'Sendable'; use '@unchecked Sendable'. 통과시키려면 final로 만들거나(진짜 안전), @unchecked Sendable로 검사를 끄거나(안전은 개발자 책임 = 이때가 "조심하라").
@unchecked Sendable — 컴파일러 검사 끄기Apple 문서: "To declare conformance to Sendable without any compiler enforcement, write @unchecked Sendable. You are responsible for the correctness."
| 선언 | 결과 |
|---|---|
class X: Sendable (non-final) | 컴파일 에러 막힘 |
final class X: Sendable | OK 컴파일러가 안전 보장 |
class X: @unchecked Sendable | OK·위험 안전은 개발자 책임("조심") |
@unchecked는 검사를 끄는 것이라 lock을 빠뜨리면 그대로 데이터 레이스가 난다. actor를 먼저 고려하고, 레거시·성능상 불가피할 때만 lock + @unchecked를 쓴다. @unchecked는 "conformance를 같은 파일에 둬야 한다"는 규칙도 함께 해제한다.
Apple 문서: struct/enum이 요구를 만족하면 다음 경우 암묵적으로 Sendable에 conform한다 — Frozen 이거나, public이 아니고 @usableFromInline도 아닐 때.
struct도 내부 변수가 let이어야 Sendable 아냐?
아니다. class와 달리 struct/enum은 var가 있어도 Sendable이 된다. 값 타입은 전달 시 복사되어 각 도메인이 자기 복사본을 가지므로 남의 것에 영향을 못 준다. 진짜 조건은 "멤버·associated value의 타입이 Sendable인가" 뿐. let 강제는 class에만 해당.
"public이 아니고 @usableFromInline도 아닐 때"가 뭔 말이야?
모듈 경계(라이브러리 진화) 문제다. internal(모듈 내부 전용) 타입은 쓰는 코드가 전부 같은 모듈에 있어 컴파일러가 안전을 확인할 수 있다 → 자동 인정. public 타입은 외부 모듈이 쓰고 작성자가 나중에 non-Sendable 멤버를 추가할 수 있어 자동 인정 못 함 → 직접 명시 필요. @usableFromInline은 internal이지만 @inlinable로 외부에 노출되는 효과라 public과 같이 제외된다. @frozen은 "구조 고정" 약속이라 public이어도 자동 인정.
| 타입 상황 | 자동 Sendable? |
|---|---|
| internal(기본) struct/enum + 멤버 Sendable | 자동 |
@frozen struct/enum | 자동 |
public struct/enum | 직접 : Sendable 명시 |
@usableFromInline struct/enum | 직접 명시 |
Sendable은 메서드/프로퍼티 요구는 없지만 컴파일 타임에 강제되는 의미적 요구가 있다. 그리고 Sendable conformance는 타입 선언과 같은 파일에서 선언해야 한다(@unchecked는 예외).
@MainActor class는 암묵적으로 SendableApple 문서: "Classes marked with @MainActor are implicitly sendable, because the main actor coordinates all access to its state."
왜 @MainActor면 final·let 없이도 Sendable이야?
위험은 "여러 도메인이 같은 mutable 상태를 동시에 만지는 것"이었다. @MainActor는 모든 접근을 메인 액터(메인 스레드) 한 줄로 직렬화한다 → var여도 동시 접근 불가 → 데이터 레이스 불가 → 자동 Sendable. 일반 Sendable class가 "불변화"로 안전을 얻는다면, @MainActor는 "격리화"로 얻는다.
actor와 뭐가 달라?
같은 원리다. @MainActor는 사실상 "메인 스레드 전용 actor". actor가 자동 Sendable인 것과 동일한 이유로 @MainActor 타입도 자동 Sendable. UIKit/SwiftUI의 UI 객체(VC, VM)는 메인 스레드 전용이라 @MainActor가 자연스럽다.
@Sendable 함수와 클로저Apple 문서: "you mark sendable functions and closures with the @Sendable attribute. Any values that the function or closure captures must be sendable. ... sendable closures must use only by-value captures."
클로저가 값을 바꿔 동시성 문제를 일으킬까 봐 @Sendable을 쓰는 거야?
방향이 거꾸로다. @Sendable은 "문제를 막는 장치"가 아니라 "이 클로저는 도메인 경계를 넘는다"는 표식이다. 그 표식이 붙으면 컴파일러가 위험한 캡처(외부 var 변경, non-Sendable 캡처)를 검사해 막아준다. 보통 내가 붙이는 게 아니라 Task.detached 같은 API가 @Sendable을 요구해서 자동 적용된다. 일반 클로저는 같은 도메인에서만 돌아 검사 대상이 아니다.
var는 let으로 복사해 보내고, class의 내부 var는 안 된다는 거지?
맞다 — 단 이유가 다르다. 값 타입은 let으로 복사하면 진짜 복사본이라 단절돼 안전. class는 let box로 잡아도 캡처되는 건 참조의 복사본이라 여전히 같은 객체를 가리킨다 → 같은 mutable 상태를 공유 → non-Sendable이라 캡처 불가. class를 넘기려면 그 타입을 Sendable로 만들거나(final+let / @MainActor / actor / @unchecked+lock), var 값만 꺼내 let으로 복사해 넘긴다.
① 외부 변수를 참조로 잡아 변경하지 말 것(값 복사만) ② 캡처한 값의 타입도 Sendable일 것. Task.detached(priority:operation:)처럼 @Sendable을 기대하는 문맥에선 조건만 맞으면 명시 없이 자동 인정된다.
| 값 타입 (struct/enum/Int) | 참조 타입 (class) | |
|---|---|---|
let으로 복사하면 | 진짜 복사 → 원본과 단절 → 안전 | 참조만 복사 → 같은 객체 공유 → 위험 |
내부 var | 복사본의 var라 무관 (Sendable 가능) | 공유되는 var → 데이터 레이스 (불가) |
| Sendable 조건 | 멤버 타입이 Sendable이면 됨 | final + let + Sendable 타입 + 부모 제한 |
| 상속 | 없음 → final 개념 불필요 | 가능 → final로 막아야 함 |
| 다른 우회로 | — | @MainActor / actor / @unchecked+lock |
var가 있어도 Sendable이 되는가? → 된다(값 복사). 조건은 멤버 타입의 Sendablelet만으로 충분한가? → 아니다. 타입도 Sendable이어야 하고 final이어야 한다@unchecked로만 우회, 책임은 개발자)@MainActor class가 자동 Sendable인 이유는? → 메인 액터가 모든 접근을 직렬화하므로@Sendable 클로저의 캡처 규칙은? → by-value 캡처 + 캡처값 Sendable