김경록의 앱 개발 여정

[Swift Concurrency] Task의 취소 본문

TIL

[Swift Concurrency] Task의 취소

Kim Roks 2025. 4. 18. 15:12

작업의 즉시 멈춤이 아닌, ‘취소를 전파’하는 개념

  • Swift의 Task는 취소를 요청받더라도, 즉시 중단되는 것이 아니다.
  • 대신, “취소되었다는 신호” 를 내부적으로 가지고 있고, 작업 내에서 그 신호를 직접 확인하고 적절히 대응해야 함.
  • 이걸 "cooperative cancellation" 이라고도 함. 작업이 스스로 확인하고 정리하는 방식.(협동적 취소)

부모-자식 관계와 취소 전파

  • Swift의 structured concurrency에서는 부모 Task가 있고, 그 안에 자식 Task가 만들어질 수 있어.
  • 부모 Task가 취소되면, 자식 Task에게도 취소가 전파됨.
  • 하지만! 자식 작업이 완전히 끝날 때까지 (심지어 에러가 발생했더라도), 부모 작업은 기다림.
  • 즉, 취소되더라도 자식 작업은 graceful하게 끝나야 하며, 그동안 부모는 대기 상태.

취소 상태 확인: Task.isCancelled

  • 작업 내부에서 현재 취소 상태인지 확인하고 싶을 때 사용

func someTask() async {
    if Task.isCancelled {
        print("취소 요청이 들어왔습니다.")
        return
    }

    // 작업 수행
}
  • 반복문이나 긴 작업 처리 중간중간에 Task.isCancelled를 확인하는 것이 일반적인 패턴.

try Task.checkCancellation()

  • 이 함수는 Task.isCancelled를 체크하고, 취소되었다면 에러를 던짐.
  • 에러 타입은 CancellationError
  • Task가 취소되었을 때는 일반적인 에러 처리 흐름을 따름
  • try 구문에서 Task.checkCancellation() 호출 시 CancellationError가 throw됨 → 이를 do-catch로 처리 가능

항목 설명

취소 방식 즉시 멈추는 게 아닌, 신호를 전달
부모-자식 구조 부모 취소 → 자식에게 전파되지만, 자식이 끝날 때까지 기다림
상태 확인 Task.isCancelled 로 수동 확인
취소 체크 및 에러 던지기 try Task.checkCancellation()
에러 처리 CancellationError 로 잡아서 분기 가능

✅ 중첩된 Task와 취소 전파

1. 중첩된 Task는 별도의 컨텍스트로 동작

  • Swift에서 Task는 기본적으로 새로운 독립적인 실행 컨텍스트를 만든다.
  • 즉, 상위 Task 내에서 Task { }로 생성한 Task는 부모와 별개임.
  • 그래서 부모 Task가 취소되더라도, 내부에서 만든 Task에는 취소가 전파되지 않는다.
swift
복사편집
func parentTask() async {
    Task {
        // 이 Task는 부모와 별개
        // 부모가 취소돼도 여기까지는 취소가 전파되지 않음
        await doWork()
    }
}

  • 위 예제에서 내부 Task는 **“중첩되어 있지만 독립적”**인 구조.
  • 취소 전파가 안 되는 이유는 같은 구조적 컨텍스트가 아니기 때문.

✅ 구조적 동시성에서는 취소가 제대로 전파됨

📌 예시 1: async let 사용 시

swift
복사편집
func example() async {
    async let result1 = doSomething()
    async let result2 = doSomethingElse()

    // 부모가 취소되면 result1, result2 도 함께 취소됨
    let _ = await (try? result1, try? result2)
}

  • async let은 parent-child 관계가 유지되며, 구조적으로 연결되어 있음
  • 부모가 취소되면 자식도 취소됨

📌 예시 2: withTaskGroup

swift
복사편집
func example() async {
    await withTaskGroup(of: Void.self) { group in
        group.addTask {
            await workA()
        }

        group.addTask {
            await workB()
        }

        group.cancelAll() // → 모든 작업에 취소가 전파됨
    }
}

✅ 직접적인 취소 처리를 구현하지 않아도 되는 경우

취소에 대응하는 API는 자동으로 취소됨

  • 예를 들어 URLSession.shared.data(from:) 같은 API는 내부적으로 CancellationError를 던질 준비가 되어 있는 구조야.
  • 즉, 우리가 따로 Task.isCancelled를 확인하지 않아도, Task가 취소되면 이 함수가 자동으로 취소되고 에러를 throw함.
func fetchData(from url: URL) async throws -> Data {
    let (data, _) = try await URLSession.shared.data(from: url)
    return data
}

  • 이 작업이 포함된 Task가 취소되면, data(from:)가 취소되고 CancellationError가 throw됨.
  • 별도의 checkCancellation() 필요 없음.
  • Task.sleep 도 이런 경우