Continuation은 Swift에서 비동기 작업을 동기 코드처럼 작성할 수 있게 돕는 중요한 개념이라고 한다.
특히 Swift의 async
/await
구문과 함께 사용될 때 유용하다고 하는데, Continuation은 기존의 콜백 기반 비동기 API를 async
/await
패턴으로 변환할 때 주로 사용되는 것이라고 한다.
아래에 Continuation의 개념과 사용법과 예제까지 작성해봤다.
Continuation은 Swift에서 비동기 코드를 작성할 때 사용되는 메커니즘인데 실행 흐름을 특정 지점에서 "중단(pause)"하고 이후에 특정 조건이 만족되면 "재개(resume)"할 수 있다고 한다.
그래서 기존의 복잡한 콜백 체인을 간소화해줄 뿐더러 비동기 작업을 동기 코드처럼 읽고 작성할 수 있게 해주는데
Swift에서 Continuation은 withCheckedContinuation
또는 withCheckedThrowingContinuation
을 통해 구현이 된다.
저 두 개는 비동기 함수 안에서 호출되고 콜백 기반 API를 async
함수로 변환할 때 유용하게 사용할 수 있다고 하니, 계속 알아보자.
withCheckedContinuation
단순 Continuation으로 에러 처리가 필요 없는 경우 사용된다.
withCheckedThrowingContinuation
에러 처리를 포함한 Continuation으로 에러를 발생시킬 가능성이 있는 비동기 작업에 적합하다.
func fetchData() async -> String {
await withCheckedContinuation { continuation in
// 비동기 작업이 완료되면 continuation.resume 호출된다.
someAsyncAPICall { result in
continuation.resume(returning: result)
}
}
}
func fetchData() async throws -> String {
try await withCheckedThrowingContinuation { continuation in
someAsyncAPICall { result, error in
if let error = error {
continuation.resume(throwing: error)
} else if let result = result {
continuation.resume(returning: result)
}
}
}
}
URLSession
의 콜백 기반 API를 async
/await
로 변환하는 예제를 살펴보면,
func fetchData(from url: URL) async throws -> Data {
try await withCheckedThrowingContinuation { continuation in
let task = URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error {
continuation.resume(throwing: error)
return
}
if let data = data {
continuation.resume(returning: data)
} else {
continuation.resume(throwing: URLError(.badServerResponse))
}
}
task.resume()
}
}
이 코드는 기존의 콜백 기반 URLSession
을 async
/await
로 변환한 예시다. 이렇게 더 간결하고 읽기 쉬운 코드를 작성할 수 있다.
Continuation은 반드시 한 번만 호출해야 함
resume
을 두 번 이상 호출하면 런타임 에러가 발생할 수 있다고 한다.
메모리 관리에 주의
비동기 작업이 강한 참조 순환을 유발할 수 있기에 [weak self]
와 같은 캡처링 규칙을 따르는 것이 좋다고 한다.
테스트와 디버깅
withCheckedContinuation
은 런타임에서 Continuation이 적절히 호출되지 않았는지 확인해주는데 이게 디버깅 단계에서 유용하다고 한다.
코드 가독성 향상: 콜백 지옥을 피하고, 동기 코드처럼 비동기 작업을 작성할 수 음.
async/await과 완벽한 조화: 기존 API와 새로운 비동기 패턴을 쉽게 연결할 수 있음.
에러 관리 간소화: try
와 함께 사용해 직관적인 에러 처리가 가능함.
Continuation를 활용하면 복잡한 비동기 작업도 간단하고 유지보수하기 쉬운 코드로 작성할 수 있게 되는걸 예시를 통해 확인했는데,
특히 기존의 콜백 기반 API를 현대적인 async
/await
방식으로 변환하는데 탁월한 방법이라고 한다.
기존의 콜백 기반 API를 Continuation으로 변환하는 과정이 생각보다 까다롭다고 느껴졌다. 특히 어디에서 resume을 호출해야 하는지 파악하지 않으면 코드 흐름이 꼬이기 쉽겠다고 생각이 들었고 ..
withCheckedThrowingContinuation을 사용해 에러를 처리하는 방식은 기존의 방식과 조금 다르게 느껴졌지만, 익숙해지니 훨씬 명확하고 간결했다.
"기존 방식"이란 비동기 작업을 처리할 때 일반적으로 사용되던 콜백 기반의 에러 처리 방식이다
.
.
기존 방식 (콜백) | withCheckedThrowingContinuation 사용 |
---|---|
에러와 결과를 콜백 클로저에서 처리해야함 | try 와 do-catch 를 사용해 에러 처리 가능함 |
클로저 중첩으로 가독성이 떨어질 수 있음 | 비동기 작업이 동기 함수처럼 읽히므로 가독성 향상됨 |
상태 관리와 흐름 제어가 복잡할 수 있음 | async /await 로 순차적인 흐름 제어 가능함 |
Result 타입으로 에러와 데이터를 전달 | Continuation을 통해 에러와 결과를 나누어 처리함 |
withCheckedThrowingContinuation
을 사용하면 에러 처리가 throws
를 통해 통합적으로 이루어지니 기존의 복잡한 콜백 구조에 비해 간단해보이기는 했다. 그런데 이것도 익숙해지기 전에는 Continuation의 resume
호출 방식을 생소하다고 생각하긴 했지만..
그래도 이렇게 기존 방식과 Continuation의 장단점을 비교해보니 Continuation이 가져다주는 코드 개선의 가치가 좀 더 높다는 건 분명히 보였다.