2023년 1월 1일
08:00 AM
Buffering ...

최근 글 👑

Swift Concurrency - Sendable 프로토콜과 컴파일러 검사

2025. 8. 20. 11:30ㆍSwift
반응형

 

Sendable 프로토콜이란?

 

Swift Concurrency에서는 데이터의 안전성(Data Safety) 을 매우 중요하게 다룹니다.

비동기 코드에서 여러 TaskActor가 동시에 같은 데이터에 접근할 수 있기 때문에,

잘못된 동기화로 인해 데이터 레이스(Data Race)가 발생할 수 있습니다.

더보기

데이터 레이스란? 오늘 Mini의 Concurrency 피어마켓에서 들었는데, 
var count = 0

count = count + 1  //스레드 A

count = count + 1 //스레드 B

상황에서 

- 스레드 A가 count를 읽어 0이라고 판단 -> 1증가 준비

- 동시에 스레드 B도 count를 읽어 0이라고 판단 -> 1증가 준비

- 두 스레드 모두 count = 1을 저장 -> 최종값은 1이 됨

But 우리 의도는 2여야 됨

이런 상황이 데이터 레이스 입니다 !

 

이를 방지하기 위해 Swift는 Sendable 프로토콜을 제공합니다.

Sendable“이 타입이 안전하게 스레드 간에 전달될 수 있다”라는 사실을 컴파일러에 알려주는 프로토콜입니다.

 

즉, Sendable을 준수하는 타입은 동시성 환경에서도 안전하게 공유할 수 있는 타입이라는 의미예요.

 

 

Sendable 프로토콜의 역할

 

Sendable동시성 환경에서 안전하게 전달할 수 있는 타입임을 보장하는 프로토콜입니다.

즉, 동시성 영역 간에 값을 전달할 때 해당 값이 안전하게 사용될 수 있는지 컴파일러가 검사해줍니다.

 

Swift 5.5 이상에서는 Sendable이 표준 라이브러리 안에 포함되어 있으며, async/await, Task, TaskGroup 같은 동시성 기능을 사용할 때 자동으로 검사 대상이 됩니다.

func performTask(_ value: Sendable) async {
    // 안전하게 다른 Task로 전달 가능
}

 


 

값 타입(Value Type)과 Sendable의 관계

 

Swift의 값 타입(Struct, Enum, Tuple)복사(Copy)를 통해 데이터가 전달되기 때문에

동시성 환경에서 비교적 안전합니다.

예를 들어, Int, String, Array 같은 표준 라이브러리 값 타입들은 대부분 Sendable을 자동으로 채택합니다.

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

func printUser(_ user: User) async {
    print(user.name)
}

위 예제에서 User는 단순한 값 타입이고, 내부에 모두 Sendable을 준수하는 프로퍼티만 있기 때문에

컴파일러가 자동으로 Sendable을 만족한다고 판단합니다.

 


 

참조 타입(Reference Type)과 Sendable의 제약

 

반대로, 참조 타입(Class)은 인스턴스를 여러 스레드에서 공유하기 때문에 데이터 레이스 위험이 있습니다.

따라서 클래스가 Sendable을 채택하려면 안전성 보장을 직접 해줘야 해요.

 

예를 들어, @MainActor로 동기화되거나, 불변(Immutable)하게 설계된 클래스라면 Sendable을 적용할 수 있습니다.

 

final class SafeLogger: Sendable {
    let messages: [String]

    init(messages: [String]) {
        self.messages = messages
    }
}

하지만 내부 프로퍼티가 var로 선언되어 있고 동기화 장치가 없다면,

컴파일러에서 “Sendable을 준수하지 않는다”는 경고를 띄웁니다.

 

 

값 타입(Value Type)과 참조 타입(Reference Type)에서의 차이

 

Swift에서 구조체(struct)나 열거형(enum) 같은 값 타입은 기본적으로 Sendable자동 준수합니다.

값 타입은 복사(copy)를 통해 전달되기 때문에, 다른 스레드에서 접근해도 원본 데이터의 안전성이 보장됩니다.

 

반면, 클래스(class)는 참조 타입이기 때문에 주의해야 합니다.

여러 스레드가 동일한 인스턴스에 동시에 접근할 수 있어 데이터 경합 가능성이 존재합니다. 따라서 Swift에서는 클래스가 Sendable을 따르려면 다음 조건을 만족해야 합니다.

 

  • final 키워드 사용 → 상속 금지
  • 저장 프로퍼티가 모두 Sendable 타입일 것
  • 동기화 메커니즘이 필요 없는 불변성(immutable) 구조 유지

@unchecked Sendable

 

때로는 개발자가 안전성을 보장할 수 있지만, 컴파일러가 이를 알지 못하는 경우가 있습니다.

이럴 때는 @unchecked Sendable을 사용하여 강제로 컴파일러 검사에서 제외할 수 있습니다.

final class UnsafeLogger: @unchecked Sendable {
    var messages: [String] = []

    func addMessage(_ message: String) {
        messages.append(message)
    }
}

하지만 @unchecked Sendable위험할 수 있으므로 최후의 수단으로만 사용하는 게 좋아요.


컴파일러 검사로 얻는 이점

 

Swift Concurrency에서 Sendable 검사를 활성화하면,

컴파일 타임에 데이터 안전성을 보장할 수 있습니다.

 

즉, 실행 중 발생할 수 있는 데이터 레이스동기화 문제

미리 차단할 수 있다는 장점이 있습니다

// 컴파일러가 자동으로 Sendable 위반을 잡아줌
func updateUser(_ user: User) async {
    Task.detached {
        // user가 Sendable이 아니면 여기서 에러 발생!
        print(user.name)
    }
}

 

요약

 

  • Sendable스레드 간 안전하게 전달할 수 있는 타입을 보장하는 프로토콜
  • 값 타입 → 대부분 자동으로 Sendable 채택
  • 참조 타입 → 불변성, @MainActor, 또는 동기화 설계 필요
  • @unchecked Sendable → 안전성 보장 시 강제 적용 가능, 하지만 주의 필요
  • 컴파일러 검사로 데이터 레이스를 사전에 방지할 수 있음
반응형