LINE Corporation이 2023년 10월 1일부로 LY Corporation이 되었습니다. LY Corporation의 새로운 기술 블로그를 소개합니다. LY Corporation Tech Blog

Blog


Swift Concurrency 성능 조사

안녕하세요. LINE Plus iOS Platform Dev 팀에서 iOS 개발을 하고 있는 김윤재입니다. 2021년에 Swift Concurrency가 등장했을 때 LINE Engineering 블로그에 Swift Concurrency를 소개하는 글(Swift Concurrency에 대해서)을 남겼습니다. 어느덧 글을 쓴 지 2년이 지났네요. 당시에는 주로 Swift Concurrency의 장점을 소개했는데요. 이번 글에서는 최근 팀에서 Swift Concurrency를 적용할 때 성능 관점에서 어떤 점을 주의해야 할지 스터디를 진행하며 알게 된 점을 공유하려고 합니다.

Swift Concurrency의 두 가지 특징

성능 관점에서 주의해야 할 점을 살펴보기 전에 Swift Concurrency 내부 작동 방식의 두 가지 특징인 TaskPriority와 Suspension points를 먼저 살펴보겠습니다.

TaskPriority 작동 방식

Swift Concurrency에는 GCD(Grand Central Dispatch)의 QoS(Quality of Service)와 같은 역할을 하는 TaskPriority가 있습니다. 아래는 Swift TaskPriority와 GCD QoS를 서로 상응하는 우선순위에 맞게 배치한 표입니다. 같은 행에 속하면 같은 우선순위입니다.

QoS(GCD) TaskPriority(Swift Concurrency)
Userinitiated High
Default Medium
Utility Low

Swift Concurrency의 장점 중 하나로 소개된 것이 스레드를 코어 수만큼만 사용해서 콘텍스트 스위칭이 적다는 것이었습니다(참고: WWDC 2021: Swift Concurrency - Behind the Scenes). 그렇다면 스레드 숫자가 코어 수로 제한되는 상황에서 우선순위에 따른 스케줄링을 어떻게 진행하는지 아래와 같은 의문이 들었습니다.

  • 우선순위가 다양한 작업이 있을 때 OS는 어떤 방식으로 각 작업에 스레드를 할당할까?
  • 우선순위가 더 높은 작업이 많을 때 우선순위가 낮은 작업은 어떻게 실행을 보장받을까?

이 의문을 해결하기 위해 우선순위가 높은 작업을 먼저 실행한 뒤 낮은 작업을 실행해 결과를 살펴보고, 다음에는 우선순위가 낮은 작업부터 실행해서 결과를 살펴봤습니다.

우선순위가 높은 작업부터 추가했을 때 스레드 할당 방식

먼저 TaskPriority가 High인 작업들을 추가한 뒤, 이어서 Low인 작업들을 추가하는 방식으로 테스트를 진행했습니다. 테스트는 코어가 6개인 iPhone 12 Pro 기기로 진행했습니다. 따라서 Swift Concurrency에서 스레드를 6개 사용할 것이라고 판단, High 작업을 6개 추가한 뒤 Low 작업을 6개 추가했습니다.

Xcode Instruments에서 Swift Concurrency 프로파일링 템플릿을 이용해 진행한 프로파일링 결과 중 두 가지, 작업 상태(Task states)와 스레드(Threads) 결과를 살펴보겠습니다.

먼저 작업 상태는 아래와 같이 나타났습니다. 상위 6개가 우선순위 High 작업, 나머지 6개가 Low 작업입니다.

각 작업은 아래에서 보다 자세히 살펴볼 'Suspension points'에서 잠시 중단(suspended)되며 스레드 점유권을 양도하기도 하고, 다시 스레드를 할당받아서 실행되기도 했는데요. 이때 우선순위 High인 작업들은 중단된 후 높은 확률로 다시 스레드를 할당받아 실행된 반면, Low인 작업들은 주로 suspendedcontinuation 상태에서 머물며 실행되지 못하고 있는 경우가 많았습니다. High 작업들이 종료되기 전까지 Low 작업들은 한 번에 하나만 실행됐으며, 실행되다가 잠시 중단되면 High 작업들이 종료된 후에야 스레드를 할당받기 시작했습니다.

다음은 프로파일링 후 스레드를 살펴 본 결과입니다.

코어 개수에 맞게 6개의 스레드만 활성화돼 Swift Concurrency 작업들을 실행할 것이라고 예상했지만, 실제로는 7개의 스레드로 실행된 것을 확인할 수 있는데요. 위 그림에서 상위 6개 스레드는 High 작업만 실행했고, 맨 아래 1개 스레드는 Low 작업만 실행했습니다. 이는 Low 작업들이 아예 실행되지 않는 것을 막기 위한 것으로 보입니다. High 작업이 모두 종료되면 High 작업만 실행하던 스레드에서 Low 작업을 실행하는 것을 확인할 수 있었습니다.

우선순위가 낮은 작업부터 추가했을 때 스레드 할당 방식

이번에는 우선순위가 낮은 작업을 먼저 실행하고 우선순위가 높은 작업을 실행했을 때 스레드 점유권을 잘 가져오는지 테스트했습니다. 테스트는 Low 작업 6개를 실행한 뒤 High 작업 6개를 추가하는 방식으로 진행했습니다. 

먼저 작업 상태 프로파일링 결과는 아래와 같습니다.

처음에는 Low 작업들만 실행하다가 High 작업을 시작해야 하는 순간부터는 Suspension points에서 High 작업 우선으로 스레드 점유권을 할당했습니다. 따라서 이 시점부터는 앞서 진행한 테스트와 같이 Low 작업은 하나만 실행됐고, High 작업은 높은 확률로 스레드를 할당받으며 실행됐습니다.

스레드 측면에서도 유사한 결과가 나타났습니다. 앞서 실험과 마찬가지로 동시에 7개의 스레드가 활성화됐고, 최초에는 Low 작업을 실행하다가 이후 High 작업을 실행하면서 6개 스레드는 High, 1개 스레드는 Low 작업만을 실행했습니다.

TaskPriority 조사 결과 정리

테스트 결과는 아래와 같이 정리할 수 있습니다.

  • 우선순위가 다른 여러 작업이 있을 때 Suspension points에서 높은 확률로 우선순위가 높은 작업이 스레드를 할당받는다.
  • 현재 실행해야 하는 작업 중 우선순위가 가장 높은 작업이 스레드를 코어 수만큼 차지하고 있다면, 우선순위가 그보다 낮은 작업을 위해 별도 스레드가 추가된다. 따라서 스레드는 코어 수로 고정되는 것이 아니라 상황에 따라 그보다 많을 수 있으며, 이렇게 추가된 스레드는 우선순위가 낮은 작업만을 실행해서 우선순위가 높은 작업이 종료되기를 무한정 기다리는 것을 방지한다.

Suspension points 작동 방식

Suspension points란 OS에서 Swift Concurrency 작업의 스레드 점유권을 가져와 다른 작업에 할당할 수 있는 지점입니다. OS async API 내부에 존재하며, 따라서 await 키워드를 잠재적 Suspension points라고도 합니다.

OS async API를 사용할 때에는 Suspension points가 있다는 가정 하에 사용합니다. 그런데 Swift Concurrency 코드를 작성하다 보면 커스텀 async 함수를 만들어야 할 때가 있습니다. 이때 아래와 같은 두 가지 의문이 생깁니다.

  • Suspension points가 없다면 Swift Concurrency가 실행되는 스레드를 오래 점유해 성능에 악영향을 미칠 수 있는가?
  • await 키워드를 잠재적 Suspension points라고 부르는데 그렇다면 await 키워드만으로 OS에서 스레드 점유권을 다른 작업에게 양도하는가?

이 두 가지 의문을 해결하기 위해 테스트를 진행했습니다.

Suspension points가 없는 async 함수 실행 테스트

먼저 아래와 같이 랜덤으로 Integer 배열을 만들어 반환하는 간단한 async 함수를 만들어 테스트를 진행했습니다. await 키워드와 함께 호출되는 async 함수가 존재하지 않으며, 배열이 커지면 랜덤 Integer를 붙이는 과정이 오래 걸리면서 스레드를 오래 점유하게 됩니다.

func generateRandomNumbers(size: Int) -> [Int] async {
    var array: [Int] = []
 
    for i in 0..<size {
        array.append(Int.random(in: 0..<maximumRandomIntegerSize))
    }
 
    return array
}

테스트는 위 함수를 같은 우선순위로 10번 실행하는 방식으로 진행했습니다.

작업 상태 프로파일링 결과는 아래와 같습니다. 6개 작업만 동시에 실행됐는데요. 6개 작업의 우선순위가 같아서 코어 개수만큼만 스레드가 할당됐기 때문입니다. 늦게 추가된 4개 작업은 먼저 실행된 작업이 종료될 때마다 하나씩 추가돼 실행됐습니다.

스레드 프로파일링 결과는 아래와 같습니다. 코어 수만큼 활성화된 스레드에 작업이 할당됐고, Suspension points가 없기 때문에 스레드 점유권을 양도하지 않아 한 작업이 종료될 때까지 다른 작업은 실행되지 않았습니다.

이 결과는 Suspension points가 없으면 Swift Concurrency 코드가 올바르게 동시성(concurrency)을 살려 작동할 수 없다는 것을 의미합니다. 이 결과를 보니 또 다른 의문이 생겼습니다.

  • 위와 같이 Suspension points가 없는 작업들이 실행되고 있을 때 우선순위가 더 높은 작업이 추가되면 스레드 점유권을 양도받지 못해 실행이 늦어질 수 있는가?

의문을 해결하기 위해 Low → Medium → High 순으로 코어 수 만큼씩 작업을 추가하는 테스트를 진행했습니다. 모두 같은 코드로 Suspension points가 없습니다. 테스트 후 스레드 프로파일링 결과는 아래와 같습니다.

우선순위가 Medium인 작업이 추가되면, 기존에 활성화된 6개 스레드는 모두 Low 작업에 할당돼 있으므로 OS에서 6개 스레드를 추가로 활성화해 Medium 작업에 할당합니다. Low 작업 때문에 Medium 작업 실행이 늦어지는 것을 막기 위한 것으로 보입니다. 같은 원리로 High 작업이 추가되자 같은 수의 스레드가 또 추가돼 실행됐습니다. 결과적으로 코어 수의 3배만큼 스레드가 생성됐습니다.

앞서 말씀드렸듯 Swift Concurrency에서 내세운 장점 중 하나가 코어 수만큼의 스레드에서 실행돼 콘텍스트 스위칭이 적다는 것이었는데요. Suspension points를 배치하지 않을 경우 이 장점을 제대로 활용할 수 없다는 것을 알게 됐습니다.

await 키워드만으로 Suspension points가 배치될 수 있는지 테스트

WWDC 세션이나 관련 문서를 보면 await 키워드를 잠재적 Suspension points라고 설명하고 있습니다. 여기서 의문은 '코드를 실행하던 중 await 키워드를 만나면 OS에서 스레드 점유권을 다른 작업에 양도하는가?'였습니다.

만약 await 키워드만으로 스레드 점유권이 양도된다면, 해당 키워드를 만났을 때 OS에서 스레드 점유권을 다른 작업에 양도해야 하는 상황으로 판단하면 양도할 것입니다. 이 가정을 확인하기 위해 아래와 같은 코드를 작성해서 테스트했습니다.

func generateRandomNumbers(size: Int) -> [Int] async {
    var array: [Int] = []
 
    for i in 0..<size {
        array.append(Int.random(in: 0..<maximumRandomIntegerSize))
        if i % 10000 == 0 { await doSomething() }
    }
 
    return array
}
 
func doSomething() async {
    // ... codes without suspension point
}

위 코드에서 doSomething()은 내부에서 async 함수를 호출하지 않는 async 함수이기 때문에 내부에 Suspension points가 없을 것입니다. Suspension points가 없는 함수에서 특정 조건을 만족할 때 await 키워드와 함께 async 함수를 호출하는 테스트를 진행했고, 결과는 Suspension points가 될 수 없다고 나왔습니다. async 함수를 내부에서 await 키워드와 함께 실행했지만 스레드 점유권을 양도하거나 양도받는 상황은 나타나지 않았습니다.

커스텀 async 함수에 Suspension points를 추가하는 방법

위 테스트 결과를 바탕으로 아래와 같은 결론에 도달할 수 있습니다.

  • OS에서 제공하는 async API에는 내부에 Suspension points가 존재할 것이다.
  • 커스텀 async 함수 작성 시 OS async API를 호출하지 않는다면 기본적으로 Suspension points가 존재하지 않는다.

그렇다면 커스텀 async 함수를 작성할 때 Suspension points를 추가하기 위해서는 어떻게 해야 하는지 알아보겠습니다.

Task.yield() 호출

Task.yield()를 호출하면 명시적으로 해당 부분에서 스레드 점유권을 양도합니다. 앞서 Suspension points가 없는 async 함수를 호출했던 자리에서 Task.yield()를 호출해 보겠습니다.

func generateRandomNumbers(size: Int) -> [Int] async {
    var array: [Int] = []
 
    for i in 0..<size {
        array.append(Int.random(in: 0..<maximumRandomIntegerSize))
        if i % 10000 == 0 { await Task.yield() }
    }
 
    return array
}

Task.yield()를 호출하면 해당 부분이 Suspension points가 되면서 Swift Concurrency가 기대했던 대로 작동합니다. 앞서 Suspension points가 없을 때처럼 6개 작업 완료 후 4개 작업이 실행되는 것이 아니라 6개 스레드를 10개 작업이 공유하며 동시에 실행되는 모습을 볼 수 있습니다.

자식 작업 생성

자식(child) 작업을 생성하는 것도 Suspension points 역할을 할 수 있습니다. 자식 작업을 생성하면 OS는 작업 스케줄링을 진행하는데요. 이때 스레드를 우선 할당받아야 하는 작업에 스레드가 할당됩니다. 자식 작업은 아래와 같은 방법으로 생성할 수 있습니다.

  • async let binding 사용
  • withTaskGroup, withThrowingTaskGroup 등을 사용해 TaskGroup에 자식 작업 추가
Task.sleep() 호출

많이 사용할 방법은 아니겠지만 Task.sleep()을 호출하면 Task.yield()와 유사하게 Suspension points로 작동할 수 있습니다.

Suspension points 조사 결과 정리

Suspension points를 조사하고 테스트한 결론은 커스텀 async 함수를 작성할 때에는 Suspension points가 있는 코드인지를 잘 확인해야 한다는 것입니다. Suspension points가 없다면 작업은 자발적으로 스레드를 양도하지 않아서 Swift Concurrency의 한정된 스레드를 오래 점유할 수 있습니다. Suspension points는 Task.yield()Task.sleep()를 호출하거나 자식 작업을 생성하는 것으로 추가할 수 있습니다.

Swift Concurrency를 GCD 환경에서 실행할 때 주의할 점

Swift Concurrency 도입 이전에 만든 프로젝트나 GCD 기반으로 만든 프로젝트라면 Swift Concurrency 코드를 추가할 때 GCD와 함께 실행될 가능성이 높습니다. 그런데 Swift Concurrency가 코어 수 안팎으로 고정된 숫자의 스레드에서 작동한다는 점을 생각해 볼 때, 많은 스레드에서 작동할 수 있는 GCD 코드와 함께 실행하면 성능 관점에서 불리할 수도 있겠다는 생각이 들었습니다. 이를 확인하기 위해 테스트를 진행했습니다.

GCD 환경에서 작동하는 Swift Concurrency 코드 성능 조사

테스트는 우선순위가 같은 GCD 작업 20개(QoS: userInitiated)와 Swift Concurrency 작업(TaskPriority: High) 20개를 동시에 실행해서 걸리는 시간을 확인하는 방식으로 진행했습니다. 둘 모두 같은 코드를 실행했으며, GCD 작업을 실행하는 스레드가 지배적인 상황을 시뮬레이션하기 위해 GCD 작업은 작업당 하나의 스레드를 생성해서 실행되도록 진행했습니다.

결과는 아래와 같았습니다. 가운데 늦게 종료되는 스레드에 할당된 작업이 Swift Concurrency 작업들입니다.

우려했던 대로 Swift Concurrency가 작동하는 스레드가 한정돼 있어서(위 상황에서는 코어 수를 따라 6개) GCD 코드가 스레드를 많이 생성한 상황이라면 성능 관점에서 Swift Concurrency가 불리한 것을 확인할 수 있었습니다.

Swift Concurrency가 작동하는 스레드 개수가 s개, GCD가 작동하는 스레드가 g개라면 특정 코어에서 Swift Concurrency 코드가 작동할 확률은 s / (s + g)입니다. 이때 s는 개수에 제한이 있기 때문에 gs보다 커질 수 있습니다. 따라서 GCD 코드가 스레드를 지배적으로 차지하고 있는 상황에서는 Swift Concurrency 코드가 GCD 코드보다 성능 관점에서 불리할 수 있습니다.

이와 같은 점을 고려해 Swift Concurrency를 사용할 때 주의해야 할 점을 살펴보겠습니다.

Swift Concurrency가 작동하는 스레드를 오래 점유하면 안 된다

Swift Concurrency가 작동하는 스레드 개수는 한정적이기 때문에 스레드를 양도하지 않고 오래 점유하면 다른 작업 실행이 늦어질 수 있습니다. 따라서 Swift Concurrency 코드를 작성할 때에는 스레드를 오래 점유하지 않도록 신경 써야 하며, 이는 앞서 다뤘던 Suspension points에 관한 내용과 이어지는데요. 이번에는 커스텀 async 함수를 만들 때 Suspension points를 누락하는 것 외에 범할 수 있는 또 다른 실수 몇 가지를 살펴보겠습니다.

Task 블록 사용 시 주의할 점

아래와 같은 함수에서 someSubFunctionasync 함수로 변경됐다고 가정하겠습니다.

func someFunction() {
    ...
    let someVariable = someSubFunction(...)
    ...
}

이상적인 대응 방법은 someFunctionasync 함수로 변경하고 someSubFunction을 사용하는 것이지만, 특정 경우에는 someFunctionasync 함수로 변경하지 못할 수도 있습니다. 보통 이런 경우에는 Task 블록을 이용해 await 키워드가 있는 부분을 묶어줍니다. 코드로 보면 아래와 같습니다.

func someFunction() {
    ...
    Task {
        let someVariable = await someSubFunction(...)
        ... // Code A
    }
}

여기서 Task 블록 안과 밖은 다른 환경이므로 someVariable을 사용하는 코드인 code ATask 블록 안에 위치해야 하는데요. 이때 code A가 실행하는 데 오래 걸리는 코드라면 Swift Concurrency가 작동하는 스레드를 오래 점유할 수 있습니다. 만약 someSubFunctionasync 함수로 변경된 상황이라면 code A는 Suspension points를 갖고 있지 않을 텐데요. Suspension points 없이 Task 블록 안에서 오래 작동하면 앞서 말씀드렸듯 동시성을 살려 작동하지 않기 때문에 다른 작업의 시작을 늦출 수 있습니다.

따라서 Swift Concurrency 코드로 마이그레이션하면서 Task 블록으로 코드를 감싸려고 한다면 스레드를 오래 점유하지 않을지 살펴봐야 합니다.

withCheckedContinuation 사용 시 주의할 점

withCheckedContinuationcompletionHandler가 있는 코드를 Swift Concurrency 코드로 변환할 때 많이 사용합니다. 아래 예시 코드는 someNormalFunctioncompletionHandler가 있어서 someAsyncFunctionasync 함수로 작성하기 위해서 withCheckedContinuation을 사용한 경우입니다.

func someAsyncFunction(...) async {
    await withCheckedContinuation { continuation in
        someNormalFunction(...) {
            continuation.resume(returning: ())
        }
    }
}
 
func someNormalFunction(... , completionHandler: @escaping () -> Void) {
 
    ... // Code B
 
    someQueue.async {
        ...
        completionHandler()
    }
}

이때 withCheckedContinuation 블록 안은 기본적으로 Swift Concurrency가 작동하는 스레드에서 작동하기 때문에 위 코드에서 code B는 Swift Concurrency가 작동하는 스레드에서 시작됩니다. completionHandlerDispatchQueue를 이용해 실행되기 때문에 스레드 전환이 일어나는데요. 만약 code B가 스레드 전환 없이 스레드를 오래 점유하면 아래와 같이 작동합니다.

앞서 봤던 Suspension points가 없는 Swift Concurrency 코드와 유사한 결과입니다. Swift Concurrency가 작동하는 스레드를 오래 점유해 다른 작업 시작을 늦추고 있는 모습입니다.

이를 방지하려면, 만약 code B가 오래 걸리는 작업이라면 다른 스레드에서 실행될 수 있도록 변경해 줘야 합니다. completionHandler가 있는 함수라면 보통 네트워크 작업 등 비동기 작업을 하는 코드일 가능성이 높기 때문에 기본적으로 스레드 전환이 존재할 가능성이 높긴 한데요. 만약의 상황에도 대비할 수 있도록 이 부분 역시 조심해야 합니다. 

작업 실행 순서가 FIFO가 아니라는 점을 인지해야 한다

Swift Concurrency 작업 실행 순서는 작업 생성 순서가 아닙니다. 여러 개의 스레드에서 작업이 실행되기 때문에 먼저 생성된 작업이 늦게 생성된 작업보다 늦게 끝날 수도 있다는 것을 인지해야 합니다.

이와 관련해서 자칫 실수할 수 있는 상황을 예로 들어보겠습니다. 아래와 같은 코드에서 fetchResult 함수를 async 함수로 변경하는 작업을 진행한다고 가정하겠습니다.

class SomeClass {
    var arr: [Int] = []
    let queue = DispatchQueue(label: "SomeLabel")
 
    func fetchResults() {
         for i in 0..<200 {
            queue.async { [weak self] in
                let result = fetchResult(i)
                self?.arr.append(result)
            }
        }
    }
}

Swift Concurrency 환경에서 작동해야 하기 때문에 Stored Property 배열을 Actor에 포함시켜 Swift Concurrency 환경에서 데이터 레이스를 방지하며 사용할 수 있도록 변경해 보겠습니다.

Actor를 이용한 데이터 레이스 방지에 관해서는 WWDC 2022 - Eliminate data races using Swift Concurrency에서 더 자세히 확인하실 수 있습니다.

이때 아래와 같이 작성하는 실수를 범할 수 있습니다.

actor SomeActor {
    var arr: [Int] = []
 
    func append(_ value: Int) {
        arr.append(value)
    }
}
 
class SomeClass {
    var someActor = SomeActor()
 
    func fetchResults() {
         for i in 0..<200 {
             Task {
                 let result = await fetchResult(i)
                 await self.someActor.append(i)
             }
        }
    }
}

원래 코드에서는 반복문을 200번 실행하며 DispatchQueueSerialQueueasync 작업을 추가, 0번째부터 199번째 작업이 순서대로 실행되는 것이 보장됩니다. 하지만 변경한 코드에서는 실행 순서가 보장되지 않기 때문에 기존과 다른 결과가 나올 수 있습니다.

이 문제를 방지할 수 있는 가장 좋은 방법은 fetchResults 함수를 async 함수로 변경하는 것입니다.

class SomeClass {
    var someActor = SomeActor()
 
    func fetchResults() async {
         for i in 0..<200 {
             let result = await fetchResult(i)
             await self.someActor.append(i)
        }
    }
}

하지만 상황에 따라 fetchResults 함수를 async 함수로 변경하기 어려울 수 있습니다. 그럴 때는 아래와 같이 반복문 전체를 Task 블록으로 감싸는 방식으로 변경해도 됩니다. Suspension points가 존재하기 때문에 Task 블록에 오래 걸리는 부분이 있어도 스레드를 오래 점유하지는 않을 것입니다.

class SomeClass {
    var someActor = SomeActor()
 
    func fetchResults() {
        Task {
            for i in 0..<200 {
                let result = await fetchResult(i)
                await self.someActor.append(i)
           }
        }
    }
}

마치며

이번 글에서 살펴본 내용을 정리해 보겠습니다.

  • 커스텀 async 함수를 작성할 때에는 Suspension points가 있는지 잘 확인하며 작성해야 한다. Suspension points가 없다면 동시성을 활용하지 못할 수 있다.
  • Swift Concurrency가 작동할 수 있는 스레드 개수는 한정적이기 때문에 GCD 코드가 스레드를 많이 생성해서 실행하는 환경이라면 GCD 코드보다 더 느릴 수 있다.
  • Swift Concurrency가 작동하는 스레드를 오래 점유하지 않도록 신경 써야 한다.
    • withCheckedContinuation 블록 안 코드는 기본적으로 Swift Concurrency가 작동하는 스레드에서 실행되므로, 스레드를 오래 점유할 만한 작업이 있다면 스레드 전환을 고려해야 한다.
    • 특정 API를 async 함수로 마이그레이션하면서 Task 블록으로 코드를 감싼다면, async 함수로 변경된 함수를 호출한 후 스레드를 오래 점유할 가능성이 있는지 확인해야 한다.
  • Swift Concurrency 작업은 생성된 순서대로 실행되지 않기 때문에 이를 염두에 두고 코드를 작성해야 한다.

현재 코드를 새로 작성하면서 Swift Concurrency를 사용하는 분들이 많으실 것이라고 생각합니다. 또한 GCD 위주로 작성된 이전 코드를 Swift Concurrency 코드로 마이그레이션하는 분들도 많을 것이라고 생각하는데요. 그런 분들께 이 글이 도움이 되길 바라며 마치겠습니다.