Swift - TaskGroup

Marble·2026년 3월 5일

Concurrency

목록 보기
8/10

이번 글에서는 TaskGroup에 대해 알아볼게요

TaskGroup

기존에 Task를 이용해서 10개의 키워드에 대해 각각 네트워크 요청을 보내야 한다면 다음과 같이 해야했죠

Task {
    for keyword in keywords {
        let result = try await fetch(keyword)
    }
}

이 코드는 1번 요청이 끝나야 2번이 시작되기 때문에 10개 요청이 각각 1초씩 걸린다면 총 10초가 걸려요. 이럴 때 사용하는게 TaskGroup이에요. TaskGroup은 여러 비동기 작업을 동적으로 생성하고, 모든 결과를 한 곳에서 모아 처리할 수 있는 구조체에요.

특징

  • 구조적 동시성(Structured Concurrency)
    그룹이 살아있는 동안만 자식 Task가 실행되고, 그룹이 끝나면 모든 자식 Task도 함께 정리되요
  • 동적 생성
    실행 전에 작업 수를 알 필요가 없기 때문에 루프 안에서 자유롭게 추가할 수 있어요
  • 자동 취소 전파
    부모 Task가 취소되면 그룹 내 모든 자식 Task도 자동으로 취소돼요

사용법

사용법은 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 반복문으로 완료되는 순서대로 결과를 받아요. 이때, 결과가 완료 순서로 오기 때문에 순서가 보장되지 않아요. 순서가 필요하다면 인덱스를 함께 반환해야 해요.

둘을 비교하면 다음과 같아요

withTaskGroupwithThrowingTaskGroup
에러 처리불가throws 가능
자식 Taskasyncasync 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()
}

vs async let

비슷해 보이는 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이 적합해요

profile
개발자가 되고 싶은 공돌이

0개의 댓글