트렌드브리프

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

게시 · 약 6분 분량

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

목차
  1. 분산 추적이 왜 필요한가
  2. 표준 API 하나로 묶은 이유
  3. SwiftPM으로 도입하고 withSpan 쓰기
  4. 수집·시각화 백엔드는 어떻게 붙이나
  5. 언제 도입을 검토할까

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는 범위 밖이다. 근거는 애플의 공식 저장소 문서를 기준으로 삼았다.

분산 추적이 왜 필요한가

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

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

단일 프로세스 안에서 특정 시점의 상태만 남기면 된다면 os_log 같은 구조적 로깅으로도 충분하다. 하지만 요청이 여러 비동기 경계와 서비스를 넘나드는 순간, 시점 기록만으로는 인과를 잇기 어려워진다. 분산 추적은 그 빈자리를 메운다.

표준 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로 콜백을 걷어낸 코드와 특히 잘 맞물린다. 스팬의 경계가 곧 함수 호출의 경계이기 때문이다.

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

트레이서로 Swift OTel을 부팅하면 스팬이 OpenTelemetry Collector로 전송된다. 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로 바꾸는 점진적 도입이 가능하다.

언제 도입을 검토할까

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

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

더 많은 글 보기 RSS 구독