본문 바로가기
콩코롱시의 밤

[Swift concurrency] 콩코롱시의 밤 4일차

by 차코.. 2025. 6. 20.

 

4일차 주제: Structured concurrency

 

 

안녕하새요.

날이 많이 덥네요 … ♨

 

저도 에어컨을 틀었어요

 

콩코롱시의 밤 4회차를 시작해보겠습니다.

 

 


🧨 문제 상황 발생

 

 

2회차의 밤에서 소개했었던 fetchThumanil 함수를 기억하시나요?

 

문자열을 URLRequest로 변환 URLRequest를 기반으로 데이터 가져오기 데이터 기반 이미지 생성  썸네일 렌더링
동기 비동기 동기 비동기

 

 

위와 같이 URLRequest로 데이터를 가져와, 이미지로 썸네일 렌더링을 하는 함수였는데요.

 

 

오늘은 여러 개의 이미지를 받아서 각각 썸네일 렌더링을 하는

fetchThumanils (s 추가) 함수를 살펴보려고 합니다.

 

 

 

 

위와 같이 for문으로 각 이미지의 아이디로 접근해서, 썸네일을 하나씩 비동기적으로 만들고 있어요.

 

그런데… 이 함수에는 함정이 있습니다.

만약 이 함수에 수천 장의 썸네일을 처리하라고 요청하면 어떻게 될까요?

 

사실 이 비동기 요청은 순차적으로 이루어지고 있습니다. 그림으로 보면,

 

Task 1이 끝나길 기다리고,

끝나면 Task 2가 시작되고, 또 끝나길 기다리고,

… 이어서 반복되는 구조인건데요.

 

(정확하게 표현하자면, 여러 개의 스레드에서 동작할 수 있긴 하지만, 순차적이라는 사실에는 변함이 없어요.)

 

 

그러니 수천 장의 사진을 요청해버리면, for문이 모두 돌 때까지 기다려야 하니 아주 오래 걸릴 것입니다.

 

 


✅ async-let

 

위 방법을 해결하기에 앞서, 기존의 let 바인딩 방식을 짚어보려고 합니다.

 

let result = URLSession.shared.data(...) // let 바인딩

 

 

  1. data(...) 함수 실행
  2. 다운로드 종료 시까지 await
  3. 결과를 result에 저장
  4. 그 이후 작업 실행

의 과정을 거치는 기존의 방식은,

다시 말해 위에서 아래로 순차적으로 발생하는데요.

 

위 방식은 nn개의 작업을 실행하고자 한다면, 순차적으로 진행되기에 당연히 느릴 것입니다.

 

 

때문에, 이 데이터를 바로 사용하는 것이 아니라면,

그동안 프로그램이 다른 일을 병행하면서 이를 여러 개 같이 처리하는 것이 효율적일 거예요.

 

이를 가능하게 하는 것이 async-let 입니다.

 

async let result = URLSession.shared.data(...) // async let 바인딩

 

 

 

 

async let 줄이 실행되면, Swift는 두 가지 작업을 동시에 시작해요.

  • 자식 작업을 생성
    • 다운로드 작업을 새로운 비동기 컨택스트로 실행을 시작합니다.
  • 부모 작업 계속
    • result에는 일단 placeholder 값만 넣고, 부모는 다음 코드로 계속 진행합니다.

다시 말해, 자식을 만들어서 백그라운드에서 일하게 하는 것이지요!!

 

 

이러한 구조를 Structured Concurrency, 구조적 동시성이라고 부릅니다.

 

 

구조적 동시성
- 병렬로 실행될 자식 Task를 생성해서, 명시적인 부모-자식 관계가 생기는 것입니다.
- 위와 같은 구조를 Task Tree라고 부릅니다.

 

이제 썸네일 가져오는 함수에도 async-let을 두 개 만들어, 자식 Task를 두 개 만들어보겠습니다.

(기존의 이미지 데이터 다운로드 작업 + 이미지 사이즈 불러오기 작업)

 

 

 

 

  • 이미지 데이터 다운로드
  • 메타 데이터 다운로드 (썸네일 사이즈 정보)

이제 한꺼번에 두 개의 네트워크 통신 요청을 보내게 되었는데요.

 

만약 일반 let을 사용했다면,

이미지 데이터가 모두 다운로드 된 이후 → 메타 데이터가 다운로드 될테지만

 

async-let을 사용했기 때문에, 두 다운로드가 동시에 시작됩니다.

 

이 때, async let으로 바인딩한 변수도 기존과 동일한 타입입니다.

 

 


Task Tree

두 명의 자식을 가지게 된…

 

그렇다면 여기에서 궁금한 점이 생기는데요.

만약 자식 Task들 중 한 개가 오류가 나면 어떻게 될까요?

 

 

 

 

예를 들어… 아까 존재했던 두 가지의 동시성 작업 중

  • 이미지 데이터 다운로드 → 아직 하는 중인데 ..🥹
  • 메타 데이터 다운로드 (썸네일 사이즈 정보) → 실패함 ! ❌

메타 데이터 다운로드가 에러를 뱉고 종료되었다고 가정해볼게요.

 

 

그렇다면 부모인 fetchOneThumbnail은 종료될텐데,

이미 진행 중인 이미지 데이터 다운로드는 어떻게 할까요?

 

 

이 때, Swift가 자동으로 이미지 데이터 다운로드 작업에 취소 신호를 보냅니다.

함수가 비정상적으로 종료돼도, 실행 중인 자식 Task를 안전하게 정리해주는 것입니다.

(ARC처럼 작업 수명을 관리하도록 도와주어, 누수를 방지합니다.)

 


+) Cancellation

 

여기서 잠깐!

 

Swift는 Task를 “당장 중단”이 아닌, 해당 작업이 더 이상 필요없다고 신호만 보냅니다.

대신 취소되었는지 스스로 확인하고, 멈추도록 하는데요.

 

이러한 방식을 협조적 (cooperative)라고 합니다

 

무슨 소리냐구요…?

 

 

요렇게 isCancelled와 같은 취소 신호를 감지하면, Task를 알아서 취소할 수 있도록 제공하는 것이지요.

 

 

강제로 Task를 중단하지 않고, 협조적 취소를 하는 이유는
- 작업이 파일을 저장 중이거나,
- 데이터베이스 쓰기 작업 등의 중요한 트랜잭션을 수행 중이라면,
도중에 강제 중단될 경우 데이터가 꼬이거나 리소스가 유출될 위험이 있기 때문이에요.

 


어쨌든.

 

엄마 저를 책임져주실거죠

 

이렇게 부모-자식의 트리 구조를 가졌기 때문에

 

  • 취소가 자동으로 전파
    • 부모가 취소되면 자식도 취소됩니다.
  • 우선순위 상속
    • 부모의 우선순위를 자식도 물려 받습니다.
  • Task-local 변수 공유
    • 부모 작업의 변수를 그대로 물려 받습니다.

 

의 특징을 가지게 됩니다.

 

또한, 부모는 자식 작업들이 모두 완료되는 것을 기다립니다.

 

 


 Group Task

 

아까의 썸네일 fetch 함수로 돌아가보겠습니다.

 

 

기존에는 이미지, 메타 데이터 두 가지를 동시에 받아오고 있었는데요.

 

그런데 nn개의 썸네일을 병렬적으로 불러오고 싶다면 async-let을 사용하는 것이 좋을까요?

썸네일의 개수가 몇 개가 될 지 모르기 때문에, async-let을 쓰는 것은 부적절할 것입니다.

 

여기에서 유용하게 사용될 수 있는 것이 TaskGroup 입니다.

 

 

때문에 썸네일을 nn개 가져오고 싶은 경우,

 

 

위처럼 Task Group을 사용할 수 있겠습니다.

 

비동기이기 때문에 작업 실행 순서는 보장되지 않지만, 결과는 순서 없이 하나씩 기다리면서 수집이 가능해요.

 


 Unstructured tasks

 

조아요.

지금까지 Task Tree로 구조적인 특징을 이용해, 효율적으로 작업들을 관리하는 방법을 알아보았는데요.

 

그런데 구조화가 불필요한 상황도 있을까요? 🤔

다음과 같은 예시를 들어보겠습니다.

 

하하 홈화면 애니메이션

 

바로바로 이번에는 메이커스에서의 예시를 들고와봤어요 (ㅎㅎ)

 

해당 애니메이션은 VC와 Cell이 모두 그려진 이후, 홈화면이 노출되는 동안 무한 루프를 구는 구조입니다.

 

애니메이션 시작 지점

 

dataSource.apply(snapshot, animatingDifferences: true) { [weak self] in
    // 애니메이션은 data가 apply된 이후에 실행
    self?.startPlaygroundNewsAnimationLoop()
    self?.startRecentPostAnimationLoop()
}

 

 

그래서 위와 같이 apply 이후에 시작하도록 했어요.

 

 

 

그 이후, Task 안에서 무한 loop을 반복할 수 있도록 구현했는데요.

 

 

그런데.. 만약 사용자가 홈화면에서 벗어난다면 어떻게 해야할까요?

무한 루프로 진행되고 있던 애니메이션을 종료해야 할텐데요.

 

 

애니메이션을 종료하고 싶은 시점은 View Disappear 시점입니다.

 

Task가 활성화된 시점은 apply 핸들러 안이었는데!!!!!

종료는 ViewDidDisappear(_)에서 해줘야 하는 상황이 온 거죠!!!!

 

즉, 작업의 생명주기가 현재 스코프를 넘어선 상황입니다.

 

 

 

이러한 상황을 위해서 Unstructured Task가 제공됩니다.

 

Unstructured Task는 구조화되지 않은 작업, 즉 작업 트리에 속하지 않는 독립적인 Task입니다.

 

위처럼 상황에 따라 명확한 작업 계층을 갖기 어려운 경우가 있기 때문에 필요한데요.

 

예를 들어, UI에서 사용자가 버튼을 눌렀을 때 작업을 시작한다던지,

알림을 받았을 때 백그라운드 작업을 시작하는 것처럼

부모 작업이 존재하지 않거나, 관리가 불가능할 때 사용합니다.

 

대신 누수 방지를 위해 꼭 수동으로 cancel 해주어야게쬬

 

 

var playgroundNewsAnimationTask: Task<Void, Never>?

 

그래서 위처럼 Task 인스턴스를 저장하고

 

override func viewDidDisappear(_ animated: Bool) {
    super.viewDidDisappear(animated)
    stopPlaygroundNewsAnimationLoop()
}

func stopPlaygroundNewsAnimationLoop() {
    playgroundNewsAnimationTask?.cancel()
    playgroundNewsAnimationTask = nil
}

 

 

viewDidDisappear 시점에서 cancel 해주었어요.

 


Detached tasks

 

때로는 내가 속한 컨텍스트로부터 아무것도 상속받지 않고 싶을수도 있겠쬬

 

엄마 아무것도 주지 마세요

 

그래서 스코프와 무관하게, 아무것도 상속받지 않는 Detached tasks가 존재합니다.

 

이는 의미 있는 작업을 백그라운드에서 안전하게, 독립적으로 처리할 때 유용합니다.

 

예를 들어 캐시 저장 작업처럼요!!!

 

 

 

  • UI와 무관합니다.
  • 비동기 가능성이 있습니다. (파일 I/O)
  • 작업 취소와 무관합니다.
  • 정확한 컨텍스트 분리가 필요합니다. (그래야 안전합니다.)

이 때, priority를 .background로 우선순위를 명시해, UI와 충돌을 방지해줍니다.

 

 

하나의 detached task 안에 Task Group을 구성해 병렬로 처리할 수도 있습니다.

 

 


Behind the scene: GCD와의 비교

 

마지막으로, 과거 구조적 동시성이 없었을 때는 어땠는지 언급하고 마무리하겠습니다.

 

 

과거에는 앱에서 동시에 여러 작업을 안전하게 처리하는 것이 어려웠기 때문에,

GCD에서는 앱의 각 기능(서브시스템)마다 하나의 직렬 큐만 쓰라고 권장했어요.

 

이렇게 하니, 작업을 병렬처리하지 못한다는 단점이 있었어요.

 

 

블록으로 표현!!

 

하지만 이제는

Task Group을 통해, 하나의 부모 작업이 여러 자식 작업들을 만들 수 있게 되었고,

부모 작업들은, 자식 작업들이 마무리되기 전까지는 절대 마무리 될 수 없게 되었잖아요?

 

이런 관계가 코드 구조(Task Group 블록)로 명확히 표현되기 때문에,

Swift 컴파일러와 런타임이 정확히 이해할 수 있게 되었고!

 

앱의 실행 흐름과 동시성 구조를 정확하게 이해하고 제어할 수 있게 되었다 합니닷.

 


참고

 

https://developer.apple.com/videos/play/wwdc2021/10134/

 

Explore structured concurrency in Swift - WWDC21 - Videos - Apple Developer

When you have code that needs to run at the same time as other code, it's important to choose the right tool for the job. We'll take you...

developer.apple.com

https://developer.apple.com/videos/play/wwdc2021/10254/

 

Swift concurrency: Behind the scenes - WWDC21 - Videos - Apple Developer

Dive into the details of Swift concurrency and discover how Swift provides greater safety from data races and thread explosion while...

developer.apple.com