티스토리 뷰

반응형

Swift Concurrency의 핵심: Structured Concurrency로 안전한 비동기 작업 제어하기

Swift Concurrency를 활용하여 비동기 처리를 구현하고 계시다면, Structured Concurrency(구조화된 동시성) 형태로 작업을 구성하는 것이 강력하게 권장됩니다. 왜냐하면 이는 작업 간의 부모/자식 관계를 명시적으로 설정하여, 복잡한 비동기 작업을 예측 가능하고 유연하게 제어할 수 있게 하기 때문입니다.

오늘은 이 원칙의 가장 큰 장점인 '취소(Cancellation) 전파' 기능을, 중첩된 Task 작업이 있는 상황을 가정해서 코드 작성해보며 실험해보겠습니다.

playground 에서 emtpy project를 생성해서 swift concurrency 기반 코드 작성 및 테스트를 해보실 수 있습니다!


1. Unstructured Concurrency: 취소 신호가 멈추는 곳

Task { ... } 블록 내에서 새로운 Task { ... }를 생성하는 방식은 Unstructured Concurrency (비구조화된 동시성)의 대표적인 예입니다.

새로 생성된 Task는 바깥 Task의 컨텍스트(예: actor 컨텍스트)는 물려받을 수 있지만, 별도의 최상위 비동기 작업으로 간주됩니다. 이 때문에 부모 Task의 취소 신호가 자식 Task자동으로 전파되지 않습니다. 이 방식은 세부 비동기 작업의 생명 주기 제어가 어렵다는 명확한 단점을 가집니다.


실험 1: 취소 전파 실패 (Unstructured Task)

// outerTask와 innerTask는 별도의 최상위 비동기 작업으로 간주됩니다.
let outerTaskWithUnstructuredConcurrency = Task {
    // ⚠️ 이 innerTask는 outerTask와 독립적인 별도의 Task입니다.
    let innerTask = Task {
        for num in 1...5 {
            // Task.sleep은 취소 가능 지점입니다.
            try? await Task.sleep(for: .seconds(1))
            try Task.checkCancellation() 
            debugPrint("inner task \(num) 초 with unstructured concurrency")
        }
    }

    do {
        // innerTask의 완료를 기다립니다. (innerTask의 value를 await)
        try await innerTask.value 
    } catch is CancellationError {
        // 이 catch는 innerTask 내부의 오류가 아닌,
        // outerTask 내부에서 발생한 CancellationError만 잡습니다.
        debugPrint("inner task cancelled with unstructured concurrency") 
    }
}
// outerTask 취소 신호가 innerTask로 전달되지 않습니다.
outerTaskWithUnstructuredConcurrency.cancel()

// 📋 출력 결과 (취소 신호를 무시하고 5초까지 완료)
// "inner task 1 초 with unstructured concurrency"
// ...
// "inner task 5 초 with unstructured concurrency"

2. Structured Concurrency: TaskGroup을 통한 안전한 제어

TaskGroup은 Swift Concurrency의 Structured Concurrency 개념 중 하나입니다. withTaskGroup이나 withThrowingTaskGroup을 사용하면 명시적인 부모-자식 관계가 형성되어, 부모 Task의 생명 주기가 자식 Task의 생명 주기와 강하게 연결됩니다.


실험 2: 취소 전파 성공 (Structured TaskGroup)

func sampleTaskGroup() async throws {
    // withThrowingTaskGroup: 이 블록 내의 모든 Task는 부모 Task의 자식이 됩니다.
    try await withThrowingTaskGroup(of: Void.self) { group in

        // group.addTask: 자식 Task를 생성하여 그룹에 추가합니다.
        group.addTask {
            for num in 1...5 {
                try await Task.sleep(for: .seconds(1))
                // 부모 Task가 취소되면 여기서 CancellationError가 발생합니다.
                try Task.checkCancellation() 
                debugPrint("inner task \(num) 초 with structured concurrency")
            }
        }

        // 부모 Task가 자식들의 완료를 기다리도록 보장합니다.
        for try await _ in group { } 
    }
}

let outerTaskWithStructuredConcurrency = Task {
    do {
        // sampleTaskGroup을 호출함으로써 부모 Task와 자식 TaskGroup 간의 관계가 형성됩니다.
        try await sampleTaskGroup() 

    } catch is CancellationError {
        // outerTask가 취소되면, sampleTaskGroup이 CancellationError를 던지고 이 곳에서 포착됩니다.
        debugPrint("inner task cancelled with structured concurrency")
    } catch {
        debugPrint("other error : \(error) with structured concurrency")
    }
}
// outerTask 취소 신호가 TaskGroup을 통해 innerTask로 전파됩니다.
outerTaskWithStructuredConcurrency.cancel()

// 📋 출력 결과 (취소 신호 감지 후 즉시 종료)
// (1초 경과 로그 후)
// "inner task cancelled with structured concurrency" 

결론 및 활용

지금까지 중첩 비동기 작업이 존재할때의 swift concurrency 사용예시를 몇가지 작성 및 실험해봤습니다. 한번 더 내용을 정리해보자면...

TaskGroup, async let, AsyncStream 등의 Structured Concurrency 도구들은 비동기 작업에 명확한 계층 구조를 부여하여 다음을 가능하게 합니다.

  1. 안전한 취소: 부모가 취소되면 관련된 모든 자식 작업이 자동으로 정리됩니다.
  2. 쉬운 에러 핸들링: 자식 Task에서 발생한 오류는 부모 Task로 명확하게 전달됩니다.

Swift Concurrency의 작업이 복잡해질 때, 단순 Task { ... } 대신 TaskGroupasync let을 활용하는 것이 코드를 더 예측 가능하고 안정적으로 만드는 핵심 비결입니다.

반응형
댓글
반응형
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/11   »
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
글 보관함