async/await와 @SendableSwift로 비동기 작업을 하다 보면 async/await는 자연스럽게 접하게 되는데,
조금만 깊게 들어가 보면 @Sendable 같은 개념도 함께 따라온다.
async와 await란?먼저 정의부터 짚고 가보자.
async는 비동기 함수임을 나타내는 키워드고,
await는 비동기 함수의 결과를 기다리기 위한 키워드다.
즉, async는 약속, await는 그 약속을 기다리는 역할이라고 보면 된다.
func fetchName() async -> String {
return "Swift User"
}
Task {
let name = await fetchName()
print(name) // Swift User
}
Swift 5.5부터 도입된 이 문법은
비동기 코드도 동기처럼 읽기 쉽게 만들어주는 강력한 도구다.
비동기 함수 정의
func getUserInfo() async -> String {
try? await Task.sleep(for: .seconds(1))
return "User Info"
}
호출 예시
Task {
let info = await getUserInfo()
print(info)
}
await는 반드시 비동기 컨텍스트에서만 사용 가능하다.
즉, Task {} 같은 비동기 환경 안에서만 쓸 수 있다.
Task {}는 새로운 비동기 실행 컨텍스트를 열어주는 문이다.
@Sendable은 뭐가 다른 걸까?@Sendable은 뭔가 생소한 느낌이 있는데,
이것도 사실 async와 함께 쓰이다 보면 자주 마주치게 된다.
@Sendable은 클로저가 스레드 안전하게 실행될 수 있음을 컴파일러에 보장해주는 키워드다.
다시 말해, 비동기 환경에서 다른 스레드로 넘어갈 수 있는 클로저인지 아닌지를 판단하는 기준인 셈.
“왜 꼭 @Sendable을 붙이라고 할까?”
이유는 명확하다.
Swift의 동시성 모델은 스레드 안전성(thread safety)을 기본으로 하기 때문이다.
예를 들어 이런 코드가 있다고 해보자:
var name = "Foo"
Task {
await getFullName(delay: .seconds(1)) {
print(name) // ❌ 외부 변수 캡처 → 컴파일 에러 가능성
return "Result"
}
}
이 코드는 name이라는 외부 변수를 참조하고 있어서
동시성 환경에서는 데이터 충돌이나 레이스 컨디션이 생길 수 있다.
그래서 Swift는 컴파일 타임에 미리 알려준다.
“이 클로저는 @Sendable이 아닌데, 스레드 안전하지 않을 수 있어!”
@Sendable을 비유로 쉽게 이해해보자이 개념이 어렵게 느껴진다면, 이렇게 비유해보자.
Task는 "그 메모를 친구에게 넘겨서 대신 처리해달라는 상황"근데 메모에 이렇게 써 있다면?
“내 방 책상 위에 있는 파일을 꺼내서 정리해줘!”
친구는 내 방에 들어올 수 없는데…?
결국 그 일은 실패하고 만다. ⚠️
Swift에서도 똑같은 상황이 벌어진다.
그래서 Swift는 명시적으로 @Sendable을 붙이라고 요구하는 것이다.
해당 클로저가 사용하는 동안은 다른 스레드에서는 사용할 수 없도록 제한을 해버리는 것이다.
| 개념 | 설명 |
|---|---|
async | 비동기 함수를 정의할 때 사용 |
await | 비동기 함수의 실행을 기다릴 때 사용 |
Task {} | 비동기 작업을 실행할 새로운 컨텍스트 생성 |
@Sendable | 클로저가 다른 스레드에서 안전하게 실행될 수 있다는 것을 보장하는 키워드 |