티스토리 뷰

iOS/Swift

[swift] Swift Concurrency - Task (2)

yanni13 2025. 6. 19. 14:58

 

지난 블로그에서 Task의 개념, Task 취소, Task가 다른 비동기 작업이랑은

어떻게 다른 건지에 대해서 살펴봤다. 

 

 

 

[swift] Swift Concurrency - Task (1)

Swift Concurrency는 Swift 자체에서 비동기와 동기 처리를 더 안전하게 하고 간결하게 만들기 위한 기능이며 swift 5.5부터 도입되었다고 한다. async/await, Actor, @sendable등 여러 가지가 있지만 그중에서 오

yanni13.tistory.com

 

오늘은 Task - 2탄으로 Task의 종류에 대해서 알아보도록 하자!

 

 

✏️ Task의 종류

 

Async-let

동시 실행을 예약해두고 필요한 시점에서 기다리는 방식

 

기존방식의 binding

 

기존 방식에서 let result가 실행되기 전에 초기화 작업(URL Session 작업)이 먼저 실행된다. 이후 결과가 나올 때까지 기다린 후에 result에 값을 할당하고 다음 코드가 실행되는 방식이었다!

 

변수를 선언하는 동시에 바로 실행&대기가 이루어지며 동기적으로 작동하는 프로세스임을 알 수 있다.

 

async let을 사용한 binding

 

실행 순서는 다음과 같다.

  1. preceding statements (이전의 모든 코드) 실행
  2. async let result에서child task가 생성되며 바로 실행 시작된다.
  3. assign placeholder - result라는 이름으로 값을 담을 공간이 먼저 예약된다.
  4. result를 기다리지 않고 바로 following statements가 실행된다.
  5. try await result 시점에서 child task 결과를 기다린다. (result가 필요한 시점에서 기다림)
  6. 작업이 끝나면 result에 값이 채워지거나 에러를 발생시킨다.

 

async let을 호출하면 내부적으로 child task가 생성되기 때문에 생성되는 시점에서 바로 비동기로 작업을 시작하게 된다. (= 그래서 비동기적으로 병렬 수행이 가능한 것!)

 

이후 마지막에 try await result를 통해 필요한 시점에서 결과를 대기한다.

 

이전 방식처럼 값이 반환될 때까지 기다리는 것이 아니라, 다음 코드로 넘어가서 작업이 동시에 진행되게끔 만들어주는 것이다.

 

📍 Child Task가 필요한 이유

여러 개의 async let을 실행하면 child task가 그만큼 생성되고, 각 작업을 실행 -> 완료 -> 결과 보관까지 하고 있다가 마지막에 try await을 통해 결과를 꺼내오면 되기 때문에 효율적으로 작업을 실행할 수 있게 됨!

 

따라서 비동기적으로 병렬 수행이 가능하다는 것을 확인할 수 있고. 필요한 순간에만 await를 실행하여 효율적인 실행흐름을 만들 수 있다.

 

 

주의할 점은 async let은 child task를 관리해야 하기 때문에 await 없이 사용하면 에러가 발생할 수 있으며 항상 try await를 호출해야 task가 종료되고 자원이 해제되어 결과를 발생시키기 때문에, 메모리 누수가 발생할 수 있다.

 

 

Task Group

여러 개의 Task를 group으로 묶어서 병렬로 실행하고 그 결과를 수집할 수 있게 해 준다.

 

 

예시로 이해해 보자

 

"모든 썸네일을 한 번에 다운로드해본다고 가정해 보자"

 

async-let 과의 차이점

 

 

예를 들어 async let을 반복문에서 사용할 경우 반복문을 돌 때마다 부모 Task가 생성되고 , 그 안에서 child Task가 만들어진다는 걸 위에 async-let에서 배웠다!

 

그렇지만 각각의 반복이 끝나기 전에 반드시 childTask가 모두 완료되어야 다음 반복이 시작되기 때문에 모든 썸네일을 동시에 가져오는 것이 아니라 각 반복마다 두 개씩만 동시에 실행되고 자식 1, 자식 2에 대한 작업이 끝나야 다음 두 개의 자식 반복이 시작되는 구조이다.

 

 

반면 Task Group을 사용하게 되면 각 반복을 돌 때마다 부모 Task가 생성되는 게 아니라, 전체에서 딱 한 번의 부모 Task가 생성되고 그 안에서 반복문을 돌며 자식 Task를 원하는 만큼 계속 추가할 수 있다.

 

즉 한번의 부모 Task 아래에서 필요한 만큼의 자식 Task가 모두 생성되어 동시에 실행되기 때문에 모든 썸네일 다운로드가 한 번에 시작될 수 있다는 것이다!

 

이외에도 TaskGroup은 정해진 범위 안에서만 사용해야 하며 구조적인 제한이 있다고 한다.

특히 group.addTask { }는 내부 상태를 바꾸는 mutating 연산이기 때문에 child task이나 다른 동시적인 context에서 실행하면 문제가 발생할 수 있기 때문에 구조적인 제한을 두고 있다.

 

 

Unstructed Task

구조화되지 않은 비동기 작업을 의미한다.

 

 

앞서 설명했던 Task들은 부모-자식 관계의 계층을 가지는 구조적 Task이다.  반면 Unstructed Task는 부모 자식 관계가 존재하지 않는 task를 의미한다.

 

구조적 task는 task의 종료, 에러 및 취소 전파를 자동으로 관리해 주지만 Unstructed Task는 더 많은 유연성을 제공하지만 개발자가 수동으로 관리해주어야 할 부분이 많다.

 

Task {
    await loadUser()
}

Task {
    await loadImage()
} 

Task {
    await loadProfile()
}

 

위와 같은 예시가 비구조적인 작업의 가장 흔한 예시이다.

 

각각의 Task 블록은 독립된 Task로 이루어지고 있으며 어떤 부모 Task에도 속해있지 않고 서로 연결되어 있지 않은 상태임을 확인할 수 있다.

 

어떤 점에서 Task가 더 많은 유연성을 제공한다는 것일까?

각 Task는 아무 위치에서나 자유롭게 사용할 수 있는 반면, 구조적인 Task는 async 블록 안에서만 사용할 수 있기 때문에 Unstructed Task에 비해 자유롭지 못하다.

 

하지만 비구조적인 만큼 수동으로 life cycle을 관리하도록 하여 task가 언제 시작되고 끝나는지를 추적해서 task취소, 종료 작업을 구현해주어야 한다.

 

📍 Unstructed Task를 사용하는 경우

1. 비동기 코드가 구조적 계층에 속하지 않을 때
2. 비동기 함수가 아닌 곳에서 비동기 작업을 시작해야 할 때
3. UI 이벤트 같은 작업을 시작하고 나중에 별도 취소를 해야 할 때

 

각각의 경우를 간단하게 살펴보자

 

1번의 경우 AppDelegate 같은 경우에 앱이 실행될 때 딱 한 번만 실행되어야 하는 초기화 작업 같은 경우나, 백그라운드에서 수행되는 동기화 로직은 구조적 계층에 속하는 것이 아니라, 독립적으로 동작해야 하는 작업이므로 Unstructed Task를 사용해야 하는 경우이다.

 

2번의 경우 onAppear , onChange 블록 안에 Task 작업을 실행해야 할 때 이다.

.onAppear {
	// View가 나타날 때 이미지 로드
	Task {
		placeImage = await placeManager.getCurrentPlaceImage(place: place, destinationId: currentDestinationId)
	}
}

 

해당 코드를 보면 이미지 로딩 도중 view가 사라지는 경우를 대비해서 Task를 취소하거나 최신 요청만 반영하는 로직등 예외처리가 필요한 케이스이기 때문에 onAppear안에 Task 작업을 처리하고 있는데 이게 비동기 작업을 비동기 함수가 아닌 곳에 사용해야 할 때의 예시이다

 

3번의 경우 어떤 버튼을 눌렀을 때 비동기 작업을 이벤트에 반응해서 시작하거나 그 이후 별도로 취소해야 하는 경우이다.

이것도 2번이랑 꽤 유사하다.

@State var loadingTask: Task<Void, Never>? = nil

Button("로딩 시작") {
    loadingTask = Task {
        await viewModel.keepLoading()
    }
}

Button("중단") {
    loadingTask?.cancel()
}

 

모두 async가 아니라 독립된 위치에서 실행된다는 특징이 있지만, 버튼을 누를 때마다 Task의 시작, 취소를 제어하는 경우엔 Task를 따로 저장하고 수동으로 직접 취소해야 하기 때문에 Unstructed Task가 필요한 경우이다.

 

 

Detached Task

우선순위와 actor context를 전혀 상속받지 않고 완전히 독립적인 실행 흐름을 가지는 작업

 

 

Detached Task는 context에서 아무것도 받지 않기 때문에 완전히 독립적인 실행 흐름을 가진다. 따라서 우선순위나 실행 actor를 지정할 수 있다.

Task.detached(priority: .background) {
    await doSomething()
}

 

이런 식으로 우선순위를 지정할 수 있다. 백그라운드로 우선순위를 지정해서 UI랑 작업을 무관하게 설정하고, 부모 Task가 취소되어도 완전히 독립적으로 동작한다.

 

반면 완전히 독립적인 실행 흐름을 갖기 때문에 부모 Task가 취소되어도 자동으로 취소되지 않아서 Retain Cycle의 위험이 더 커질 수 있다.

이를 방지하기 위해선 Task 객체를 개발자가 직접 저장하고 수동으로 취소해야 하기 때문에 매번 코드에서 Task를 사용하고 직접 저장, 취소까지 해야 하는 번거로움이 존재한다.

 

그래서 Detached Task는 꼭 필요한 경우에만 사용하고 최소한으로 사용하는 걸 권장하고 있다.

 

 

정리

  생성방법 어디서 생성되는지 lifecycle 취소 방식 상속받는 컨텍스트
async-let async let async 함수 내부 해당 문장 범위 자동 우선순위,
task-local 값
Group Tasks group.async withTaskGroup Task Group 범위 자동 우선순위,
task-local 값
Unstructed Tasks Task - 제한 x Task로 수동 취소 우선순위, t
ask-local 값, actor
Detached tasks Task.detached{} - 제한 x Task로 수동 취소 x

 

 

앞서 공부해 봤던 내용을 바탕으로 표로 요약해 봤다

 

필요한 상황에 따라 판단해서 사용하는 것이 중요하겠지만 apple에서는 구조화된 동시성 async let이나, group tasks와 같은 걸 사용하여 lifecycle에 맞추도록 처리하는 것을 권장하고 있다고 한다.

 


참고

https://developer.apple.com/documentation/swift/taskgroup

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

https://www.avanderlee.com/concurrency/detached-tasks/

 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/08   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31
글 보관함