---
id: observability-correlation-ids
title: Correlation ID — 분산 추적의 핵심
category: Coding
status: draft
source_trust_level: B
verification_status: conceptual
created_at: 2026-05-09
updated_at: 2026-05-09
tags: [observability, correlation-id, tracing, vibe-coding]
tech_stack: { language: "Any backend", applicable_to: ["Microservices"] }
applied_in: []
aliases: [request id, trace id, X-Request-Id, distributed tracing]
---

# Correlation ID

> 한 사용자 요청이 여러 서비스를 거친다. **하나의 ID 가 끝까지 따라다녀야** 로그 / 메트릭 / trace 를 한 그래프로 묶을 수 있다. 외부 호출 / 큐 / DB 쿼리 모두 ID 전파.

## 📖 핵심 개념
- Request ID: 요청 단위 고유 ID. 입구에서 생성 또는 검증.
- Trace ID + Span ID: OpenTelemetry 표준. trace 한 그래프 안의 span.
- 모든 로그 / 외부 호출 헤더 / 큐 메시지 / DB 쿼리에 전파.

## 💻 코드 패턴

### Express middleware — 입구
```ts
import { randomUUID } from 'node:crypto';

app.use((req, res, next) => {
  const incoming = req.header('x-request-id');
  const requestId = (incoming && /^[\w-]{8,64}$/.test(incoming)) ? incoming : randomUUID();
  req.requestId = requestId;
  res.setHeader('x-request-id', requestId);
  next();
});
```

### AsyncLocalStorage 로 자동 전파
```ts
import { AsyncLocalStorage } from 'node:async_hooks';
export const ctx = new AsyncLocalStorage<{ requestId: string }>();

app.use((req, res, next) => ctx.run({ requestId: req.requestId }, next));

// 외부 호출 wrapper
export async function fetchEx(input: RequestInfo, init: RequestInit = {}) {
  const requestId = ctx.getStore()?.requestId;
  return fetch(input, {
    ...init,
    headers: { ...init.headers, 'x-request-id': requestId ?? '' },
  });
}
```

### Logger 자동 포함
```ts
export function log() {
  return logger.child(ctx.getStore() ?? {});
}
log().info({ orderId }, 'created'); // 자동 requestId
```

### 큐 메시지에 포함
```ts
await queue.add('email', {
  payload: { userId },
  meta: { requestId: ctx.getStore()?.requestId },
});

// Worker
worker.process(async (job) => {
  ctx.run({ requestId: job.data.meta.requestId }, async () => {
    // 모든 후속 호출이 같은 ID 전파
    await sendEmail(job.data.payload);
  });
});
```

### W3C Trace Context (OpenTelemetry)
```
traceparent: 00-{trace-id}-{span-id}-{flags}
tracestate: vendor=opaque
```
trace-id 가 전체 trace, span-id 는 한 작업 단위. OpenTelemetry SDK 가 자동 propagate.

## 🤔 의사결정 기준
| 환경 | 권장 |
|---|---|
| 단일 서비스 + 외부 API | x-request-id 만 충분 |
| 마이크로서비스 다수 | OpenTelemetry trace context |
| async 작업 (큐) | 메시지 메타에 requestId/traceparent 포함 |
| 외부 SDK 호출 (Stripe, AWS) | 각 SDK 의 idempotency-key + 우리 ID 별도 매핑 |
| 사용자에게 노출 (지원 요청 시) | 응답 헤더 + 에러 페이지에 request id |

## ❌ 안티패턴
- **각 서비스마다 새 ID 생성**: 끝까지 추적 불가. 한 ID 가 흐른다.
- **로그에 ID 포함 안 함**: ID 있어도 쓸모없음. logger.child 자동.
- **들어온 ID 검증 없이 신뢰**: 클라이언트가 임의 값 보내면 로그 오염. 형식 검증 + 길이 제한.
- **AsyncLocalStorage 안 쓰고 함수 시그니처에 requestId 매번 전달**: 실수 누락. ALS 가 표준.
- **외부 호출 헤더 누락**: 외부 서비스 로그에서 추적 불가.
- **trace-id 와 request-id 따로**: 매핑 추가 필요. 표준 trace context 우선.

## 🤖 LLM 활용 힌트
- 입구 middleware + AsyncLocalStorage + logger.child + fetch wrapper 4종 세트.
- OpenTelemetry 도입 권장 — auto-instrumentation 으로 대부분 자동.

## 🔗 관련 문서
- [[Observability_Structured_Logging]]
- [[Observability_OpenTelemetry]]
