[Swift Concurrency] 구조적 동시성(Structured Concurrency)

정한별·2026년 1월 4일

Swift Concurrency

목록 보기
4/5

구조적 동시성이란?

구조적 동시성은 비동기 작업들이 명확한 부모-자식 관계와 생명주기를 가지도록 하는 프로그래밍 패러다임을 말한다.
"모든 비동기 작업은 정의된 범위(scope) 내에서 시작되고 종료되어야 한다"
마치 중괄호 {} 안에서 변수의 생명주기가 관리되는 것처럼, 비동기 작업도 구조적으로 관리된다.

장점들은 다음과 같다.

  • 작업 관계 명확화: 작업들 사이의 의존성과 실행 순서가 명확해지고, 부수 작업 관리가 단순해진다.

쉽게말해 주방장이 파스타 주문이라고 말하면 보조 요리사들이 각각 재료를 준비하고, 면을 삶고, 소스를 만들기를 함께 진행한다.
이후 모든 과정이 끝나면 주방장은 기다렸다 플레이팅을 한다.

  • 표준화된 관리: 하위 작업이 생성/관리 방식이 표준화되어 관리됨

병원진료처럼 누구나 접수를하고 진료를 받고 처방을 받고 약을 받는것처럼 누가 봐도 같은 방식으로 이해하고 사용할수 있는 표준이 있다는 것이다.

  • 가독성 향상: 명확한 코드의 가독성과 유지 보수가 향상됨

기존 병렬처리의 낮은 가독성과 달리 읽기쉬운 코드로 구현이 가능하다.

  • 취소 전파 자동화: 부모 작업 취소 시 모든 자식 작업들도 자동으로 취소됨
  • 에러 전파 자동화: 자식 작업에서 에러 발생 시 부모로 자동 전파되고, 다른 자식 작업들도 자동 취소됨
func fetch(num: Int) async throws -> UIImage? {
    try await Task.sleep(nanoseconds: 1_000_000_000)
    print("\(num) 함수")
    return UIImage(named: "")
}

아래와 같은코드는 병렬 실행의 방법이 아니다 실제로 모든 print가 출력되는데 4초가 걸리는것을 볼 수 있다.

Task {
    let data1 = try await fetch(num: 1) // 1초 대기
    let data2 = try await fetch(num: 2) // 위에가 완료시 또 1초대기
    let data3 = try await fetch(num: 3) // 반복...
    let data4 = try await fetch(num: 4)
}

병렬 처리 방법 비교

1. async let - 간결함이 최고

첫번째 병렬처리 방법은 async let 이다.

장점

  • 가장 간결하고 읽기 쉬운 문법이다.
  • 구조적 동시성이다.
  • 순서 보장 가능

단점

  • 작업 개수가 컴파일 타임에 고정되어야한다.
  • 동적으로 작업을 추가하기 어렵다.

병렬처리의 갯수가 많지않거나ㅡ 작업개수를 미리 알고있거나, 코드 가독성이 중요할때 사용하는것이 좋다.

Task {
    async let data1 = fetch(num: 1)
    async let data2 = fetch(num: 2)
    async let data3 = fetch(num: 3)
    async let data4 = fetch(num: 4)
    let datas = try await [data1, data2, data3, data4]
}

2. TaskGroup - 동적인 작업 처리

두번째 방법으로는 TaskGroup 이다.

장점

  • 동적으로 작업 개수를 조절할 수 있다.
  • 구조적 동시성이다.
  • 기본적으로 순서 보장이 되지않는다.

단점

  • 문법이 상대적으로 복잡하다.

배열이나 반복문같이 작업개수가 런타임에 결정될때, 많은 수의 작업을 병렬로 처리할 때 사용하는것이 좋다.

Task {
    let datas = try await withThrowingTaskGroup(of: UIImage?.self) { group in
        for i in 1...4 {
            group.addTask {
                try await fetch(num: i)
            }
        }
        var results: [UIImage?] = []
        for try await data in group {
            results.append(data) // 완료된 순서대로 수집 (순서 보장 X)
        }
        return results
    }
}

3. 독립 Task - 자유도가 높지만 위험함

세번째 방법으로는 Task를 각자 생성하는것이다. 하지만 이건 병렬 처리는 맞지만 구조적 동시성은 아니다

장점

  • 각 Task를 독립적으로 관리가 가능해 가장 유연하다.

단점

  • 구조적 동시성이 아니다.
  • 작업이 생명주기와 분리되어 예측 불가능한 실행이 될 수 있다.
  • 취소 관리가 어렵다.

부모와 생명주기를 끊고 싶을때와 같은 정말 특수한경우일때만 사용한다, 결과를 기다리지않고 성공실패에 관심없는 작업 등등..

Task {
    let task1 = Task { try await fetch(num: 1) }
    let task2 = Task { try await fetch(num: 2) }
    let task3 = Task { try await fetch(num: 3) }
    let task4 = Task { try await fetch(num: 4) }
    
    let datas = try await [task1.value, task2.value, task3.value, task4.value]
}
profile
iOS Developer

0개의 댓글