안녕하세요!
이전 글들( [Concurrency] Task 톺아보기, [Concurrency] 천천히 알아보는 Task.detached(), 언제 어떻게 써야 할까? )에서 Swift Concurrency의 기초와 비구조화 Task에 대해 정리했는데요.
이번 글에서는 그 연장선으로, 구조화된 병렬 작업을 Swift에서 어떻게 더 유연하고 효율적으로 처리할 수 있는지 함께 살펴보려 해요!
async let vs TaskGroup: 어떤 걸 언제 써야 할까?
async let과 TaskGroup은 모두 구조화된 병렬 처리를 위한 도구지만, 사용하는 방식과 적절한 상황은 꽤 다릅니다.
예를 들어, 이미지 여러 장을 한꺼번에 다운로드할 때는 async let이 직관적이고 간단하지만,
반복문 안에서 비동기 작업을 여러 개 실행해야 할 땐 TaskGroup이 훨씬 더 유연하게 동작하죠.
그래서 실제로 코드를 작성하다 보면 둘 중 어떤 걸 써야 할지 고민되는 순간이 자주 찾아오곤 해요.
단순히 문법을 아는 것만으로는 부족하고, 상황에 맞게 도구를 선택하는 감각이 더 중요하다고 생각해요.
이번 글에서는 async let과 TaskGroup을 나란히 비교하면서, 각각의 차이점과 쓰임새를 예제를 통해 천천히 살펴보겠습니다!
async let: 간단하고 직관적인 병렬 처리
앱을 개발하다 보면 동시에 여러 작업을 처리하고 싶은 순간이 정말 많은데요!
예를 들어:
- 이미지 여러 장을 동시에 다운로드하거나
- 여러 API를 동시에 호출해야 할 때
이럴 때 작업을 하나하나 기다리지 않고 동시에 실행할 수 있다면, 얼마나 효율적일까요?
이런 상황을 깔끔하게 해결해주는 도구가 바로 async let입니다!
await를 사용한다면? - 순차적 작업 처리
이 작업은 순서대로 하나씩 처리됩니다!
let firstPhoto = await downloadPhoto(named: photoNames[0])
let secondPhoto = await downloadPhoto(named: photoNames[1])
let thirdPhoto = await downloadPhoto(named: photoNames[2])
let photos = [firstPhoto, secondPhoto, thirdPhoto]
show(photos)
첫 번째 사진을 다 받아야 두 번째 작업이 시작되고, 두 번째가 끝나야 세 번째가 시작돼요.
세 개 작업을 동시에 할 수 있는데도, 이렇게 하면 총 세 번 기다려야 하니 전체 처리 시간이 늘어나겠죠?
async let을 사용한다면? - 병렬적 작업 처리
같은 작업을 async let을 쓰면 이렇게 바뀝니다!
async let firstPhoto = downloadPhoto(named: photoNames[0])
async let secondPhoto = downloadPhoto(named: photoNames[1])
async let thirdPhoto = downloadPhoto(named: photoNames[2])
let photos = await [firstPhoto, secondPhoto, thirdPhoto]
show(photos)
이 코드에서는 세 개의 다운로드가 동시에 시작되고, 마지막 줄에서 결과가 필요할 때 await을 사용해 한꺼번에 기다려요.
덕분에 결과를 기다리는 시점 전까지는 다른 코드들도 계속 실행될 수 있어서, 전체 흐름이 더 효율적이게 되죠!
async let의 한계
- async let은 선언된 함수의 스코프 안에서만 사용할 수 있음
- 반복문에서 작업 수가 동적으로 바뀌는 경우에는 사용하기 어려움
언제 어떤 방식을 쓰면 좋을까?
방식 | 실행 흐름 | 상황 |
await | 순차 실행 | 작업 순서가 중요하고, 앞 작업 결과가 다음에 필요할 때 |
async let + await | 병렬 실행 | 여러 작업이 서로 독립적이고, 동시에 실행해도 될 때 |
예를 들어, 여러 이미지를 다운받아 보여줄 때, 또는 각기 다른 서버에서 데이터를 가져올 때처럼 작업들이 서로 관련이 없다면 async let이 훨씬 효율적이겠죠?!
async let은 Swift의 구조화된 동시성에서 제공하는 기능 중에서도 가장 직관적이고, 처음 접했을 때도 부담 없이 쓸 수 있는 도구예요.
그렇다고 해서 모든 병렬 작업에 무조건 async let만 쓰는 게 좋은 건 아니겠죠?!
작업의 개수가 런타임에 따라 달라질 수 있거나, 반복문 안에서 여러 작업을 동시에 실행해야 한다면, 이때는 TaskGroup이 훨씬 더 유연하고 잘 맞는 선택이 될 수 있어요.
앞으로 이어지는 내용에서는 TaskGroup을 어떻게 활용하면 좋을지, 어떤 점에서 async let과 다른지를 자세히 살펴보려고 해요.
함께 알아봅시다!!!
TaskGroup: 반복문에 강한 유연한 병렬 처리
TaskGroup은 Swift에서 더 유연한 병렬 처리를 가능하게 해주는 도구예요.
- 작업 개수가 정해져 있지 않거나
- 반복문 안에서 여러 작업을 동시에 실행해야 할 때 매우 유용합니다.
또한 여러 개의 자식 태스크(child task)를 만들어 동시에 실행하고, 각 작업이 끝나는 순서대로 for await를 통해 결과를 받아올 수 있어요.
순서가 보장되진 않지만, 작업이 끝나는 대로 빠르게 처리할 수 있어서 병렬 처리에 딱이겠죠?!
💡 TaskGroup은 만든 Task 안에서만 사용해야 합니다.
밖으로 넘기려고 하면 Swift가 타입 시스템으로 막아주기때문인데요!
(작업의 생명 주기를 명확히 관리하려는 구조화된 동시성의 철학이 반영)
기본 사용법
await withTaskGroup(of: Int.self) { group in
for i in 1...3 {
group.addTask {
return i * 2
}
}
for await result in group {
print(result)
}
}
보통은 withTaskGroup(of:returning:body:)라는 함수를 사용해서 TaskGroup을 만들고, 그 안에서 group.addTask { ... } 형태로 원하는 만큼 작업을 추가해요. 작업이 전부 끝날 때까지 기다렸다가 결과를 수집하면 됩니다!
TaskGroup의 특징
- 작업 수가 유동적일 때 유리
- 반복문 안에서도 자연스럽게 사용 가능
- Task 내부에서만 사용 가능 → 구조화된 동시성 원칙 때문
전체적으로 보면, TaskGroup은 단순히 작업을 병렬로 처리하는 걸 넘어서, 작업 흐름을 상황에 맞게 유연하게 조율할 수 있게 해주는 도구라고 생각해요. 실전에서 병렬 처리를 조금 더 세밀하게 다뤄야 할 때, 꼭 기억해두면 좋겠죠?!
ThrowingTaskGroup: 실패할 수 있는 작업의 병렬 처리
ThrowingTaskGroup은 이름처럼 에러를 던질 수 있는 작업들을 병렬로 실행할 수 있게 해주는 TaskGroup의 확장 버전입니다.
기본적인 동작 방식은 TaskGroup과 비슷하지만, 각 작업이 throw를 통해 에러를 던질 수 있다는 점에서 차이가 있어요.
만약 그룹 안의 작업 중 하나라도 실패하면, 그 즉시 그룹 전체가 취소되고(for-cancellation), 에러는 상위로 전파됩니다.
그래서 작업 중간에 문제가 생겼을 때 빠르게 대응할 수 있죠.
기본 사용법
do {
try await withThrowingTaskGroup(of: Int.self) { group in
group.addTask {
throw SomeError()
}
for try await value in group {
print(value)
}
}
} catch {
print("에러 발생: \(error)")
}
ThrowingTaskGroup 특징
- 하나라도 실패하면 전체 작업이 취소
- try/catch로 에러를 한 번에 처리할 수 있음
- 불필요한 리소스를 아끼고 빠르게 실패를 감지할 수 있음
유용한 메서드
- group.cancelAll()
→ 그룹 안의 모든 작업을 수동으로 취소 - group.addTaskUnlessCancelled { ... }
→ 그룹이 이미 취소된 상태라면 새 작업을 추가하지 않음
즉,ThrowingTaskGroup은 에러 가능성이 있는 작업들을 동시에 실행해야 할 때 적합해요.
실패 시 전체를 빠르게 멈추고, 안전하게 에러를 처리할 수 있어요.
반복문, 다량의 네트워크 요청, 실패 복구가 필요한 상황에서 적극 활용할 수 있어요!
마지막으로 한번 TaskGroup과 async let을 표로 한번 정리해보겠습니다!
TaskGroup vs async let
구분 | async let | TaskGroup |
실행 개수 | 고정 | 유동적 (반복문 등) |
선언 위치 | 고정된 스코프 내 | 클로저 내부 자유롭게 |
결과 수집 | 개별 await 또는 배열로 한 번에 | for await 반복문 |
에러 처리 | 불가능 | ThrowingTaskGroup 사용 가능 |
유용한 상황 | 소수의 작업, 정적 병렬 처리 | 반복문 기반 동적 병렬 처리 |
그럼, 실제 앱 개발에서는 어떤 기준으로 선택하면될까요?
- 작업 수가 적고 고정적일 때 → async let
- 작업 수가 유동적이거나 반복문 안에서 처리할 때 → TaskGroup
- 실패 가능성이 있는 병렬 작업들을 안전하게 다룰 때 → ThrowingTaskGroup
마무리하며
이번 글에서는 Swift Concurrency에서 자주 마주치는 async let, TaskGroup, 그리고 ThrowingTaskGroup의 차이점과, 각각을 언제 어떻게 쓰면 좋을지 실제 예제를 통해 정리해봤어요.
이 도구들은 모두 병렬 작업을 구조화하는 데 정말 유용하지만, 상황에 따라 적절하게 선택하는 것이 성능이나 안정성에 큰 영향을 줄 수 있거든요.
결국 중요한 건 문법 그 자체보다, “왜 이걸 선택하는가?” 에 대한 기준을 갖는 거라고 생각해요.
이 글이 그런 기준을 세우는 데 조금이라도 도움이 되었으면 좋겠습니다!
혹시 글을 읽으면서 더 궁금한 점이 생기셨거나, 비슷한 주제로 더 깊이 다뤘으면 하는 부분이 있다면 편하게 알려주세요! 읽어주셔서 감사합니다!!
참고 자료
'iOS' 카테고리의 다른 글
iOS 데이터 저장 방식 알아보기 - SwiftData (0) | 2025.01.18 |
---|---|
iOS 데이터 저장 방식 알아보기 - Core Data (0) | 2024.12.16 |
iOS 데이터 저장 방식 알아보기 - File System (3) | 2024.11.14 |
APFS(Apple File System) (1) | 2024.11.13 |
iOS 데이터 저장 방식 알아보기 - UserDefaults 편 (1) | 2024.11.04 |