๊ด€๋ฆฌ ๋ฉ”๋‰ด

study record

[Swift Concurrency] Task๋ฅผ killํ•˜๋ฉด ์–ด๋–ป๊ฒŒ ๋˜๋‚˜? ๋ณธ๋ฌธ

Swift/Concurrency

[Swift Concurrency] Task๋ฅผ killํ•˜๋ฉด ์–ด๋–ป๊ฒŒ ๋˜๋‚˜?

asong 2025. 12. 15. 18:29

์ฐธ๊ณ ํ•œ Medium ๊ธ€ ๐Ÿ”ฝ

https://medium.com/@avula.koti.realpage/the-ios-interview-that-started-with-what-happens-if-you-kill-this-task-bcae4f6dfa67

 

Swift Concurrency์—์„œ Task ์ทจ์†Œ(Cancelation)๊ฐ€ ๋™์ž‘ํ•˜๋Š” ๋ฐฉ์‹ ์ •๋ฆฌ

 

Swift Concurrency์˜ Task.cancel()์€ ๋งŽ์€ ๊ฐœ๋ฐœ์ž๋“ค์ด ์˜คํ•ดํ•˜๊ณ  ์žˆ๋Š” ๋ถ€๋ถ„ ์ค‘ ํ•˜๋‚˜์ž…๋‹ˆ๋‹ค.

์•„๋ž˜ ๋‚ด์šฉ์€ Swift์—์„œ Task ์ทจ์†Œ๊ฐ€ ์‹ค์ œ๋กœ ์–ด๋–ป๊ฒŒ ๋™์ž‘ํ•˜๋Š”์ง€,

๊ทธ๋ฆฌ๊ณ  ์ทจ์†Œ๊ฐ€ ์ „ํŒŒ๋˜๋Š” ๊ธฐ์ค€์„ ์ •๋ฆฌํ•œ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

 


 

1. Task.cancel()์€ Task๋ฅผ “์ฃฝ์ด๋Š”(kill)” ๊ฒƒ์ด ์•„๋‹™๋‹ˆ๋‹ค

 

Swift์—์„œ Task๋Š” ์ฆ‰์‹œ ์ข…๋ฃŒ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

์ทจ์†Œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ํŠน์ง•์„ ๊ฐ€์ง‘๋‹ˆ๋‹ค:

 

  • Task๋Š” ์ทจ์†Œ๋˜๋ฉด ์ฆ‰์‹œ ์ข…๋ฃŒ๋˜์ง€ ์•Š๊ณ ,
  • “์ทจ์†Œ๋จ(canceled) ์ƒํƒœ๋กœ ํ‘œ์‹œ”๋งŒ ๋ฉ๋‹ˆ๋‹ค.
  • Task ๋‚ด๋ถ€์—์„œ ์ทจ์†Œ ์—ฌ๋ถ€๋ฅผ ํ™•์ธํ•˜๊ฑฐ๋‚˜(Task.isCancelled)
  • ์ทจ์†Œ ๊ฐ€๋Šฅํ•œ await ์ง€์ ์„ ๋งŒ๋‚˜๊ธฐ ์ „๊นŒ์ง€๋Š” ๊ณ„์† ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค.

์˜ˆ์‹œ

let task = Task {
    print("Start")
    try await Task.sleep(nanoseconds: 2_000_000_000)
    print("End")
}
task.cancel()

์œ„ ์ฝ”๋“œ์—์„œ ์ทจ์†Œํ•ด๋„ "Start"๋Š” ์ถœ๋ ฅ๋˜๋ฉฐ,

์ž ์ž๊ธฐ(sleep) ์ค‘์— CancellationError๊ฐ€ ๋ฐœ์ƒํ•ด์•ผ Task๊ฐ€ ์ข…๋ฃŒ๋ฉ๋‹ˆ๋‹ค.

 

 

2. ์ทจ์†Œ๋Š” ์ž๋™์œผ๋กœ ์ฝ”๋“œ ์ „์ฒด์— ์ „ํŒŒ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค

 

์•„๋ž˜์™€ ๊ฐ™์€ ์˜คํ•ด๊ฐ€ ํ”ํ•ฉ๋‹ˆ๋‹ค:

 

“Task๋ฅผ ์ทจ์†Œํ•˜๋ฉด ๋‚ด๋ถ€์—์„œ ํ˜ธ์ถœํ•˜๋Š” ํ•จ์ˆ˜๋„ ๋ชจ๋‘ ์ž๋™์œผ๋กœ ์ทจ์†Œ๋œ๋‹ค?”

 

 ๊ทธ๋ ‡์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

 

doSomething() ๊ฐ™์€ ํ•จ์ˆ˜๊ฐ€ ์ทจ์†Œ ์—ฌ๋ถ€๋ฅผ ์ฒดํฌํ•˜์ง€ ์•Š๊ฑฐ๋‚˜

์ทจ์†Œ ๊ฐ€๋Šฅํ•œ await๋ฅผ ํ˜ธ์ถœํ•˜์ง€ ์•Š์œผ๋ฉด,

Task๋Š” ์ทจ์†Œ ํ›„์—๋„ ๋๊นŒ์ง€ ์‹คํ–‰๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

 

์˜ˆ

func doSomething() async {
    // ์ทจ์†Œ ์ฒดํฌ ์—†์Œ
    // ์ทจ์†Œ ๊ฐ€๋Šฅํ•œ await ์—†์Œ
}

์ด ๊ฒฝ์šฐ task.cancel()์„ ํ˜ธ์ถœํ•ด๋„ ์ž‘์—…์€ ๊ณ„์† ์ˆ˜ํ–‰๋ฉ๋‹ˆ๋‹ค.

 

 

3. “์ทจ์†Œ ์ „ํŒŒ(Cancelation Propagation)”๋Š” Task ์ƒ์„ฑ ๋ฐฉ์‹์— ๋”ฐ๋ผ ๋‹ค๋ฆ…๋‹ˆ๋‹ค

 

Swift์—์„œ Task๋Š” ์ƒ์„ฑ ๋ฐฉ์‹์— ๋”ฐ๋ผ ๋ถ€๋ชจ์˜ ์ทจ์†Œ ์ƒํƒœ๋ฅผ ์ƒ์†๋ฐ›๊ธฐ๋„ ํ•˜๊ณ , ๋ฐ›์ง€ ์•Š๊ธฐ๋„ ํ•ฉ๋‹ˆ๋‹ค.

 

 

์ •๋ฆฌ

Task ์ƒ์„ฑ ๋ฐฉ์‹์ทจ์†Œ ์ „ํŒŒ ์—ฌ๋ถ€

Task { } โŒ ์ „ํŒŒ ์•ˆ ๋จ (๋…๋ฆฝ์ ์ธ top-level task)
Task.detached โŒ ์ „ํŒŒ ์•ˆ ๋จ
async let โœ… ์ „ํŒŒ๋จ
TaskGroup โœ… ์ „ํŒŒ๋จ

์˜ˆ์‹œ

let parentTask = Task {
    let child = Task {
        await doSomethingSlow()
    }
    return await child.value
}

parentTask.cancel()

์œ„ ์ฝ”๋“œ์—์„œ child๋Š” ์ทจ์†Œ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

Task { }๋Š” ๋ถ€๋ชจ-์ž์‹ ๊ด€๊ณ„๋ฅผ ๋งŒ๋“ค์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

 

 

4. Swift๋Š” ์ทจ์†Œ ์‹œ ์ž๋™ ์ •๋ฆฌ(cleanup)๋ฅผ ํ•ด์ฃผ์ง€ ์•Š์Šต๋‹ˆ๋‹ค

 

์ทจ์†Œ๋Š” ๋‹จ์ˆœํžˆ CancellationError๋ฅผ ๋˜์ง€๋Š” ๋™์ž‘์ด๋ฉฐ,

์•„๋ž˜์™€ ๊ฐ™์€ ์ž‘์—…์„ ์ž๋™์œผ๋กœ ๋˜๋Œ๋ฆฌ๊ฑฐ๋‚˜ ๋ณต๊ตฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

 

  • ํŒŒ์ผ ์“ฐ๊ธฐ ์ค‘๋‹จ ๋ฐ ๋กค๋ฐฑ
  • ๋„คํŠธ์›Œํฌ ์—…๋กœ๋“œ ์ค‘๋‹จ ํ›„ ์ •๋ฆฌ
  • DB ํŠธ๋žœ์žญ์…˜ ๋ณต๊ตฌ
  • ์บ์‹œ ์ •๋ฆฌ

 

์ •๋ฆฌ๋Š” ๊ฐœ๋ฐœ์ž๊ฐ€ ์ง์ ‘ ๊ตฌํ˜„ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

 

 

์˜ˆ์‹œ

let task = Task {
    do {
        try await doExpensiveWork()
    } catch is CancellationError {
        await cleanUp()
    }
}
task.cancel()

 

5. ์ทจ์†Œ๊ฐ€ ํ๋ฅด๋„๋ก ํ•˜๋ ค๋ฉด ๋ถˆํ•„์š”ํ•œ Task { } ๋ž˜ํ•‘์„ ํ”ผํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค

 

์•„๋ž˜ ํ•จ์ˆ˜๋Š” ๋‚ด๋ถ€์—์„œ ๋…๋ฆฝ์ ์ธ top-level task๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค:

func fetchUserData() async throws -> User {
    let task = Task {
        await refreshAuthToken()
        return try await networkService.loadUser()
    }
    return try await task.value
}

→ ์ด ๊ฒฝ์šฐ, ์ƒ์œ„ ํ˜ธ์ถœ์ž๊ฐ€ ์ทจ์†Œ๋˜์–ด๋„ ๋‚ด๋ถ€ Task๋Š” ๊ทธ๋Œ€๋กœ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค.

 

 

ํ•ด๊ฒฐ๋ฒ•: Task๋ฅผ ์“ฐ์ง€ ์•Š๊ณ  ํ•จ์ˆ˜ ์ž์ฒด๋ฅผ async๋กœ ์œ ์ง€ํ•˜๊ธฐ

func fetchUserData() async throws -> User {
    try await refreshAuthToken()
    return try await networkService.loadUser()
}

์ด ๋ฐฉ์‹์€ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ํ˜ธ์ถœ์ž์˜ ์ทจ์†Œ๊ฐ€ ์ „ํŒŒ๋˜๋ฉฐ,

๋ถˆํ•„์š”ํ•œ Task ์ค‘์ฒฉ์„ ๋ฐฉ์ง€ํ•ฉ๋‹ˆ๋‹ค.

 

 

6. ์ทจ์†Œ ์ „ํŒŒ๊ฐ€ ํ™•์‹คํ•œ ํŒจํ„ด: async let

 

async let์„ ์‚ฌ์šฉํ•˜๋ฉด, ์ทจ์†Œ๋Š” ์ž๋™์œผ๋กœ ์ž์‹ ์ž‘์—…์— ์ „ํŒŒ๋ฉ๋‹ˆ๋‹ค.

 

 

์˜ˆ์‹œ

func fetchData() async throws -> String {
    async let name = loadName()
    async let age = loadAge()
    return "\(try await name), \(try await age)"
}

 fetchData()๊ฐ€ ์ทจ์†Œ๋˜๋ฉด

loadName()๊ณผ loadAge()๋„ ์ž๋™์œผ๋กœ ์ทจ์†Œ๋ฉ๋‹ˆ๋‹ค.

 

 

7. ์ •๋ฆฌ: Swift์—์„œ Task ์ทจ์†Œ๊ฐ€ ์‹ค์ œ๋กœ ์ผ์–ด๋‚˜๋Š” ๋ฐฉ์‹

 

Task ์ทจ์†Œ ํ›„ ์‹ค์ œ ์ข…๋ฃŒ๋˜๋ ค๋ฉด ์•„๋ž˜ ์ค‘ ํ•˜๋‚˜๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค:

 

  1. Task ๋‚ด๋ถ€์—์„œ Task.isCancelled ์ฒดํฌ
  2. ์ทจ์†Œ ๊ฐ€๋Šฅํ•œ await๋ฅผ ๋งŒ๋‚˜ CancellationError๊ฐ€ ๋ฐœ์ƒ
  3. ๊ฐœ๋ฐœ์ž๊ฐ€ ์ˆ˜๋™์œผ๋กœ ์ทจ์†Œ๋ฅผ ์ „ํŒŒ
  4. async let ๋˜๋Š” TaskGroup์„ ํ†ตํ•ด ๋ถ€๋ชจ ์ทจ์†Œ๊ฐ€ ์ „ํŒŒ๋จ

 

์œ„ ์กฐ๊ฑด์ด ํ•˜๋‚˜๋„ ์—†๋‹ค๋ฉด,

Task๋Š” ์ทจ์†Œ๋˜๋”๋ผ๋„ ๋๊นŒ์ง€ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค.

 


 

๊ฒฐ๋ก 

 

Swift Concurrency์˜ ์ทจ์†Œ ๋กœ์ง์€ ๋‹จ์ˆœํ•œ ๊ธฐ๋Šฅ์ฒ˜๋Ÿผ ๋ณด์ด์ง€๋งŒ

์‹ค์ œ ๋™์ž‘์€ ๋‹ค์Œ์„ ์ดํ•ดํ•ด์•ผ ์ œ๋Œ€๋กœ ๋‹ค๋ฃฐ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

 

  • Task๋Š” ์ฆ‰์‹œ “์ฃฝ์ง€ ์•Š๋Š”๋‹ค”
  • ์ทจ์†Œ๋Š” ์ „ํŒŒ๋˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ๋‹ค
  • cleanup์€ ์ž๋™์œผ๋กœ ์ผ์–ด๋‚˜์ง€ ์•Š๋Š”๋‹ค
  • async let / TaskGroup์€ ์˜ฌ๋ฐ”๋ฅธ ์ทจ์†Œ ์ „ํŒŒ๋ฅผ ์ œ๊ณตํ•œ๋‹ค
  • ๋ถˆํ•„์š”ํ•œ Task wrapping์€ ์ทจ์†Œ ๋ฒ„๊ทธ๋ฅผ ๋งŒ๋“ ๋‹ค

 

Concurrency๋ฅผ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๋‹ค๋ฃจ๊ธฐ ์œ„ํ•ด์„œ๋Š” ์ทจ์†Œ์˜ ๋™์ž‘ ์›๋ฆฌ์™€ ์ „ํŒŒ ๊ทœ์น™์„ ์ •ํ™•ํžˆ ์ดํ•ดํ•˜๋Š” ๊ฒƒ์ด ํ•„์ˆ˜์ ์ž…๋‹ˆ๋‹ค.

 

 


 

++) ์˜ˆ์‹œ ์ถ”๊ฐ€

1. Task ๋‚ด๋ถ€์—์„œ Task.isCancelled ์ฒดํฌํ•˜๋Š” ์˜ˆ์‹œ๋“ค

 

์˜ˆ์‹œ 1 — ๋ฐ˜๋ณต ์ž‘์—… ์ค‘๊ฐ„์— ์ทจ์†Œ ์—ฌ๋ถ€ ํ™•์ธ

let task = Task {
    for i in 0..<10 {
        if Task.isCancelled {
            print("Canceled while iterating. Cleaning up...")
            return
        }
        print("Processing \(i)")
        try await Task.sleep(nanoseconds: 300_000_000)
    }
}
task.cancel()

 

์˜ˆ์‹œ 2 — ๋ฌด๊ฑฐ์šด ์—ฐ์‚ฐ ์ค‘ ์ฃผ๊ธฐ์ ์œผ๋กœ ์ทจ์†Œ ์ฒดํฌ

func heavyComputation() async {
    var result = 0

    for i in 0..<100_000 {
        if Task.isCancelled {
            print("Canceled during computation")
            return
        }
        result += i
    }

    print("Finished: \(result)")
}

let task = Task {
    await heavyComputation()
}
task.cancel()

 

์˜ˆ์‹œ 3 — ๋„คํŠธ์›Œํฌ ์žฌ์‹œ๋„ ๋กœ์ง์—์„œ ์ทจ์†Œ ๋ฐ˜์˜

func fetchWithRetry() async throws -> Data {
    for attempt in 1...3 {
        if Task.isCancelled {
            throw CancellationError()
        }

        do {
            return try await networkService.fetch()
        } catch {
            if attempt == 3 { throw error }
        }
    }
    throw URLError(.unknown)
}

 

 

2. ์ทจ์†Œ ๊ฐ€๋Šฅํ•œ await๋ฅผ ๋งŒ๋‚˜ CancellationError๊ฐ€ ๋ฐœ์ƒํ•˜๋Š” ์˜ˆ์‹œ๋“ค

 

Swift Concurrency์—์„œ ์ผ๋ถ€ await๋Š” ์ž๋™์œผ๋กœ ์ทจ์†Œ๋ฅผ ์ „ํŒŒํ•ฉ๋‹ˆ๋‹ค.

 

๋Œ€ํ‘œ์ ์ธ ์˜ˆ:

 

  • Task.sleep
  • ๋„คํŠธ์›Œํฌ API (URLSession ๊ธฐ๋ฐ˜)
  • Actor ์ ‘๊ทผ
  • async let ๊ฒฐ๊ณผ ๊ธฐ๋‹ค๋ฆฌ๊ธฐ
  • TaskGroup ๋‚ด ์ž‘์—… ๊ธฐ๋‹ค๋ฆฌ๊ธฐ ๋“ฑ

 

์˜ˆ์‹œ 1 — Task.sleep๋Š” ์ทจ์†Œ ์‹œ ๋ฐ”๋กœ CancellationError ๋ฐœ์ƒ

let task = Task {
    print("Start")
    try await Task.sleep(nanoseconds: 5_000_000_000)
    print("End") // ์‹คํ–‰๋˜์ง€ ์•Š์Œ
}

task.cancel()

 

์˜ˆ์‹œ 2 — URLSession ์š”์ฒญ์—์„œ ์ทจ์†Œ๊ฐ€ ๋ฐœ์ƒ

func load() async throws -> Data {
    let url = URL(string: "https://example.com")!
    let (data, _) = try await URLSession.shared.data(from: url)
    return data
}

let task = Task {
    try await load()
}

task.cancel() // URLSession์ด ์ž๋™์œผ๋กœ ์ทจ์†Œ๋จ → CancellationError ๋ฐœ์ƒ

 

์˜ˆ์‹œ 3 — Actor ์ ‘๊ทผ ์ค‘ ์ทจ์†Œ

actor Counter {
    var value = 0
    func increment() { value += 1 }
}

let counter = Counter()

let task = Task {
    try await Task.sleep(nanoseconds: 1_000_000_000)
    await counter.increment() // ์ด ์ง€์ ์—์„œ ์ทจ์†Œ๊ฐ€ ๊ฐ์ง€๋  ์ˆ˜ ์žˆ์Œ
}

task.cancel()

 

 

3. ๊ฐœ๋ฐœ์ž๊ฐ€ ์ˆ˜๋™์œผ๋กœ ์ทจ์†Œ๋ฅผ ์ „ํŒŒํ•˜๋Š” ์˜ˆ์‹œ๋“ค

 

์ˆ˜๋™ ์ „ํŒŒ๋ž€, ํ•œ Task๊ฐ€ ์ทจ์†Œ๋˜์—ˆ์Œ์„ ๊ฐ์ง€ํ•˜๊ณ 

๋‚ด๋ถ€์—์„œ ๋‹ค๋ฅธ ์ž‘์—…์—๋„ ์ทจ์†Œ๋ฅผ ์ „๋‹ฌํ•˜๋Š” ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค.

 

 

์˜ˆ์‹œ 1 — ์ทจ์†Œ ์‹œ cleanup + ์ž์‹ task ์ทจ์†Œ

func loadData() async throws {
    let child = Task {
        try await Task.sleep(nanoseconds: 5_000_000_000)
        print("Child finished")
    }

    do {
        try await child.value
    } catch is CancellationError {
        child.cancel()
        print("Canceled and cleaned")
        throw CancellationError()
    }
}

์˜ˆ์‹œ 2 — ์—ฌ๋Ÿฌ ์ž‘์—…์„ ๋™์‹œ์— ์‹คํ–‰ํ•˜๊ณ  ์ทจ์†Œ ์‹œ ๋ชจ๋‘ ์ค‘๋‹จ

func fetchMultiple() async throws {
    let t1 = Task { try await loadA() }
    let t2 = Task { try await loadB() }

    do {
        let resultA = try await t1.value
        let resultB = try await t2.value
        print(resultA, resultB)
    } catch is CancellationError {
        t1.cancel()
        t2.cancel()
        throw CancellationError()
    }
}

์˜ˆ์‹œ 3 — Operation-style cleanup

let task = Task {
    do {
        try await longRunningProcess()
    } catch is CancellationError {
        await rollbackChanges()
        throw CancellationError()
    }
}
task.cancel()