
본 글은 TaskLocal (애플 공식 문서)를 한국어로 번역하여 옮긴 글입니다.
태스크-로컬의 키 값을 정의하는 래퍼 타입(wrapper type)
iOS 13.0+ | iPadOS 13.0+ | Mac Catalyst 13.0+ | macOS 10.15+ | tvOS 13.0+ | visionOS 1.0+ | watchOS 6.0+
final class TaskLocal<Value> where Value : Sendable
태스크-로컬 값은 작업(Task) 컨텍스트 내에서 바인딩하고 읽을 수 있는 값입니다. 태스크-로컬 값은 작업과 함께 암시적으로 전달(carried)되며, 이 작업이 생성하는 (TaskGroup이나 async-let에서 생성한 작업 등) 모든 하위 작업(child tasks)에서도 접근할 수 있습니다.
태스크-로컬은 반드시 정적(static) 프로퍼티나 전역 프로퍼티로 선언되어야 합니다.
enum Example {
@TaskLocal
static var traceID: TraceID?
}
// Global task local properties are supported since Swift 6.0:
@TaskLocal
var contextualNumber: Int = 12
태스크-로컬 값에 아무런 값을 바인딩하지 않고 읽으면 태스크-로컬의 기본값을 반환합니다. 태스크-로컬을 (TraceID?처럼) 옵셔널 타입으로 선언하는 경우, 기본값은 nil이 됩니다. 하지만 아래와 같이 태스크-로컬을 선언할 때 다른 기본값을 직접 지정할 수 있습니다.
enum Example {
@TaskLocal
static var traceID: TraceID = TraceID.default
}
기본값은 현재 작업이나 상위 작업 컨텍스트에 값이 바인딩되지 않았거나, 호출 스택(call stack) 상에 어떤 비동기 함수도 없는 동기 함수에서 태스크-로컬 값을 읽으려 할 때 반환됩니다.
태스크-로컬 값을 읽는 건 일반적인 정적 프로퍼티를 읽는 것처럼 간단합니다.
guard let traceID = Example.traceID else {
print("no trace id")
return
}
print(traceID)
태스크-로컬 값은 비동기 함수뿐만 아니라 동기 함수에서도 읽을 수 있습니다.
태스크-로컬 값은 직접 설정(set)할 수 없으며, 반드시 $traceID.withValue() { ... } 연산을 통해 바인딩해야 합니다. 이 값은 해당 범위가 유지되는 동안에만 바인딩되며, 이 범위 내에 생성된 모든 하위 작업에서도 사용할 수 있습니다.
Detached Task는 태스크-로컬 값을 상속하지 않습니다. 반면에, Task { ... }로 생성된 작업은 비록 구조화되지 않은 작업(unstructured task)이라 하더라도, 새로운 비동기 작업으로 값을 복사하는 방식으로 태스크-로컬 값을 상속합니다.
작업의 바깥에서 태스크-로컬 값을 바인딩하고 읽을 수 있습니다.
이는 작업 내에서 호출된다고 보장되지 않는 동기 함수에서 특히 유용합니다. 작업의 바깥에서 태스크-로컬 값을 바인딩하는 경우, 런타임은 작업과 동일한 저장 메커니즘을 사용하는 스레드-로컬(thread-local)을 설정합니다. 이는 아래 예제와 같이 특정 호출 컨텍스트를 염려할 필요 없이, 태스크-로컬 값을 안정적으로 바인딩하고 읽을 수 있음을 의미합니다.
func enter() {
Example.$traceID.withValue("1234") {
read() // always "1234", regardless if enter() was called from inside a task or not:
}
}
func read() -> String {
if let value = Self.traceID {
"\(value)"
} else {
"<no value>"
}
}
// 1) Call `enter` from non-Task code
// e.g synchronous main() or non-Task thread (e.g. a plain pthread)
enter()
// 2) Call `enter` from Task
Task {
enter()
}
위에서 언급한 두 경우 모두에서, 태스크-로컬 값의 바인딩과 읽기는 예상대로 동작합니다.
enum Example {
@TaskLocal
static var traceID: TraceID?
}
func read() -> String {
if let value = Self.traceID {
"\(value)"
} else {
"<no value>"
}
}
await Example.$traceID.withValue(1234) { // bind the value
print("traceID: \(Example.traceID)") // traceID: 1234
read() // traceID: 1234
async let id = read() // async let child task, traceID: 1234
await withTaskGroup(of: String.self) { group in
group.addTask { read() } // task group child task, traceID: 1234
return await group.next()!
}
}
Task { // unstructured tasks do inherit task locals by copying
read() // traceID: 1234
}
Task.detached { // detached tasks do not inherit task-local values
read() // traceID: nil
}
⚪️ See Also
TaskLocal-macro