async/await와 @Sendable

BS_Lee·2025년 7월 13일

swift

목록 보기
17/21

async/await@Sendable

Swift로 비동기 작업을 하다 보면 async/await는 자연스럽게 접하게 되는데,
조금만 깊게 들어가 보면 @Sendable 같은 개념도 함께 따라온다.

asyncawait란?

먼저 정의부터 짚고 가보자.

async비동기 함수임을 나타내는 키워드고,
await비동기 함수의 결과를 기다리기 위한 키워드다.

즉, async는 약속, await는 그 약속을 기다리는 역할이라고 보면 된다.

func fetchName() async -> String {
    return "Swift User"
}

Task {
    let name = await fetchName()
    print(name)  // Swift User
}

Swift 5.5부터 도입된 이 문법은
비동기 코드도 동기처럼 읽기 쉽게 만들어주는 강력한 도구다.


async await 선언방법

비동기 함수 정의

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클로저가 다른 스레드에서 안전하게 실행될 수 있다는 것을 보장하는 키워드

0개의 댓글