각 자식 작업에서 타입이 지정된 Result를 반환하고 수집 후 언래핑하여 동시 Swift 계산 전반에서 타입이 지정된 오류를 보존하세요. 이 Mac 예제는 입력 순서 유지, 명시적 실패, 모두-성공-또는-오류 배치 계약을 지키면서 합성 직사각형 픽셀 수를 계산합니다.
작업 그룹 경계에서 타입이 지정된 결과를 사용하는 이유?
여러 직사각형의 픽셀 수를 계산하는 데스크톱 그래픽 미리보기를 만들고 있다고 가정해 보세요. 각 계산은 잘못된 차원을 거부하거나 정수 오버플로를 보고할 수 있습니다. 호출자는 완전한 순서 배열이나 특정 계산 오류가 필요하므로, 지원되는 모든 실패를 명시적으로 처리할 수 있습니다.
Swift 6.0에서 타입이 지정된 throws가 도입되었습니다. throws(CalculationError)과 같은 선언은 함수가 던질 수 있는 값을 해당 오류 타입으로 제한합니다. 이는 구현과 호출자가 함께 진화하는 작고 닫힌 계산 계약에 유용합니다. SE-0413: Typed throws를 참조하세요.
동시성 API 경계가 중요합니다. Apple의 문서화된 withThrowingTaskGroup 시그니처는 본문에 ThrowingTaskGroup<ChildTaskResult, any Error>를 제공합니다. 제네릭처럼 보이는 이 그룹 타입은 해당 팩토리를 통해 구체적인 도메인 오류 매개변수를 제공하지 않습니다. 따라서 계산에만 타입이 지정된 throws를 추가해도 이 그룹 API 전체에 구체적인 던져지는 타입이 확립되지 않습니다. withThrowingTaskGroup을 참조하세요.
지원되는 방법은 withTaskGroup을 사용하고 각 자식이 Result<Int, CalculationError>를 반환하게 하는 것입니다. 작업은 성공 또는 실패를 나타내는 값과 함께 정상적으로 완료됩니다. 해당 값들을 수집한 후, 배치 함수는 get()을 호출하며, 그 문서화된 시그니처는 throws(Failure)입니다. 이는 런타임 캐스트 없이 타입이 지정된 throws를 복원합니다. Apple의 Result 문서와 get() 문서를 참조하세요.
동시성 코드 작성 전에 배치 계약 정의하기
이 예제에서 각 입력은 픽셀 단위로 측정된 정수 너비와 높이를 포함합니다. 계산은 이들의 곱입니다. 모든 것이 합성이며 메모리에만 유지됩니다. 프로그램은 이미지를 렌더링하지 않으며 애플리케이션 데이터를 로드하거나 저장하지 않습니다.
다음 정책들은 이 예제를 위한 설계 선택입니다. 이들은 반환된 값과 선택된 오류를 모두 예측 가능하게 만듭니다:
| 상황 | 배치 동작 |
|---|---|
| 모든 직사각형이 유효함 | 입력 순서대로 픽셀 수 반환 |
| 너비 또는 높이가 0 이하 | 해당 직사각형을 유효하지 않음으로 보고 |
곱이 Int에 맞지 않음 |
해당 직사각형에 대한 오버플로 보고 |
| 여러 직사각형이 실패 | 가장 낮은 입력 인덱스를 가진 실패 보고 |
| 배치 체크포인트에서 취소 관찰됨 | .cancelled 보고 |
| 입력 배열이 비어 있음 | 취소가 관찰되지 않는 한 빈 배열 반환 |
가장 낮은 입력 인덱스를 선택하려면 계산 실패를 선택하기 전에 모든 자식 결과를 수집해야 합니다. Swift는 작업 그룹 스케줄링 순서를 보장하지 않으며, 완료 순서는 제출 순서와 다를 수 있습니다. 각 결과에 인덱스를 첨부하면 수집기가 의도된 순서를 복원할 수 있습니다. TaskGroup을 참조하세요.
이 정책은 반복 가능한 피드백이 중요한 작은 미리보기 배치에 적합합니다. 모든 계산을 의도적으로 기다립니다. 실행 중에 관찰된 첫 번째 실패를 보고하는 설계는 다른 트레이드오프를 만들며, 실행마다 다른 직사각형을 선택할 수 있습니다.
Mac에서 완전한 로컬 예제 빌드하기
설치된 Swift 6 이상 툴체인을 사용하세요. 터미널에서 swift --version으로 확인하세요. Swift.org는 시작하기에서 이 확인을 문서화하고 Swift 설치 — macOS에서 Mac 설치 지침을 제공합니다.
설치 페이지에 문서화된 워크플로를 사용하여 작은 실행 가능 패키지를 생성하세요:
mkdir TypedCalculations
cd TypedCalculations
swift package init --name TypedCalculations --type executable
Sources 아래의 생성된 Swift 진입 파일을 다음 세 코드 블록으로 순서대로 한 파일에 교체하세요. 실행 가능 진입점은 하나만 유지하세요. 이 예제는 패키지의 일반적인 기본 액터 격리를 사용합니다. 기본 메인 액터 격리로 구성된 프로젝트는 계산 타입과 함수를 해당 격리 밖에 배치해야 합니다.
입력과 구체적 실패 정의하기
오류 열거형은 구조화된 정보를 보존합니다. 케이스는 문제를 식별하고, 연관된 인덱스는 입력을 식별합니다. Foundation의 LocalizedError 프로토콜은 프레젠테이션을 위한 선택적 errorDescription를 제공합니다. 아래의 영어 메시지는 샘플 문자열입니다. 프로토콜을 준수한다고 자동으로 번역되지는 않습니다. LocalizedError를 참조하세요.
import Foundation
struct Rectangle: Sendable {
let width: Int
let height: Int
}
enum CalculationError: Error, Sendable, LocalizedError {
case invalidDimensions(index: Int)
case overflow(index: Int)
case cancelled
var errorDescription: String? {
switch self {
case .invalidDimensions(let index):
return "Rectangle \(index + 1) needs positive dimensions."
case .overflow(let index):
return "Rectangle \(index + 1) has too many pixels for Int."
case .cancelled:
return "Calculation cancelled."
}
}
}
struct IndexedOutcome: Sendable {
let index: Int
let result: Result<Int, CalculationError>
}
계산, 수집, 타입이 지정된 throws 복원하기
산술은 multipliedReportingOverflow(by:)를 사용합니다. Apple은 그 partialValue이 overflow가 false일 때만 전체 곱이며, 그렇지 않으면 잘린 곱을 포함한다고 문서화합니다. 아래의 가드는 잘린 값이 성공적인 픽셀 수가 되는 것을 방지합니다. multipliedReportingOverflow(by:)를 참조하세요.
func pixelCount(
for rectangle: Rectangle,
index: Int
) throws(CalculationError) -> Int {
if Task.isCancelled {
throw .cancelled
}
guard rectangle.width > 0, rectangle.height > 0 else {
throw .invalidDimensions(index: index)
}
let product = rectangle.width.multipliedReportingOverflow(
by: rectangle.height
)
guard !product.overflow else {
throw .overflow(index: index)
}
return product.partialValue
}
func calculateBatch(
_ rectangles: [Rectangle]
) async throws(CalculationError) -> [Int] {
if Task.isCancelled {
throw .cancelled
}
let outcomes: [IndexedOutcome] = await withTaskGroup(
of: IndexedOutcome.self,
returning: [IndexedOutcome].self
) { group in
for (index, rectangle) in rectangles.enumerated() {
group.addTask {
let result = Result<Int, CalculationError> {
() throws(CalculationError) -> Int in
try pixelCount(for: rectangle, index: index)
}
return IndexedOutcome(index: index, result: result)
}
}
var collected: [IndexedOutcome] = []
for await outcome in group {
collected.append(outcome)
}
return collected.sorted { $0.index < $1.index }
}
if Task.isCancelled {
throw .cancelled
}
var counts: [Int] = []
counts.reserveCapacity(outcomes.count)
for outcome in outcomes {
counts.append(try outcome.result.get())
}
return counts
}
Result 클로저의 명시적 throws(CalculationError) 어노테이션은 클로저 추론에 의존하는 대신 오류 계약을 가시적으로 유지합니다. SE-0413은 원래 계획된 완전한 타입 throws 추론 변경이 Swift 6.0에 구현되지 않았음을 언급합니다. Typed throws의 구현 노트를 참조하세요.
부모 수집기만 collected에 추가합니다. 각 자식은 직사각형과 인덱스를 받고 자체 Sendable 결과를 반환합니다. 이는 자식 결과 값이 Sendable이어야 한다는 작업 그룹 요구사항과 일치하며, 비던지기 그룹 본문은 for await로 수집합니다. withTaskGroup을 참조하세요.
실행 가능 진입점 추가하기
이 마지막 블록은 모든 샘플 입력을 제공하고 배치 함수를 호출합니다. catch 내부의 명시적 할당은 의도된 API 계약을 컴파일 타임에 검사합니다: 잡힌 값은 반드시 CalculationError여야 합니다.
@main
struct TypedCalculations {
static func main() async {
let examples: [(name: String, rectangles: [Rectangle])] = [
(
name: "Valid",
rectangles: [
Rectangle(width: 640, height: 480),
Rectangle(width: 320, height: 200)
]
),
(
name: "Multiple failures",
rectangles: [
Rectangle(width: 0, height: 20),
Rectangle(width: Int.max, height: 2)
]
),
(
name: "Overflow",
rectangles: [
Rectangle(width: Int.max, height: 2)
]
),
(name: "Empty", rectangles: [])
]
for example in examples {
do {
let counts = try await calculateBatch(example.rectangles)
print("\(example.name): \(counts)")
} catch {
let failure: CalculationError = error
print("\(example.name): \(failure.errorDescription ?? "Calculation failed.")")
}
}
}
}
패키지 디렉터리에서 실행 파일을 실행하세요:
swift run TypedCalculations
빌드 메시지를 제외한 예상 애플리케이션 출력은 아래와 같습니다. 연구 환경에서 Swift 컴파일러를 사용할 수 없었으므로 이 프로그램은 검색된 API 문서에 대해 검사되었지만 그곳에서 컴파일되거나 실행되지는 않았습니다.
Valid: [307200, 64000]
Multiple failures: Rectangle 1 needs positive dimensions.
Overflow: Rectangle 1 has too many pixels for Int.
Empty: []
유효한 개수는 예시 입력에서 직접 나옵니다: 640 × 480 = 307,200이고 320 × 200 = 64,000입니다. 다중 실패 예제에서 첫 번째 직사각형이 이깁니다. 결과가 입력 순서대로 검사되기 때문입니다. 두 번째 자식이 더 일찍 완료되더라도 말입니다.
취소와 부분 결과는 어떻게 동작하는가?
Swift에서 취소는 협력적입니다. 작업은 취소를 관찰하고 응답해야 합니다. 취소를 요청한다고 해서 계산이 자동으로 중단되지는 않습니다. 구조화된 작업 그룹도 자식이 완료될 때까지 기다렸다가 반환합니다. Swift 언어 가이드의 동시성 장과 Apple의 withTaskGroup 문서를 참조하세요.
여기서는 제출 전, 각 계산 내부, 수집 후에 Task.isCancelled를 확인합니다. 체크포인트에서 취소가 관찰되면 코드는 기존 .cancelled 케이스를 사용하여 이를 표현합니다. 불리언 값을 읽음으로써 이 닫힌 계약에 또 다른 던져지는 오류 타입을 도입하지 않습니다. Apple은 Task.isCancelled에서 이 프로퍼티를 문서화합니다.
수집 후 확인은 관찰된 배치 취소에 직사각형 실패보다 우선권을 줍니다. 이 체크포인트들은 원자적 보장이 아닙니다. 최종 체크 이후에 취소가 요청되면 반환된 결과에 영향을 주지 않을 수 있습니다. 반복 작업이 포함된 더 긴 계산의 경우, 해당 작업 내에 체크를 추가하여 취소를 더 빨리 관찰할 수 있도록 하세요.
실패한 자식은 이 구현에서 취소를 유발하지 않습니다. 정상적인 Result.failure 값을 반환하고 수집기는 계속합니다. 모든 자식이 성공적으로 완료되면 호출자는 모든 개수를 받습니다. 언래핑에서 실패를 만나면 함수가 던지고 로컬의 부분적으로 조립된 counts 배열은 절대 반환되지 않습니다.
데스크톱 인터페이스가 대신 모든 직사각형에 대한 성공 또는 실패 상태가 필요하다면, 공개 배치 계약을 순서대로 결과를 반환하도록 변경하세요. 각 실패를 해당 인덱스에 연결하세요. 이는 여기 구현된 모두-성공-또는-오류 배열과는 다른 리더 대상 동작입니다.
실패를 숨기지 않고 손실된 오류 타입 진단하기
적용된 코드가 컴파일되지 않으면, 다음 순서로 각 경계에서 타입을 추적하세요:
- 계산이
throws(CalculationError)을 선언하는지 확인하세요. - 자식 값이
Result<Int, CalculationError>인지, 실패가any Error인 결과인지 확인하세요. - 추론이 오류 타입을 넓히는 경우 클로저의 명시적 타입 throws 어노테이션을 유지하세요.
- 이 결과 값들을 전송하기 위해 비던지기
withTaskGroup을 사용하세요. - 타입이 지정된 배치 함수 내부에서
get()로 언래핑하세요. - 새로 추가된 던지기 연산이 다른 오류 타입을 도입하는지 확인하세요.
강제 캐스트는 이 계약을 증명할 수 없습니다. 마찬가지로 실패한 결과를 버리거나 0으로 대체하면 계산의 의미가 바뀝니다. 선택한 배치 정책이 어떻게 보고할지 결정하는 지점까지 실패를 값으로 보존하세요.
이 작은 곱셈들에 대해 동시성은 아키텍처적 시연일 뿐입니다. 성능 개선을 주장하지 않습니다. 유용한 결과는 정확한 계약입니다: 성공적인 개수는 입력 순서를 유지하고, 다중 실패에는 결정적 선택 규칙이 있으며, 호출자는 직접 처리할 수 있는 구체적인 오류 열거형을 받습니다.
출처
Typed throws — SE-0413 — Swift Project.
withThrowingTaskGroup(of:returning:isolation:body:) — Apple.
Result — Apple.
get() — Apple.
TaskGroup — Apple.
Getting Started — Swift.org.
Install Swift — macOS — Swift.org.
LocalizedError — Apple.
multipliedReportingOverflow(by:) — Apple.
Concurrency — The Swift Programming Language — Swift Project.
isCancelled — Apple.