Swift Concurrency에서 Task는 어떤 역할을 담당하나

2024.08.19 - [◽️ Programming/T I L] - Swift의 async/await에 대해서 알아보자

 

Swift의 async/await에 대해서 알아보자

Swift 5.5부터 도입된 async/await는 기존의 비동기 처리방식인 DispatchQueue나 completionHandler를 사용해 처리했지만 더욱 직관적이고 관리하기 쉽게 만들어 주는 기능이다. 기존의 비동기 처리 방식과 비

dongdida.tistory.com

 

이전에 async/await 에 대해서 알아보는 시간을 가졌지만 이번엔 Task는 어떤 역할을 담당하고 있는지 한번 살펴보자

 

Task

Task는 Swift Concurrency에서 비동기 작업을 나타내며, async/await 문법과 결합하여 동기 코드처럼 읽히면서도 내부적으로는 비동기로 실행될 수 있도록 한다!

 

역할은 크게 세가지로 볼 수 있는데 첫번째로 비동기 작업을 실행시키는 역할이다. Task는 비동기 함수나 작업을 시작하고, 그 결과를 기다리거나 에러를 처리할 수 있도록 한다.

 

두번째로 구조적으로 동시성을 띄고 있는데 Task는 계층적인 구조를 형성해서 상위 Task가 하위 Task들을 생성하고 관리할 수 있게 설정할 수 있다. 이를 통해 코드 전체의 실행 흐름과 에러, 취소 처리 등 용이한 여러가지를 가능하게 해준다.

 

마지막 세번째로 우선 순위를 관리해 Task 생성 후 우선 순위를 지정해 작업 간의 중요도를 관리할 수 있게 된다.

 

그렇다면 Task는 왜 사용하게 되는 걸까?

  • 가독성과 유지보수성이 향상 된다.

기존의 GCD를 활용한 방식에서는 중첩된 클로저와 복잡한 콜백 체인으로 인해 코드의 가독성이 크게 떨어지는 경우가 종종 있었다. 하지만 Task를 사용하면 async/await 문법을 통해 마치 동기 코드처럼 순차적이고 명확하게 비동기 로직을 작성할 수 있다.

  • 에러 핸들링 용이

Task 내 async 함수는 throws와 결합해서 사용될 수 있는데 이를 통해 에러가 발생했을때 do-catch 구문을 사용해 깔끔하게 에러를 처리할 수 있다는 큰 장점이 있다. 에러가 Task 트리 전체로 전파되기 때문에 상위 Task에서 하위 Task의 에러를 쉽게 관리할 수 있게 된다는 점이 아주 유용하다.

  • Cancellation 지원

Task는 내장된 취소 메커니즘을 제공하는데, 작업 도중 Task.checkCancellation()을 호출하거나, Task.isCancelled를 확인해 불필요한 작업을 조기에 중단할 수 있다. 이 방법은 리소스 낭비를 막고, 사용자 경험을 향상시키는데 중요한 역할을 한다!

 

Task의 주요 역할 및 중요 포인트!!

  • 비동기 실행 단위

Task는 비동기 작업의 가장 작은 실행 단위로, 각 Task는 독립적으로 실행되며, 결과를 반환하거나 에러를 발생시킬 수 있다. 이로 인해 복잡한 비동기 로직도 명확하게 분리하고 관리할 수 있다!!

  • Structured Concurrency의 핵심이다

Swift Concurrency에서는 Task를 계층 구조로 구성함으로써, 상위 Task가 하위 Task들의 생명 주기를 관리할 수 있다. 이는 에러 전달, Cancellation 그리고 리소스 관리 측면에서 큰 이점을 제공한다. 예를 들어 상위 Task가 취소되면 그에 속한 모든 하위 Task도 함께 취소되어 예기치 않은 동시성 문제를 방지할 수 있다.

  • 우선순위와 취소

Task 생성 시 우선순위를 지정하면, 시스템은 해당 작업의 실행 우선 순위를 고려해 스케줄링한다. 또한 Task 내 Task.isCancelled를 통해 현재 작업이 취소되었는지 확인 할 수 있어 불필요한 작업을 조기에 종료시킬 수 있다!


그렇다면 코드를 통해 지금까지 설명한 걸 적용해보고 이해하는 시간을 가져보자!

import Foundation

// 비동기 함수: 네트워크 요청이나 데이터 처리 등을 시뮬레이션
func fetchData() async throws -> String {
    // 2초 동안 대기 (비동기 작업 시뮬레이션)
    try await Task.sleep(nanoseconds: 2 * 1_000_000_000)
    // 작업 도중 취소되었는지 체크
    try Task.checkCancellation()
    return "Fetched Data"
}

이 코드를 살펴보면 async throws를 사용해 비동기 작업 중 발생할 수 있는 에러를 처리할 수 있도록 구현되어 있는 메서드이다.

 

Task.sleep(nanoseconds:)를 통해 2초간 작업을 대기하면서 비동기 작업을 시뮬레이션 한다. 그 다음 Task.checkCancellation()을 호출해서 작업 도중 취소 요청이 들어왔는지 확인할 수 있다는 장점이 보인다 🙂

func loadDataAndUpdateUI() {
    // 새로운 Task 생성 (구조적 동시성 내에서 실행)
    Task {
        do {
            // 비동기 함수 호출
            let data = try await fetchData()
            // 메인 스레드에서 안전하게 UI 업데이트
            await MainActor.run {
                print("UI 업데이트: \\(data)")
            }
        } catch {
            // 에러 처리
            print("에러 발생: \\(error)")
        }
    }
}

두번째 메서드를 살펴보면 Task { } 블록 내 비동기 작업을 시작하며, 이는 구조적 동시성에 의해 관리되게 된다. 비동기 함수 호출 후 await MainActor.run 을 사용해 UI 업데이트를 메인 스레드에서 안전하게 수행하도록 설정할 수 있다.

 

그 다음 do-catch 구문을 통해 에러를 처리하고 코드의 흐름을 명확하게 유지하는 특징까지 살펴볼 수 있다!!


결론적으로 Task를 잘 사용하게 되면 비동기 코드의 복잡성을 크게 줄일 수 있고 더 안정적이고 유지보수하기 쉬운 코드를 작성할 수 있다.

 

이 개념을 대충은 알고 프로젝트에 적용했지만 한번 더 짚고 넘어가면서 더 이해가 되는 것 같다. Task 관련 글은 이번 한번으로 끝내는 것이 아닌, 공식문서를 토대로 더 자세한 기능과 사용, 역할에 대해서 정리할 예정이다!! 오늘은 여기까지 🙂