티스토리 뷰
Swift Concurrency를 API 요청 로직에 적용할때, 단일 요청뿐만 아니라 다수의 API 요청을 수행해야할 때가 있습니다.
이때 모든 API 요청을 Concurrent하게 진행하고, feeding(요청 결과를 실제 수신하는 과정)을 순차적으로 받는 방법을 사용하면 더욱 효율적인 비동기 작업을 수행할 수 있습니다.
이러한 것을 가능하게 해주는 개념 중 하나인 Structured Concurrency, async let에 대해서 코드를 함께 작성해보면서 알아보겠습니다.
structured concurrency, async let 사용하여 다수의 API 요청 concurrent하게 처리하기
// try await을 사용하였기에 equifaxUrl로부터 결과 값을 수신받을때까지 suspend 된다. ㅠㅠ equifaxUrl 요청이 끝나야 experianUrl로부터 요청을 수행한다..
// => Concurrently하게 두개 다 요청하는 방법?
// "Let's work on these two tasks(equifax, experian) concurrently!!"
// => then, how do we do that?? => async let!
// MARK: Async-let
// - async let을 사용하면, async 작업에 대한 reference를 잡고 있는다. 즉시 반환되며, concurrent task로 동작하게 된다.
// - async let을 붙였다면 뒤에 붙여 사용했던 try await은 명시하지 않아도 된다.(ex) 아래 코드의 URLSession 앞에 try await를 명시할 의무가 없음
// * 아래 equifaxData, experianData는 모두 async let으로 정의된다.
async let (equifaxData, _) = URLSession.shared.data(from: equifaxUrl)
async let (experianData, _) = URLSession.shared.data(from: experianUrl)
// custom code
// async throws 메서드로부터 async let 상수를 받은 것이므로, 이를 사용할때는 try await을 사용해야 한다.
// 아래와 같이 async let 값에 대한 await(try await)을 할때 비로소 suspend 된다! 따라서 async task는 동시에 동작시키고, 이후에 실제 값을 받는 부분에서 기다리는 것 => API 요청은 concurrently하게 하고, 받은 값을 feeding할때만 순차적으로 나눠줌.
let equifaxCreditScore = try? JSONDecoder().decode(CreditScore.self, from: try await equifaxData)
let experianCreditScore = try? JSONDecoder().decode(CreditScore.self, from: try await experianData)
위 코드는 특정 URL로 부터 데이터를 받아오는 코드입니다. 만약 각각의 URLSession.shared.data(from:) 사용 라인에 try await을 사용했다면, 첫번째 요청 작업이 완료되기 전까지 두번째 요청작업은 시작도 할수가 없습니다. 😢
그렇다면, 어떻게 두개의 작업을 Concurrent하게 진행할 수 있을까요? 바로 async let을 사용하는 방법이 있습니다.
async let을 사용하면 try await 키워드를 사용하던 코드는 실제 feeding 단계에 사용하게 됩니다. (try await, await을 사용하는 시기가 suspended 되는 시기임)
async let으로 선언을 하면 선언 당시 지은 상수 값으로 async 작업(위의 경우 URLSession.shared.data(from:))에 대한 reference를 잡고 있게 됩니다.
또한 async let을 사용하는 코드는 당장 suspended되지 않고 작업과 관련된 reference만 참조하고 곧바로 아랫라인을 실행합니다.
그렇게 위 async let 을 사용한 2개 라인은 concurrent하게 수행이 되고, 그 아래 async let 상수 앞에 try await을 사용한 코드 라인에서 suspended가 되고, 결과 수신을 기다리게 됩니다. (위 코드에서는 try await을 사용했지만, 그냥 await 만 사용하는 코드에 async let을 적용했다면, feeding 단계에서 await만 작성했을 거에요.)
즉, 다수의 async task는 concurrent하게 진행하고, 요청 결과를 기다릴때만 순차적으로 serial하게(async 동작 내에서) 기다릴 수 있는 것입니다. 그렇게 되면, 하나의 작업이 모두 끝날때까지 두번째 이후 작업을 진행하지 못했던 이전 방식보다 효율적으로 작업을 수행할 수 있는 것입니다.
이렇게 async let을 사용한 Concurrency를 Structured Concurrency라고 합니다. 그와 반대로 Unstructured Concurrency는 .task { ... }, Task { ... } 블럭을 사용할때를 의미합니다. (Udemy 강의로 참고한 내용인데, 혹시 잘못 된 내용이 있다면 의견 부탁드려요.)
제 개인앱 코드에 위 예시를 적용할 수 있는 코드를 발견해서 위 Structured Concurrency 방식인 async let을 적용해보는 시간을 가져보겠습니다.
실제 프로젝트에 Async-let Structured Concurrency을 적용하지 않는 경우 문제점
이번엔 실제 프로젝트에 async let을 적용해보겠습니다.
위 코드를 보시면, getMarketPriceListInfo, getFuturePriceListInfo 두개의 async API method를 요청하고 있는데요. 요청할때마다 try? await을 사용하고 있습니다.
이렇게 사용하면 각각의 async API를 요청할때마다 suspended상태가 되어서 첫번째 요청 작업이 끝나기 전까지 두번째 요청작업은 시작도 못하게 됩니다. 물론 모든 작업이 async하게 동작은 되겠지만, 좀 비효율적으로 돌아갈 수 있는 거죠.
결과적으로 getMarketPriceListInfo, getFuturePriceListInfo 각각 따로 요청이 진행되고, 두개가 전부 성공했을때, 튜플 데이터로 만들어서 데이터를 던져주고 있어요.
이번에는 async let을 사용해보겠습니다.
실제 프로젝트에 Async-let Structured Concurrency 적용방법
각각의 API 요청 작업에 try? await을 붙였던 방식 대신, async let을 앞에 붙혀주었어요. 이렇게 하면 async 작업 관련 reference만 반환받고 바로 아래 라인들을 수행합니다.
getMarketPriceListInfo, getFuturePriceListInfo 작업들이 concurrent하게 수행되는 것입니다.
이후에 결과값을 받는 위치에서만 suspend 해서 결과값을 순차적으로 받고, 튜플형태의 데이터로 던져주는 모습이에요.
두 개의 API 요청을 concurrent하게 진행한 이후, feeding을 순차적으로 받기 때문에 전자 방식보다 async let을 사용한 방식이 더 효율적으로 동작할 수 있는 것입니다.
앞서 구현했던 async 메서드를 외부(TCA의 Reducer)에서 사용하는 모습입니다. 외부에서 수정한 코드는 1도 없습니다. 메서드 내부만 async let을 도입해서 더욱 효율적인 비동기 작업을 수행할 수 있게 되었습니다.
TCA의 Reducer 내에서 await, try await 동작을 수행하고 싶을때 .task { ... }를 사용할 수 있습니다.
오늘은 structured concurrency라고 하는 async let을 사용해서 다수의 API 요청을 concurrent하게 수행할 수 있는 방법을 알아보았습니다.
이 외에도 AsyncStream, AsyncThrowingStream, withTaskGroup, withThrowingTaskGroup 등을 통해 AsyncSequence 개념을 활용한 for try await / for await 비동기 작업을 수행할 수도 있는데요. 나중에 한번씩 코드를 작성해보며 포스팅 해보도록 하겠습니다.
AsyncSequence에 대해 관심 있으시다면 아래 포스팅도 참고하세요.
관련 의견이나 질문 언제든 댓글 달아주세요. 감사합니다! 👩🏻💻
'iOS 개발 > iOS 개발 팁' 카테고리의 다른 글
iOS async await, escaping closure callback 메서드 개선방법 (0) | 2023.01.26 |
---|---|
iOS TDD, UnitTest UITest Target 생성 및 테스트코드 작성방법 (0) | 2023.01.24 |
Swift KVC, KeyPath, WritableKeyPath 읽기 쓰기 사용방법 (0) | 2022.10.23 |
Swift 5.1, Opaque Type 문법, protocol 타입 반환 간 사용방법 (0) | 2022.10.11 |
Xcode 팁, 한 화면에 다수의 editor 창 보이게 설정하는 방법 (0) | 2022.10.02 |
- Total
- Today
- Yesterday
- 자연어처리
- 프로토콜
- 프로그래머스
- 알고리즘
- 컬렉션
- CoreML
- 개발자문서
- 백준swift
- ios
- swift reduce
- 알고리즘문제
- swift언어
- swift문제
- 부스트코스
- createML
- swift 기초
- publisher
- SwiftUI
- swift알고리즘
- Swift 알고리즘
- 스위프트
- 프로그래머스swift
- swift 문자열
- Collection
- Protocol
- 김프매매
- swift
- uikit
- swift string
- 백준알고리즘
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |