구조적 동시성 환경에서 특정 Task 내에서만 데이터를 공유하기 위해 사용하는 프로퍼티 래퍼
static 한정: static 프로퍼티에만 적용 가능
Task 범위 한정: 값이 설정된 특정 Task와 그 하위 Task에서만 접근 가능
상속: 부모 Task에서 생성된 Task는 부모의 Task-local 값을 그대로 상속
읽기 전용: Task 내부에서는 값을 직접 수정할 수 없으며, 반드시 withValue(_:operation:) 메서드를 통해서만 새로운 값으로 바인딩 가능
Thread-Safety: 각 Task에 바인딩되므로 멀티스레드 환경에서 데이터 경합(Data Race) 문제로부터 안전
import Foundation
enum Storage {
@TaskLocal static var id: String = "Default"
}
func printID(label: String) {
// 같은 전역 변수를 참조하지만, 어떤 Task에서 호출하느냐에 따라 값이 다름
print("\(label): \(Storage.id)")
}
Task {
await Storage.$id.withValue("Task-A") {
printID(label: "A-1") // Task-A
try? await Task.sleep(nanoseconds: 1_000_000_000)
printID(label: "A-2") // Task-A (시간이 흘러도 유지)
}
}
Task {
await Storage.$id.withValue("Task-B") {
printID(label: "B-1") // Task-B
printID(label: "B-2") // Task-B
}
}
// 결과 출력 (순서는 바뀔 수 있음)
// A-1: Task-A
// B-1: Task-B
// B-2: Task-B
// A-2: Task-A
Task마다 다른 Storage.id 값을 가짐.protocol LogContext {
static var traceID: String { get }
}
enum Logger: LogContext {
@TaskLocal static var _traceID: String = "UNKNOWN"
static var traceID: String { _traceID }
}
struct PaymentService {
func process() async {
print("[Log] 결제 시작 (ID: \(Logger.traceID))") // "REQ-123" 출력
await verify()
}
private func verify() async {
print("[Log] 검증 중 (ID: \(Logger.traceID))") // 자동으로 상속
}
}
각 함수의 실행 순서를 추적할 때 시작 지점을 찾기 수월해짐.
부모 Task로부터 이어지는 동일한 @TaskLocal 값으로 추적 간편
protocol APIClient {
func fetchData() async -> String
}
enum AppEnvironment {
@TaskLocal static var client: APIClient = RealClient()
}
// 테스트 코드
func test_logic() async {
let mock = MockClient()
// 이 클로저 내부에서만 'client'는 mock
await AppEnvironment.$client.withValue(mock) {
let result = await someBusinessLogic() // 내부에서 AppEnvironment.client 사용
assert(result == "Mock Data")
}
}
Mock으로 교체하여 검증 가능접근은 전역에서 가능.
실제 값은 각 Task의 메모리에 따로 저장
Task는 객체로 내부에 local_storage라는 숨겨진 필드 존재.
key-value 형태로 key는 프로퍼티의 주소, value는 주소와 연결된 값. 이 때, key는 진짜 구별하기 위한 key임. 주소를 찾아가려고 하는게 아님.
이때 value는 Linked List 형태로 Task가 withValue 메서드를 실행할 때마다 노드를 추가하여 갱신
withValue 메서드를 탈출하면 해당 노드는 pop, 이전 값으로 업데이트
⚠️ 부모와 자식 Task의 저장소는 개별
상속 시 자식 Task는 부모 저장소의 최상단 노드를 가리킴.
참고
https://github.com/swiftlang/swift-evolution/blob/main/proposals/0311-task-locals.md
https://developer.apple.com/documentation/swift/tasklocal
https://developer.apple.com/documentation/swift/task
https://developer.apple.com/videos/play/wwdc2021/10134/
https://github.com/swiftlang/swift/blob/main/stdlib/public/Concurrency/TaskLocal.swift