Swift Concurrency: @TaskLocal

틀틀보·2026년 2월 15일

Swift Concurency

목록 보기
10/10

@TaskLocal

구조적 동시성 환경에서 특정 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 값을 가짐.

어디다 써야 할까?

1.Logger

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 값으로 추적 간편

2. 의존성 주입

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 형태로 TaskwithValue 메서드를 실행할 때마다 노드를 추가하여 갱신

  • 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

profile
안녕하세요! iOS 개발자입니다!

0개의 댓글