[회고] 신입 iOS 개발자가 되기까지 feat. 카카오 자세히보기

🍎 Apple/Concurrency & GCD

[Concurrency] Continuation

inu 2023. 3. 31. 20:38
반응형

Continuation

Continuation

  • Continuation는 동기식 코드와 비동기식 코드 간의 인터페이스를 위한 메커니즘이다.
    • 즉, 동기식 코드를 비동기식 코드에 이식할 때 간단하게 인터페이스를 제공해줄 수 있는 것이다.
  • 그 종류에는 정확성 체크를 진행하는 CheckedContinuation와 체크하지 않는 UnsafeContinuation가 있다.
    • 정확성 체크란 누락되거나 여러 번 재개된 작업이 있는지 런타임 검사를 수행하는 것을 의미한다.
    • UnsafeContinuation는 오버헤드가 적은 것을 목표로 하기 때문에 불변성을 체크하지 않는다.

  • 대표적으로 사용하는 함수는 withCheckedThrowingContinuation(withCheckedContinuation)이다.
func getData(url: URL) async throws -> Data {
    return try await withCheckedThrowingContinuation { continuation in
        URLSession.shared.dataTask(with: url) { data, response, error in
            if let data = data {
                continuation.resume (returning: data)
            } else if let error = error {
                continuation.resume (throwing: error)
            }
        }
    }
}
  • 이런식으로 일반적인 동기식 코드를 async 스타일에 맞도록 만들어준다.
    • resume을 통해 데이터를 비동기적으로 리턴할 수 있다.
  • Continuation을 사용할 때 지켜야할 규칙이 있다.
    • 기본적으로 resume은 단 한번 호출되어야 한다. 두 번 이상 resume이 호출되면 예상치 못한 동작이 발생한다.
    • 아예 resume을 호출하지 않으면 코드가 영원히 await하게 된다. 이는 리소스 낭비로 이어진다.

CheckedContinuation Vs UnsafeContinuation

  • 우선 UnsafeContinuation를 사용해보자.
func getData(url: URL) async throws -> Data {
    return try await withUnsafeThrowingContinuation { continuation in
        URLSession.shared.dataTask(with: url) { data, response, error in
            if let data = data {
                continuation.resume (returning: data)
                continuation.resume (returning: data)
            } else if let error = error {
                continuation.resume (throwing: error)
            }
        }
    }
}

Task {
    guard let url = URL(string: "<https://www.google.com/url?sa=i&url=https%3A%2F%2Fwww.joongang.co.kr%2Farticle%2F25121160&psig=AOvVaw31JuPFpNklL68IoZGm0v12&ust=1678975033567000&source=images&cd=vfe&ved=0CBAQjRxqFwoTCOi_xMWL3v0CFQAAAAAdAAAAABAE>") else {
        print("Url error")
        return
    }
    do {
        let data = try await getData(url: url)
        print(data)
    } catch {
        print(error)
    }
}

// Result
// 1814 bytes
  • continuation에 두번 resume을 했음에도 정상적으로 동작이 수행되는 것처럼 보인다.
  • 다만 이는 예상치 못한 에러를 야기할 수 있는 위험한 코드이다. 에러의 이유도 알 수 없다.
  • 다음으로 CheckedContinuation를 사용해보자.
func getData(url: URL) async throws -> Data {
    return try await withCheckedThrowingContinuation { continuation in
        URLSession.shared.dataTask(with: url) { data, response, error in
            if let data = data {
                continuation.resume (returning: data)
                continuation.resume (returning: data)
            } else if let error = error {
                continuation.resume (throwing: error)
            }
        }
    }
}

Task {
    guard let url = URL(string: "<https://www.google.com/url?sa=i&url=https%3A%2F%2Fwww.joongang.co.kr%2Farticle%2F25121160&psig=AOvVaw31JuPFpNklL68IoZGm0v12&ust=1678975033567000&source=images&cd=vfe&ved=0CBAQjRxqFwoTCOi_xMWL3v0CFQAAAAAdAAAAABAE>") else {
        print("Url error")
        return
    }
    do {
        let data = try await getData(url: url)
        print(data)
    } catch {
        print(error)
    }
}

// Result
// 1814 bytes
// _Concurrency/CheckedContinuation.swift:164: Fatal error: SWIFT TASK CONTINUATION MISUSE: getData(url:) tried to resume its continuation more than once, returning 1814 bytes!
  • 두번 resume을 수행하려고 하면 명시적으로 에러가 발생함을 알 수 있다.
  • 이를 통해 개발자는 자신의 실수를 파악할 수 있을 것이다.
func getData(url: URL) async throws -> Data {
    return try await withCheckedThrowingContinuation { continuation in
        URLSession.shared.dataTask(with: url) { data, response, error in
        }
        .resume()
    }
}

Task {
    guard let url = URL(string: "https://www.google.com/url?sa=i&url=https%3A%2F%2Fwww.joongang.co.kr%2Farticle%2F25121160&psig=AOvVaw31JuPFpNklL68IoZGm0v12&ust=1678975033567000&source=images&cd=vfe&ved=0CBAQjRxqFwoTCOi_xMWL3v0CFQAAAAAdAAAAABAE") else {
        print("Url error")
        return
    }
    do {
        let data = try await getData(url: url)
        print(data)
    } catch {
        print(error)
    }
}

// SWIFT TASK CONTINUATION MISUSE: getData(url:) leaked its continuation!
  • resume을 아예 하지 않은 경우도 마찬가지이다.
반응형

'🍎 Apple > Concurrency & GCD' 카테고리의 다른 글

[Concurrency] Explore structured concurrency in Swift  (1) 2023.10.29
[Concurrency] Actor  (0) 2023.03.31
[Concurrency] Task  (0) 2023.03.31
[Concurrency] async / await  (2) 2023.03.31
[Swift] GCD 정리하기  (2) 2022.05.30