이번 글에서는 TaskGroup에 대해 알아볼게요
기존에 Task를 이용해서 10개의 키워드에 대해 각각 네트워크 요청을 보내야 한다면 다음과 같이 해야했죠
Task {
for keyword in keywords {
let result = try await fetch(keyword)
}
}
이 코드는 1번 요청이 끝나야 2번이 시작되기 때문에 10개 요청이 각각 1초씩 걸린다면 총 10초가 걸려요. 이럴 때 사용하는게 TaskGroup이에요. TaskGroup은 여러 비동기 작업을 동적으로 생성하고, 모든 결과를 한 곳에서 모아 처리할 수 있는 구조체에요.
사용법은 withTaskGroup 또는 withThrowingTaskGroup으로 그룹을 만들어요
// 에러가 없을 때
let results = await withTaskGroup(of: String.self) { group in
group.addTask { await fetchA() }
group.addTask { await fetchB() }
group.addTask { await fetchC() }
var collected: [String] = []
for await result in group {
collected.append(result)
}
return collected
}
// 에러가 있을 때
let results = try await withThrowingTaskGroup(of: String.self) { group in
group.addTask { try await fetchA() }
group.addTask { try await fetchB() }
var collected: [String] = []
for try await result in group {
collected.append(result)
}
return collected
}
group.addTask{ } 로 작업을 추가하면 즉시 실행이 시작되고 이후 for await result in group 반복문으로 완료되는 순서대로 결과를 받아요. 이때, 결과가 완료 순서로 오기 때문에 순서가 보장되지 않아요. 순서가 필요하다면 인덱스를 함께 반환해야 해요.
둘을 비교하면 다음과 같아요
| withTaskGroup | withThrowingTaskGroup | |
|---|---|---|
| 에러 처리 | 불가 | throws 가능 |
| 자식 Task | async | async throws |
| 결과 반환 | 일반 값 | 일반 값 또는 에러 |
| 에러 발생 시 | — | 나머지 Task 취소 후 에러 전파 |
TaskGroup은 구조적 동시성을 따르기 때문에 부모 Task가 취소되면 자식 Task에도 취소가 전파돼요. 단 직접 취소하는 경우가 있을 경우 긴 작업을 하는 자식 Task 안에서는 중간중간 취소 여부를 확인해주는 게 좋아요.
group.addTask {
try Task.checkCancellation() // 취소되었으면 CancellationError throw
let data = try await fetchLargeData()
try Task.checkCancellation() // 네트워크 완료 후 다시 확인
return process(data)
}
또는 Task.isCancelled로 직접 분기할 수도 있음
group.addTask {
guard !Task.isCancelled else { return nil }
return try await fetch()
}
비슷해 보이는 async let과 비교해볼게요
// async let — 작업 수가 컴파일 타임에 고정
async let a = fetchA()
async let b = fetchB()
let (resultA, resultB) = try await (a, b)
// TaskGroup — 런타임에 동적으로 작업 추가 가능
try await withThrowingTaskGroup(of: String.self) { group in
for keyword in keywords { // 개수가 런타임에 결정됨
group.addTask { try await fetch(keyword) }
}
...
}
위처럼 async let은 컴파일 시점에 갯수가 고정되기 때문에 정해진 2~3개 작업은 async let이 더 읽기 쉽고, 배열을 순회하며 작업을 만들어야 할 때는 TaskGroup이 적합해요