# Swift 서버 요청은 어디서 느려질까, distributed-tracing

- 게시: 2026.07.14
- 원문(HTML): https://trendbrief.news/articles/swift-distributed-tracing%EC%9C%BC%EB%A1%9C-swift-%EC%84%9C%EB%B2%84-%EC%9A%94%EC%B2%AD-%ED%9D%90%EB%A6%84-%EC%B6%94%EC%A0%81%ED%95%98%EA%B8%B0.html
- 발행: [트렌드브리프](https://trendbrief.news/)

> 애플이 공개한 Swift 서버용 분산 추적 API. SwiftPM으로 도입해 withSpan으로 요청 흐름을 스팬으로 묶고, Swift OTel로 OpenTelemetry 백엔드에 연결하는 법을 개발자 눈높이로 정리했다.

swift-distributed-tracing은 애플이 오픈소스로 공개한 Swift 서버용 분산 추적 API다. 요청 하나가 여러 함수와 비동기 작업, 여러 서비스를 거치며 남기는 흔적을 스팬(span, 요청 처리의 한 구간)으로 묶어 어디서 시간이 걸리고 어디서 오류가 났는지 한 줄기로 잇는다. 애플 GitHub 저장소의 릴리스 페이지 기준 2026년 7월 현재 최신 안정 버전은 1.4.1이며, 문맥 전파에 쓰는 ServiceContext 타입은 별도 패키지인 swift-service-context에 의존한다.

이 글은 Vapor나 Hummingbird 같은 Swift 서버를 async/await로 짜는 개발자를 대상으로, SwiftPM 도입부터 withSpan 사용, OpenTelemetry 백엔드 연결까지 실무 순서대로 정리한다. iOS 앱 화면을 계측하는 opentelemetry-swift는 범위 밖이다. 근거는 [애플의 공식 저장소](https://github.com/apple/swift-distributed-tracing) 문서를 기준으로 삼았다.

## 분산 추적이 왜 필요한가

요청 하나가 여러 조각으로 흩어지기 때문이다. 사용자가 주문 버튼을 한 번 누르면 서버 안에서는 인증 확인, 재고 조회, 결제 호출, 알림 발송이 순서대로 또는 동시에 일어난다. 각 단계가 남기는 로그는 시각이 찍힌 점일 뿐이라, 이 점들이 같은 요청에서 나왔는지 이어 붙이기가 어렵다.

분산 추적은 이 여정 전체를 하나의 트레이스(trace)로 보고, 그 안의 개별 구간을 스팬으로 나눈다. 주문 요청이라면 인증 조회 스팬, DB 질의 스팬, 외부 결제 API 스팬이 부모와 자식으로 이어져 하나의 트레이스를 이룬다. 스팬마다 시작·종료 시각과 걸린 시간, 붙여 둔 속성(attribute)이 담기므로, 완성된 트레이스를 펼치면 어느 구간이 느렸는지 눈으로 짚을 수 있다.

단일 프로세스 안에서 특정 시점의 상태만 남기면 된다면 [os_log 같은 구조적 로깅](https://trendbrief.news/articles/os-log-%EC%82%AC%EC%9A%A9%EB%B2%95-%EC%95%A0%ED%94%8C-%EC%95%B1-%EB%A1%9C%EA%B9%85%EC%9D%84-print%EC%97%90%EC%84%9C-%EC%98%AE%EA%B8%B0%EA%B8%B0.html)으로도 충분하다. 하지만 요청이 여러 비동기 경계와 서비스를 넘나드는 순간, 시점 기록만으로는 인과를 잇기 어려워진다. 분산 추적은 그 빈자리를 메운다.

## 표준 API 하나로 묶은 이유

핵심은 swift-distributed-tracing 자체가 추적 데이터를 수집하거나 서버로 보내지 않는다는 점이다. 이 패키지는 스팬을 열고 닫는 방법, 속성을 붙이는 방법 같은 표준 인터페이스만 정의한다. 실제 수집과 전송은 이 인터페이스를 구현한 백엔드가 맡는다.

덕분에 트레이서(tracer, 추적 구현체)를 갈아 끼워도 애플리케이션 코드는 그대로 둔다. 앱을 띄울 때 `InstrumentationSystem.bootstrap(tracer)`로 트레이서를 한 번 등록하면, 이후 코드가 만드는 모든 스팬이 그 트레이서로 흘러간다. 로컬 개발에서는 콘솔 출력 트레이서를, 운영에서는 OpenTelemetry 트레이서를 물리는 전환이 설정 한 줄로 끝난다.

이 표준의 값어치는 이미 계측된 라이브러리에서 드러난다. 애플 저장소 README에 따르면 AsyncHTTPClient 1.29.0 이상, Hummingbird, Vapor, Valkey Swift, 그리고 gRPC Swift 2(grpc-swift-extras 미들웨어 경유)가 이 API를 기본 지원한다. 이 라이브러리들을 거치는 요청은 내가 별도 코드를 넣지 않아도 자동으로 스팬을 남기고, 내가 `withSpan`으로 만든 스팬과 같은 트레이스에 이어 붙는다.

## SwiftPM으로 도입하고 withSpan 쓰기

도입은 두 단계다. 먼저 Package.swift에 의존성과 Tracing 프로덕트를 추가한다.

```
dependencies: [
 .package(url: "https://github.com/apple/swift-distributed-tracing.git", from: "1.0.0"),
],
targets: [
 .target(
  name: "App",
  dependencies: [
   .product(name: "Tracing", package: "swift-distributed-tracing"),
  ]
 ),
]
```

그다음 추적하고 싶은 함수를 withSpan으로 감싼다. withSpan은 클로저 안의 코드를 하나의 스팬으로 묶고, 클로저에 그 스팬 객체를 넘겨 준다.

```
import Tracing

func loadOrder() async throws -> Order {
 try await withSpan("loadOrder") { span in
  span.attributes["db.system"] = "postgres"
  return try await database.fetchOrder()
 }
}
```

async 함수를 이렇게 감싸면 클로저 안에서 시작한 자식 작업도 `ServiceContext.current`를 타고 같은 트레이스에 붙는다. loadOrder 안에서 다시 다른 withSpan 함수를 부르면 부모·자식 스팬 관계가 자동으로 만들어진다. 클로저가 에러를 던지면 그 에러 정보도 스팬에 기록되므로, 실패한 요청만 골라 어디서 깨졌는지 되짚을 수 있다. 이 구조는 [async/await로 콜백을 걷어낸 코드](https://trendbrief.news/articles/%EC%8A%A4%EC%9C%84%ED%94%84%ED%8A%B8-async-await-%EC%BD%9C%EB%B0%B1%EC%97%90%EC%84%9C-%EC%88%9C%EC%B0%A8-%EC%BD%94%EB%93%9C%EB%A1%9C.html)와 특히 잘 맞물린다. 스팬의 경계가 곧 함수 호출의 경계이기 때문이다.

## 수집·시각화 백엔드는 어떻게 붙이나

트레이서로 Swift OTel을 부팅하면 스팬이 OpenTelemetry Collector로 전송된다. [Swift OTel](https://github.com/swift-otel/swift-otel)은 이 추적 API의 대표 백엔드 구현이다.

애플 저장소는 Swift OTel을 호환 백엔드로 안내한다. 이 구현은 스팬을 OpenTelemetry Collector(추적 데이터를 받아 중계하는 수집 서버)로 내보내고, Collector가 다시 Zipkin·Jaeger·AWS X-Ray 같은 시각화 도구로 넘긴다. 앱 코드에서는 앞서 본 `InstrumentationSystem.bootstrap`에 Swift OTel의 트레이서를 넘기기만 하면 되고, 어느 시각화 도구를 쓸지는 Collector 설정에서 정한다.

전체 구성 요소의 역할을 정리하면 아래와 같다.

| 구성 요소 | 역할 |
| --- | --- |
| swift-distributed-tracing | 스팬·트레이서를 다루는 표준 추적 API |
| swift-service-context | 요청 문맥(ServiceContext)을 코드 경계 너머로 전파 |
| Swift OTel | 스팬을 수집기로 보내는 실제 백엔드 구현 |
| AsyncHTTPClient·Vapor·Hummingbird | API로 이미 계측돼 자동으로 스팬을 남기는 라이브러리 |

API와 문맥 전파, 백엔드가 이렇게 분리돼 있어서, 처음에는 콘솔 트레이서로 스팬이 제대로 생기는지 확인하고 나중에 Swift OTel로 바꾸는 점진적 도입이 가능하다.

## 언제 도입을 검토할까

단일 서비스에 요청량도 적다면 로깅만으로 충분한 경우가 많다. 다음 상황이라면 분산 추적의 값이 분명해진다.

- 요청 하나가 여러 서비스나 여러 비동기 단계를 거쳐, 로그만으로는 병목 지점을 짚기 어려울 때

- 이미 Vapor·Hummingbird·AsyncHTTPClient를 쓰고 있어 라이브러리 계측을 그대로 얻을 수 있을 때

- OpenTelemetry Collector나 Jaeger 같은 관측 도구를 이미 운영 중이라 스팬을 흘려보낼 곳이 있을 때

부팅 전 기본 트레이서는 아무 일도 하지 않는 no-op이라 추적 비용이 거의 들지 않는다. 그래서 계측 코드를 먼저 심어 두고 백엔드는 나중에 붙이는 순서로 가도 무리가 없다.

## 관련 글

- [에이전트가 읽는 Meta 디자인 시스템, Astryx](https://trendbrief.news/articles/astryx-meta-%EC%98%A4%ED%94%88%EC%86%8C%EC%8A%A4-%EB%94%94%EC%9E%90%EC%9D%B8-%EC%8B%9C%EC%8A%A4%ED%85%9C%EC%9D%98-%EA%B5%AC%EC%84%B1%EA%B3%BC-%EB%8F%84%EC%9E%85.md)
- [Vapor로 Swift 서버 만들기, 라우팅부터 DB까지](https://trendbrief.news/articles/vapor%EB%A1%9C-swift-%EC%84%9C%EB%B2%84-%EA%B0%9C%EB%B0%9C-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0.md)
- [iOS 27 MetricKit, Swift 새 API와 상태별 지표](https://trendbrief.news/articles/wwdc26-metrickit-ios-27%EC%9D%98-swift-%EC%83%88-api%EC%99%80-%EC%83%81%ED%83%9C%EB%B3%84-%EC%B8%A1%EC%A0%95.md)
- [FastMCP로 파이썬 MCP 서버 만들기, 클로드에 연결](https://trendbrief.news/articles/mcp-%EC%84%9C%EB%B2%84-%EB%A7%8C%EB%93%A4%EA%B8%B0-%ED%8C%8C%EC%9D%B4%EC%8D%AC%EC%9C%BC%EB%A1%9C-%EC%B2%AB-%EB%8F%84%EA%B5%AC-%EC%97%B0%EA%B2%B0%ED%95%98%EA%B8%B0.md)
- [ROAS란? 계산 공식과 데이터를 어디서 가져오나](https://trendbrief.news/articles/roas-%EC%B8%A1%EC%A0%95-%EA%B0%9C%EB%B0%9C%EC%9E%90%EA%B0%80-%EC%A7%9A%EC%96%B4%EC%95%BC-%ED%95%A0-%EB%8D%B0%EC%9D%B4%ED%84%B0-%ED%9D%90%EB%A6%84.md)