Sendable을 활용한 안전한 데이터 공유
Swift의 동시성(Concurrency) 환경에서는 여러 Task가 동시에 실행될 수 있기 때문에, 데이터가 여러 스레드에서 안전하게 공유될 수 있도록 해야 합니다. 이를 위해 Swift에서는 Sendable 프로토콜을 도입했습니다.
이번 글에서는 Sendable의 개념, 사용법, 그리고 실전에서의 활용 방법을 자세히 살펴보겠습니다.
Sendable이란?
Sendable은 Swift에서 데이터가 안전하게 Task 간에 전달될 수 있는지를 보장하는 프로토콜입니다.
Swift에서는 비동기 코드에서 객체가 여러 Task 간에 공유될 때 데이터 레이스(Data Race) 문제가 발생할 수 있습니다. 데이터 레이스가 발생하면 예측할 수 없는 버그가 발생할 가능성이 높으며, 프로그램의 안정성을 떨어뜨릴 수 있습니다. 이를 방지하기 위해 Sendable을 활용하면, 컴파일 타임에서 데이터의 안전성을 확인할 수 있도록 도와줍니다.
Sendable 적용 예제
struct SafeCounter: Sendable {
let value: Int
}
위와 같이 Sendable을 적용하면 SafeCounter 구조체는 여러 Task에서 안전하게 공유될 수 있습니다. Swift의 모든 값 타입(struct, enum)은 기본적으로 Sendable을 따릅니다. 하지만, 클래스(class)는 다릅니다.
클래스에서 Sendable 적용하기
Swift에서 actor는 class와 유사한 참조 타입(reference type)입니다. 다만 actor는 자동으로 동시성 안전성을 보장하며, 내부 속성에 접근할 때 항상 await를 사용해야 합니다.
🔹 class는 안전하지 않음
class Counter {
var value: Int = 0
func increment() {
value += 1
}
}
이 코드에서 Counter는 여러 Task가 동시에 increment()를 호출할 경우 데이터 충돌이 발생할 수 있습니다. 따라서 이런 경우에는 Sendable을 적용할 수 없으며, 개발자가 직접 동기화 처리를 해야 합니다.
final class SafeClass: Sendable {
let value: Int
init(value: Int) {
self.value = value
}
}
이처럼 final 키워드를 사용하고, 내부 속성을 let으로 선언하면 Sendable을 준수할 수 있습니다. 다만, 이 경우 클래스 내부의 값을 변경할 수 없으므로 불변(immutable) 객체일 때만 적용 가능합니다.
🔹 변경 가능한 Sendable 클래스 사용하기
만약 클래스 내부의 값을 변경해야 한다면, 다음과 같은 방법을 고려할 수 있습니다:
1️⃣ actor 사용하기 (가장 안전한 방법)
actor SafeCounter {
private var value: Int = 0
func increment() {
value += 1
}
func getValue() -> Int {
return value
}
}
✅ actor는 내부 접근을 자동으로 직렬화하여 데이터 레이스를 방지합니다.
actor와 class의 차이점
Swift에서
class는 참조 타입(reference type)이며, 다중 스레드 환경에서 동일한 객체를 공유할 경우 데이터 레이스가 발생할 위험이 있습니다. 반면,actor는class와 유사한 참조 타입이지만, 내부적으로 동시 접근을 직렬화하여 안전성을 보장합니다.
2️⃣ NSLock 또는 DispatchQueue.sync 사용하기
final class NSLockCounter: Sendable {
private let lock = NSLock()
private var value: Int = 0
func increment() {
lock.lock()
value += 1
lock.unlock()
}
func getValue() -> Int {
lock.lock()
let result = value
lock.unlock()
return result
}
}
final class DispatchQueueCounter: Sendable {
private let queue = DispatchQueue(label: "com.example.counterQueue")
private var value: Int = 0
func increment() {
queue.sync {
value += 1
}
}
func getValue() -> Int {
queue.sync {
return value
}
}
}
✅ NSLock또는 DispatchQueue.sync 을 사용하여 동기화 처리하여 안전하게 값을 변경할 수 있습니다.
3️⃣ @unchecked Sendable을 활용하여 직접 안전성 보장
final class UnsafeCounter: @unchecked Sendable {
private var value: Int = 0
func increment() {
value += 1 // 데이터 레이스 발생 가능!
}
}
❌ @unchecked Sendable을 사용할 경우, 컴파일러가 안전성을 검사하지 않으므로 개발자가 직접 동기화 처리를 해야 합니다.
위 방법 중 actor를 사용하는 것이 가장 권장됩니다. NSLock을 활용하는 방법도 동기화가 필요한 기존 클래스를 Sendable하게 만들 때 고려할 수 있습니다.
static 속성과 Sendable
static 속성은 클래스나 구조체에 속한 공유 데이터로, 여러 스레드에서 동시에 접근할 가능성이 높습니다. 따라서 static 속성이 동시성 안전성을 보장하는지 확인하는 것이 중요합니다.
static 속성이 Sendable을 준수하는 경우
기본적으로 let으로 선언된 static 속성은 변경되지 않기 때문에 Sendable을 따릅니다.
struct SafeConfig: Sendable {
static let apiEndpoint = "https://api.example.com"
}
이 경우 apiEndpoint는 불변(immutable) 값이므로 여러 Task에서 안전하게 공유할 수 있습니다.
기존 Singleton 패턴과 actor
기존의 Singleton 패턴을 사용하던 코드들은 actor 내부에 static let shared로 선언하면 동시성 안전성을 보장할 수 있습니다.
기존의 Singleton 패턴 (안전하지 않음)
final class Logger {
static let shared = Logger()
private init() { }
func log(_ message: String) {
print("[LOG]: \(message)")
}
}
하지만 위 코드에서는 다중 스레드 환경에서 log(_:) 메서드가 동시에 호출될 경우 안전성을 보장할 수 없습니다.
actor를 활용한 안전한 Singleton
actor Logger {
static let shared = Logger()
private init() { }
func log(_ message: String) {
print("[LOG]: \(message)")
}
}
이렇게 하면:
Logger.shared.log(_:)호출 시, 동시 접근이 자동으로 직렬화(Serialization) 됨- 기존의
DispatchQueue.sync나NSLock같은 동기화 메커니즘 없이도 안전함 - 싱글톤을 유지하면서도 Swift Concurrency에 최적화된 코드가 됨
(물론 싱글톤을 제거하는게 더 좋겠지만, 때로는 어쩔 수 없는 경우도 있으니까요..)
@unchecked Sendable의 활용
@unchecked Sendable은 Swift가 특정 클래스의 동시성 안전성을 자동으로 검사하지 않도록 하는 키워드입니다. 즉, 개발자가 직접 해당 객체가 안전하게 동작할 것임을 보장해야 합니다. Swift가 Sendable 준수를 확인하지 않도록 우회하는 방법이지만, 신중하게 사용해야 합니다.
final class UnsafeClass: @unchecked Sendable {
var value: Int = 0
}
하지만, @unchecked Sendable을 사용할 경우 컴파일러가 동시성 안전성을 보장해주지 않기 때문에, 데이터 레이스가 발생할 가능성을 개발자가 직접 검토해야 합니다.
@unchecked Sendable을 신중하게 사용해야 하는 이유
- 데이터 레이스 가능성: Swift 컴파일러가 안전성을 검사하지 않으므로, 개발자가 직접 스레드 안전성을 보장해야 합니다.
- 동시성 오류 디버깅 어려움: 코드가 커질수록 데이터 경쟁 상태를 추적하는 것이 어려워질 수 있습니다.
- 안전한 대안 고려: 가능하면
actor나 명시적인Sendable구현을 고려해야 합니다.
Sendable 객체를 외부에서 접근하고 호출하는 방법
1️⃣ 일반적인 Sendable 객체 접근
Swift에서 Sendable을 준수하는 객체(예: 값 타입)는 여러 Task에서 안전하게 공유할 수 있습니다.
struct SafeCounter: Sendable {
let value: Int
}
let counter = SafeCounter(value: 10)
Task {
print(counter.value) // 여러 Task에서 안전하게 접근 가능
}
✅ 값 타입(struct, enum)은 기본적으로 Sendable을 따르므로 문제 없음
❌ 하지만 참조 타입(class)은 Sendable을 따르지 않으므로 주의가 필요함
2️⃣ actor를 외부에서 호출할 때 (await 필요)
actor는 동시성을 자동으로 보장하지만, 외부에서 접근할 때 await을 사용해야 함.
actor SafeCounter {
var value: Int = 0
func increment() {
value += 1
}
}
let counter = SafeCounter()
Task {
await counter.increment() // ✅ 반드시 await 필요
print(await counter.value) // ✅ actor 내부 속성 접근 시에도 await 필요
}
✅ actor 내부의 속성과 메서드는 자동으로 직렬화(Serialization)되어 안전
❌ 하지만 외부에서 호출할 때는 await을 반드시 붙여야 함
3️⃣ @unchecked Sendable 객체를 외부에서 접근할 때
@unchecked Sendable을 사용하면 컴파일러가 동시성 안전성을 확인하지 않음.
따라서 외부에서 접근할 때 개발자가 직접 안전성을 보장해야 함.
final class UnsafeCounter: @unchecked Sendable {
var value: Int = 0
func increment() {
value += 1
}
}
let counter = UnsafeCounter()
Task {
counter.increment() // ❌ 데이터 레이스 발생 가능 (안전하지 않음)
print(counter.value)
}
❌ @unchecked Sendable을 적용해도 멀티스레드 환경에서 동시 접근하면 데이터 레이스 발생 가능
✅ NSLock 같은 동기화 메커니즘을 직접 추가해야 안전
final class SafeCounter: @unchecked Sendable {
private let lock = NSLock()
private var value: Int = 0
func increment() {
lock.lock()
value += 1
lock.unlock()
}
}
✅ 동기화 메커니즘(NSLock, DispatchQueue.sync)을 활용해야 안전
언제 Sendable을 사용해야 할까?
- 비동기 함수에서 객체를 Task 간에 공유할 경우 → 값 타입(
struct)이 기본적으로 안전하지만, 클래스(class)는Sendable을 따르거나actor를 활용해야 함. - 멀티 스레드 환경에서 데이터 무결성이 중요한 경우 →
Sendable을 적용하면 코드의 신뢰성이 높아짐. Sendable경고가 발생하는 경우 → 경고가 발생하면, 데이터 레이스 문제가 있을 가능성이 크므로 이를 해결해야 함.- 타사의 비동기 라이브러리와 연동할 경우 →
Sendable을 준수하면 안전한 동작을 보장할 수 있음.
결론
Swift의 Sendable은 Concurrency 환경에서 데이터 레이스를 방지하는 중요한 도구입니다. 값 타입은 기본적으로 Sendable을 따르지만, 클래스는 명시적으로 준수해야 합니다. actor를 활용하면 기본적으로 안전하며, 필요할 경우 @unchecked Sendable을 사용할 수도 있습니다.
동시성 프로그래밍을 안전하게 구현하기 위해 Sendable을 적극적으로 활용하는 것이 중요합니다. 이제 여러분의 코드에서도 Sendable을 적용해보고, 데이터 레이스 없는 안전한 Swift 코드를 작성해시기 바랍니다!
⏰ 딜라이트룸에서 알라미와 함께 아침을 바꿀 분들을 모십니다 🙌
- 딜라이트룸 채용 공고 살펴보기 ⏰
(+ 여기서 지원하면 입사 축하금 100만원💸)