Swift

Swift Concurrency #마무리 - 여러 번 보면서 이해한 Swift Concurrency

빨간체리반지 2026. 3. 2. 15:00

앞서 Swift Concurrency와 관련된 WWDC 영상을 보며 전반적인 내용을 훑어보았다.

나 역시 해당 영상을 여러 번 반복해서 시청했다. 처음에는 잘 이해되지 않던 개념들도, 다시 보면서 조금씩 정리가 되었고, 영상을 본 뒤 한동안 Concurrency를 사용하지 않다 보면 다시 헷갈리는 부분들도 있었다.
그럴 때마다 내가 작성했던 필기 내용을 다시 읽으면서 많은 도움을 받을 수 있었다.

결국 모르는 내용은 끈기 있게 찾아보고, 스스로 정리하면서 공부하는 방법밖에는 없는 것 같다는 생각이 들었다.

영어 블로그도 작성하면서 개념이 또 다시 정리되었고, 복습의 효과가 있어서 개념을 기억하기 좋았던것 같다.

 

원래는 매주 1블로그 포스팅으로 시작했던 나만의 소소한 프로젝트였는데..ㅎㅎ

개인적인 이슈도 있어서 미뤄지다가 이제야 마무리를 할 수 있었다. 휴.. 큰 숙제 하나 끝낸 느낌이다 🥳

 

Swift Concurrency 정리 목록

아래는 블로그에 정리한 Swift Concurrency 시리즈이다. 전반적으로 기초적인 개념 위주로 정리되어있다.

추가로 공부하면 좋은 자료들

위 블로그 내용 외에도, Swift Concurrency를 더 자세히 이해하는 데 도움이 될만한 자료도 첨부한다.

WWDC Sessions

공식 문서


마지막으로 실제 프로젝트에 적용하면서 헷갈렸던 부분과 주의해야 했던 점들을 기록하고 마무리하려고 한다.

 

Swift Concurrency 테스트용으로 사용했던 GitHub repo도 첨부한다.

 

GitHub - hobin-han/SwiftConcurrencyLab

Contribute to hobin-han/SwiftConcurrencyLab development by creating an account on GitHub.

github.com

 

withCheckedContinuation 와 Task cancel

  1. withCheckedContinuation을 사용할 땐, 반드시 최소 한번 resume(succeed/failed/cancelled) 되어야 한다.
    누락되는 경우 메모리 leak이 발생한다.
  2. Task는 Combine의 subscription과 달리 Task 가 deinit 되어도 cancel()이 자동 호출되지 않는다.
  3. Task.isCancelled 는 Swift Concurrency 가 아닌 다른 비동기 로직 내부에서는 동작하지 않는다.

 

Task { } 로 여러번 감쌀 때에는 주의해서 사용한다

Task {}는 structured Task 가 아니기 때문에 부모~자식 Task 구조를 갖지 않으며, 상위 Task 가 취소되어도 하위 Task 가 취소되지 않는다.

Task {
  let value = await getValue()
  doSomething(value)
}

func doSomething(value: Any) {
  Task {
    // ...
  }
}

 

Task 사용시 [weak self] 가 필요할까?

필요 없다. 강한 순환 참조가 발생하지 않는다.

Unstructured Task는 아래처럼 withTaskCancellationHandler 를 사용해 관리해주자.

func performUnstructuredWork() async throws {
    let task = Task {
        // ... some work ...
    }

    try await withTaskCancellationHandler {
        try await task.value
    } onCancel: {
        task.cancel() // <- unstructured task 를 이런식으로 cancel 시켜줄 수 있다.
    }
}

 

Actor 전환은 최소화 한다

테스트 코드

특히나 MainActor 에서의 다른 actor 로의 호핑 하는건 비용이 많이들며, MainActor가 아닌 경우에도 actor hoping이 무한히 반복되는 경우 성능이 좋지 않다.

  • Test testActorPerformanceWithMainActorAtOnce() passed after 0.001 seconds.
  • Test testActorPerformanceWithCommonActorAtOnce() passed after 0.001 seconds.
  • Test testActorPerformanceWithCommonActorSeveralTimes() passed after 0.002 seconds.
  • Test testActorPerformanceWithMainActorSeveralTimes() passed after 0.030 seconds.

→ MainActor로의 hoping 시도시 더 오래 걸리긴 하지만, 성능 이슈가 크진 않으므로 무한히 반복되는 구조가 아니라면 Actor hoping 횟수 제한을 두지는 않아도 될것 같다.

 

Actor의 isolation

테스트 코드

Actor내의 함수들은 동시에 호출되어도 진행중인 actor 작업이 끝나야 다음 작업 실행된다.

 

Task와 Priority

테스트 코드

  • Task {} 의 default priority 는 .high
  • Task.detached {} 의 default priority 는 .medium
  • Task {} 생성시 priority 를 직접지정하지 않으면, 상위 Task 의 priority 로 생성된다.
  • Task {} 생성시 priority 를 직접 지정해줄 수도 있다.
  • Task.detached {} 로 생성시에는 current Task 의 priority 와 관계없이 생성된다.

 

MainActor 내의 nonisolated 함수 와 MainThread 보장여부

import Foundation
import Testing

@MainActor
class NonisolatedTests {
    
    @Test
    func inheritedActorContextTask() {
        Task {
            #expect(#isolation === MainActor.shared)
            
            let actor = CounterActor()
            await actor.resetSlowly(to: 10)
            
            #expect(#isolation === MainActor.shared)
        }
    }
    
    @Test
    nonisolated func nonInheritedActorContextTask() {
        Task {
            #expect(#isolation !== MainActor.shared)
            
            let actor = CounterActor()
            await actor.resetSlowly(to: 10)
            
            #expect(#isolation !== MainActor.shared)
        }
    }
}
  • inheritedActorContextTask() → Main Actor O
  • nonInheritedActorContextTask() → Main Actor X