본문 바로가기

Swift 5.5의 Continuation

기술적인 이야기/애플 플랫폼 개발 2021. 7. 16.
반응형

Swift 5.5의 동시성(Concurrency) 기능의 강화로 드디어 asyncawait를 사용할 수 있게 되었다. 하지만 우리가 사용하게 될 기존의 프레임워크들은 여전히 이런 연속성 구현이 빠진 형태도 많을 것이고 레거시도 종종 사용하게 될 수도 있다.

다행히도 Swift 5.5에서 이런 동시성 인터페이스를 제공하기 힘든 코드를 동시성 코드에 어울리게 쓸 수 있는 방법이 제공된다. 이름하야 연속성(Continuation)이라 불리는 방법이다. 개발자가 약간 고생해서 감싸는(wrapping) 코드를 작성해야 되겠지만 말이다.

문제가 될 만한 예제

예를 들어 아래와 같은 인터페이스의 함수를 사용해야 한다고 생각해보자.

func fetchResults(name: String, completion: ([String]?) -> Void)

아마도 이름을 입력받아 무슨 일을 한 뒤에 completion 클로저를 비동기로 호출하는 그런 함수라고 생각된다. 물론 이 함수를 사용하는 방법은 간단하다.

fetchResults(name: "foo") { results in
    if let results = results {
        doSomeWork(with: results)
    } else {
        print("Upda")
    }
}

전통적으로 클로저를 이용하는 비동기 처리 방식이다.

하지만 이를 Swift 5.5의 동시성 스타일로 쓰려면 이 fetchResults 함수를 아래와 같은 형식으로 호출할 수 있는 편이 자연스럽고 편할 것이다.

let results = await fetchResults(name: "foo")

물론 현재는 위와 같이 호출할 수가 없다. 인터페이스가 전혀 맞지 않으니까.

자 그럼 이 문제를 풀어보자.

with checked continuation

이 문제 해결을 위해 제공되는 함수가 바로 withCheckecdContinuation이다. 원형은 아래처럼 생겼다.

func withCheckedContinuation<T>(
    function: String = #function,
    _ body: (CheckedContinuation<T, Never>) -> Void
) async -> T

뭔가 인터페이스가 무시무시하다. 특히 첫 매개변수 function의 기본 값이 #function이다. 이 값은 이 함수를 사용하는 함수의 이름이 문자열로 전달된다. 내부에서 뭔가 처리하는 데 필요하니 받는 것 같지만 일단 이번 글에서 이 매개변수는 따로 명시하지는 않는다. 안심하고 지나가자.

어쨌든 이를 써서 우리가 원하는 형태로 감싸 볼 수 있다.

func fetchResults(name: String) async -> [String]? {
    await withCheckedContinuation { continuation in
        // 레거시 코드 호출
        fetchResults(name: name) { results in
            continuation.resume(returning: results)
        }
    }
}

fetchResults라는 함수 이름을 그대로 쓰고 있어서 헷갈릴 수도 있는데, withCheckContinuation 함수에 넘기는 클로저 내부에서 호출하는 것이 바로 우리가 이전에 사용하던 레거시 함수다. 이 함수를 동일한 이름으로 감싸서 클로저 대신 비동기로 반환 값을 받을 수 있는 함수로 변신시킬 수 있다.

위에서 continuation이라는 클로저 매개변수가 좀 특별한데 CheckedContinuation이라는 구조체로 내부에는 여러 멤버가 있지만 핵심적인 것이 바로 여기서 사용한 것이 바로 resume이다. 특히 resume(returning: T)를 사용해 태스크 반환값을 조작(?)할 수 있게 된다.

결과적으로 이 함수를 이용해서 원하던 형식의 코드를 작성할 수 있게 된다.

if let results = await fetchResults(name: "foo") {
    doSomeWork(with: results)
}

동시성(continuation)을 이용할 수 있게 되면서 이렇게 아름다운 코드가 나올 수 있게 되었다. 마치 아이폰과 맥을 연속성이란 이름의 기능으로 서로 연결하듯이 과거(레거시)와 현재를 이어준다.

감싸는 함수 이름을 일부러 같게 한 것은 앞서 설명했지만 withCheckContinuation()의 첫 매개변수가 함수 이름을 전달받기 때문이다. 아마도 감싸는 함수 이름과 호출되는 함수 이름은 같아야 하지 않을까? 다만 추측이며 확신은 없다. 다른 예제들을 찾아봐도 다들 이름은 동일하게 지었다.

예외 처리 지원

앞에서 사용한 withCheckContinuation 함수를 검색해봤다면 비슷한 이름의 함수를 봤을 수도 있다. 바로 withCheckedThrowingContinuation 함수다. Throwing 이라는 이름에서 뭔가 느낄 수 있는데 바로 예외처리까지 가능하게 해주는 함수다.

위에서 새로 작성한 감싸는 함수인 fetchResults를 예외처리를 이용할 수 있게 바꿔보자.

func fetchResults(name: String) async throws -> [String] {
    try await withCheckedThrowingContinuation {
        continuation in
        // 레거시 코드 호출
        fetchResults(name: name) { results in
            if let res = results {
                continuation.resume(returning: res)
            } else {
                // 예외가 발생한 경우
                continuation.resume(throwing:
                    FetchedResultsError.noResults
                )
            }
        }
    }
}

눈에 띄는 바뀐 점은 반환값에서 옵셔널이 사라졌다는 점과 try가 붙어있는 점도 있지만, 핵심적으로 .resume(throwing: ...) 코드 부분이 추가되었다는 점이 눈에 뜨인다. 이 코드가 특정 예외(exception)를 발생시키는 코드라는 점을 눈치챌 수 있을 것이다.

따라서 이제 이 함수를 이용하면 아래와 같은 식으로 코딩이 가능해진다.

do {
    let results = try await fetchResults(name: "foo")
    doSomeWork(with: results)
} catch FetchedResultsError.noResults {
    print("Upda")
}

이런 식으로 예외처리가 가능한 코드로 재탄생되었다.

그리고

아마도 위에서 소개한 함수나 구조체들의 레퍼런스 문서들을 살펴보면 다음과 같이 비슷하면서도 위험해(?) 보이는 이름들을 찾을 수도 있을 것이다.

  • UnsafeContinuation 구조체와 withUnsafeContinuation 함수
  • UnsafeThrowingContinuation 구조체와 withUnsafeThrowingContinuation 함수

이들의 사용법은 비슷한데 이름에서 차이점을 찾다면 Chekced와 Unsafe의 차이가 있다. 어감만 보면 뭔가 Unsafe 쪽은 보호받지 못한다는 느낌이 있다.

사실 이제야 설명하긴 좀 늦긴 했지만, Checked 시리즈의 경우는 동시 실행 시 단일 실행을 보장하는 함수라는 특징이 있다. 흔히 쓰던 말로 스레드 안정성(Thread-Safety)을 보장한다는 말이다.

반대로 Unsafe 쪽은 동시에 호출하면 병렬로 동시 실행될 가능성이 있다는 말이다. 따라서 이 경우 동시 실행으로 공유 객체를 조져버릴(?) 수도 있다는 말이 된다.

스레드 안정성이 생기면 성능 저하 이슈가 따라오다 보니 사정에 맞게 잘 골라서 사용하면 될 것 같다.

마무리

사실 이 글은 해외의 튜토리얼 포스팅의 일부분이 이해가 잘 안 되어서 그 부분들을 파고들다 탄생한 글이다. 내용도 결국 이렇게 하나의 포스팅이 될 정도로 커져버렸다. 생각보단 간단한 내용이 아니었다는 말일지도 모르겠다. 아니면 나만 이렇게 이해를 못 하는 걸지도 모르겠지만 말이다.

어쨌든 이렇게 또 하나의 지식을 건졌다. 하지만 이 기능도 얼마나 자주 활용될지는 잘은 모르겠다. 동시성 지원도 동일하지만 일단 Swift 5.5가 iOS 15 혹은 macOS Monterey 이상에서만 지원되다 보니 대중화되려면 시간이 많이 필요할 것 같다.

관련된 글들

728x90
반응형

댓글