Correlation ID μ ν
π κ°μβ
μ΄ λ¬Έμλ HTTP μμ²λΆν° λΉμ¦λμ€ λ‘μ§μ κ±°μ³ μΈλΆ μμ€ν
(GCP Pub/Sub)κΉμ§ x-correlation-idλ₯Ό μΌκ΄λκ² μ ννλ ꡬν λ΄μ©μ μ€λͺ
ν©λλ€.
λͺ©νβ
- HTTP μμ²μμ μμλ correlation-idλ₯Ό μ ν리μΌμ΄μ μ 체 μλͺ μ£ΌκΈ° λμ μΆμ
- λͺ μμ μΈ νλΌλ―Έν° μ λ¬ μμ΄ λΉλκΈ° νΈμΆ μ²΄μΈ μ 체μ 컨ν μ€νΈ μ μ§
- λΆμ° μμ€ν κ° μμ² μΆμ λ° λλ²κΉ μ§μ
- λ‘κ·Έ μ§κ³ λ° μ±λ₯ λͺ¨λν°λ§ ν₯μ
λ²μβ
HTTP μμ²
β HttpRequestLoggingInterceptor (correlation-id μΆμΆ)
β λΉμ¦λμ€ λ‘μ§ (Service/Handler)
β CrossEventPublisher.publishXXX()
β EventEmitter2.emit('event', standardEvent)
β EventBridgeService @OnEvent('event')
β GCP Pub/Sub.publish()
β λ€λ₯Έ λλ©μΈ μμ
π― νμ¬ λ¬Έμ μ β
Before (ꡬν μ )β
1. HTTP Request
ββ x-correlation-id: abc-123 β
2. HttpRequestLoggingInterceptor
ββ correlationId μΆμΆ λ° λ‘κΉ
β
ββ μλ΅ ν€λ μ€μ β
ββ νμ§λ§ 컨ν
μ€νΈ μ μ₯ μμ β
3. Controller β Service β EventPublisher
ββ correlationIdκ° μ λ¬λμ§ μμ β
4. StandardEvent λ°ν
ββ correlationId λλ½ β
5. GCP Pub/Sub
ββ μΆμ μ 보 μμ β
ββ λ€μ λλ©μΈμμ μλ³Έ μμ² μΆμ λΆκ° β
μν₯β
- λΆμ° μμ€ν μμ μμ² μΆμ λΆκ°
- ν¬λ‘μ€ λλ©μΈ μ΄λ²€νΈμ μμΈ νμ μ΄λ €μ
- λλ²κΉ μ λ‘κ·Έ μ°κ΄μ± νμ μ΄λ €μ
- μ±λ₯ λ³λͺ© μ§μ λΆμ μ΄λ €μ
ποΈ μ€κ³ κ²°μ μ¬νβ
1. AsyncLocalStorage κΈ°λ° CLS (Continuation Local Storage) ν¨ν΄ μ±νβ
μ ν μ΄μ :
- β λͺ μμ νλΌλ―Έν° μ λ¬ μμ΄ λΉλκΈ° νΈμΆ μ²΄μΈ μ 체μ 컨ν μ€νΈ μ μ§
- β NestJS DIμ μμ°μ€λ½κ² ν΅ν©
- β ν μ€νΈνκΈ° μ¬μ
- β
Node.js 14+ 곡μ μ§μ (
async_hooksλͺ¨λ) - β κ²μ¦λ ν¨ν΄ (Express, NestJS REQUEST scope λ΄λΆμμ μ¬μ©)
λμ λΉκ΅:
| λ°©λ² | μ₯μ | λ¨μ | μ±ν μ¬λΆ |
|---|---|---|---|
| AsyncLocalStorage | μλ 컨ν μ€νΈ μ ν, κΉλν μ½λ | μ½κ°μ μ±λ₯ μ€λ²ν€λ (~0.1ms) | β μ±ν |
| λͺ μμ νλΌλ―Έν° μ λ¬ | λͺ νν μμ‘΄μ± | λͺ¨λ ν¨μ μκ·Έλμ² μμ νμ | β |
| ThreadLocal (Java μ€νμΌ) | μ΅μν ν¨ν΄ | Node.jsλ μ±κΈ μ€λ λ, μ ν©νμ§ μμ | β |
| cls-hooked | λ κ±°μ μ루μ | μ μ§λ³΄μ μ€λ¨, λ²κ·Έ λ§μ | β |
2. StandardEvent ꡬ쑰 νμ₯β
export interface StandardEvent<T = any> {
eventId: string;
type: string;
timestamp: string;
source: string;
payload: T;
version: string;
correlationId?: string; // π μΆκ°
}
μ€κ³ μμΉ:
correlationIdλ optional νλ β νμ νΈνμ± μ μ§- μ΅μμ λ 벨 νλ β νμ€νλ μμΉ
- νμ μμ μ± μ μ§
3. BaseCrossEventPublisher λμ β
- λͺ¨λ CrossEventPublisherμ κ³΅ν΅ λ‘μ§ μΆμΆ
- μ€λ³΅ μ½λ μ κ±°
- μΌκ΄λ correlation-id μ²λ¦¬
π¬ AsyncLocalStorage λμ μ리β
ν΅μ¬ κ°λ β
AsyncLocalStorageλ "μ μ μ μ₯μ"κ° μλλΌ **"λΉλκΈ° μ€ν 컨ν μ€νΈλ³ μ μ₯μ"**μ λλ€.
μ€ν΄ vs μ€μ β
β μλͺ»λ μ΄ν΄: "μ μ μ μ₯μμ μ¬λ¬ μμ²μ λ°μ΄ν°κ° μμΈλ€"β
// μ΄λ κ² λμνλ κ²μ΄ μλλλ€!
globalStorage = {
'request-1': { correlationId: 'abc-123' },
'request-2': { correlationId: 'def-456' },
'request-3': { correlationId: 'ghi-789' },
};
// β μ΄λ€ κ±Έ κΊΌλ΄μΌ νμ§? (νΌμ λ°μ) β
β μ€μ λμ: "κ° λΉλκΈ° μ€ν 컨ν μ€νΈκ° λ 립μ μΈ μ μ₯μλ₯Ό κ°μ§"β
// μμ² 1μ μ€ν 컨ν
μ€νΈ
[Request 1 Context]
ββ AsyncLocalStorage Store: { correlationId: 'abc-123' }
ββ Controller
ββ Service
ββ EventPublisher
ββ getCorrelationId() β 'abc-123' β
// μμ² 2μ μ€ν 컨ν
μ€νΈ (μμ ν λΆλ¦¬λ¨)
[Request 2 Context]
ββ AsyncLocalStorage Store: { correlationId: 'def-456' }
ββ Controller
ββ Service
ββ EventPublisher
ββ getCorrelationId() β 'def-456' β
λμ μ¦λͺ μ½λβ
import { AsyncLocalStorage } from 'async_hooks';
const asyncLocalStorage = new AsyncLocalStorage<{ id: string }>();
// 3κ°μ μμ²μ λμμ μ²λ¦¬
async function simulateRequest(requestId: string, delay: number) {
return asyncLocalStorage.run({ id: requestId }, async () => {
console.log(`[${requestId}] Request started`);
await new Promise((resolve) => setTimeout(resolve, delay));
await nestedFunction(requestId);
console.log(`[${requestId}] Request completed`);
});
}
async function nestedFunction(expectedId: string) {
const context = asyncLocalStorage.getStore();
console.log(`[${expectedId}] context.id = ${context?.id}`);
// νμ μ¬λ°λ₯Έ 컨ν
μ€νΈλ₯Ό κ°μ Έμ΄!
if (context?.id !== expectedId) {
throw new Error('Context mismatch!'); // μ λ λ°μνμ§ μμ
}
}
// λμ μ€ν
Promise.all([simulateRequest('REQUEST-1', 200), simulateRequest('REQUEST-2', 100), simulateRequest('REQUEST-3', 150)]);
/* μΆλ ₯:
[REQUEST-1] Request started
[REQUEST-2] Request started
[REQUEST-3] Request started
[REQUEST-2] context.id = REQUEST-2 β
[REQUEST-3] context.id = REQUEST-3 β
[REQUEST-1] context.id = REQUEST-1 β
β λͺ¨λ μμ²μ΄ μ¬λ°λ₯Έ 컨ν
μ€νΈ μ μ§!
*/
λ΄λΆ λμ μ리β
// AsyncLocalStorage λ΄λΆ (λ¨μνλ λ²μ )
class AsyncLocalStorage<T> {
private stores = new Map<number, T>();
run<R>(store: T, callback: () => R): R {
// 1. μλ‘μ΄ μ€ν 컨ν
μ€νΈ ID μμ± (async_hooksκ° μλ κ΄λ¦¬)
const executionId = createNewExecutionContext();
// 2. μ΄ μ€ν 컨ν
μ€νΈμ store μ μ₯
this.stores.set(executionId, store);
// 3. μ½λ°± μ€ν (μ΄ μμ λͺ¨λ λΉλκΈ° μμ
λ κ°μ executionId μ μ§)
try {
return callback();
} finally {
this.stores.delete(executionId);
}
}
getStore(): T | undefined {
// νμ¬ μ€ν 컨ν
μ€νΈμ IDλ₯Ό μλμΌλ‘ νμ
νμ¬ ν΄λΉ store λ°ν
const currentExecutionId = getCurrentExecutionId(); // Node.jsκ° μλ μΆμ
return this.stores.get(currentExecutionId);
}
}
ν΅μ¬:
- Node.jsμ
async_hooksκ° κ° λΉλκΈ° μμ μ κ³ μ ν μ€ν 컨ν μ€νΈ IDλ₯Ό μλ ν λΉ await,Promise,setTimeoutλ±μ κ±°μ³λ ID μ μ§getStore()νΈμΆ μ νμ¬ μ€ν μ€μΈ 컨ν μ€νΈλ₯Ό μλ νμ
β οΈ μ»¨ν μ€νΈ μ μ€ μμΈκ³Ό ν΄κ²°β
μ run()μ΄ RxJS/Nest νμ΄νλΌμΈμμ μ μ€λ μ μλκ°β
AsyncLocalStorage.run(store, callback)μ βcallbackμ΄ μμλλ©΄μ μμ±λ λΉλκΈ° νλ¦βμλ§ storeλ₯Ό μ νν©λλ€.- NestJSλ μΈν°μ ν° β 컨νΈλ‘€λ¬ β νμ΄ν/κ°λ β μλ΅μΌλ‘ μ΄μ΄μ§λ κ³Όμ μμ λ΄λΆμ μΌλ‘ μλ‘μ΄ Promise 체μΈκ³Ό λ§μ΄ν¬λ‘νμ€ν¬λ₯Ό μμ±ν μ μμ΅λλ€.
defer(() => run(() => next.handle()))μ²λΌ ꡬλ μμ μ runμΌλ‘ κ°μΌ ν¨ν΄μ, κ·Έ μ΄μ /μΈλΆμμ μ΄λ―Έ λ§λ€μ΄μ§ λΉλκΈ° 체μΈκΉμ§ ν¬μνμ§ λͺ»ν΄ 컨ν μ€νΈκ°enabled: falseλ‘ λ³΄μ΄κ±°λgetStore()κ° undefinedλ₯Ό λ°νν μ μμ΅λλ€.
λ¬Έμ κ° λλ ν¨ν΄:
return defer(() =>
requestContextService.run(ctx, () => {
return next.handle().pipe(/* ... */);
})
);
μ μ€ λ°©μ§ ν¨ν΄:
- μΈν°μ
ν° μ΄λ°μ
enterWith(ctx)λ‘ βνμ¬ μ€ν 컨ν μ€νΈβμ μ μ₯μλ₯Ό μ£Όμ ν©λλ€. κ·Έλ¬λ©΄ μ΄ν μμ±λλ λͺ¨λ λΉλκΈ° μμ (컨νΈλ‘€λ¬async/await, λ΄λΆ Promise, RxJS μ²΄μΈ λ±)μΌλ‘ μ νλ©λλ€.
requestContextService.enterWith(ctx);
return next.handle().pipe(/* ... */);
Request Body/DTOλ‘ correlationId μ λ¬ λΉκΆμ₯β
- ν΄λΌμ΄μΈνΈ μ‘°μ μν, λͺ¨λ DTO/λ¬Έμ νμ°, λ΄λΆ νΈμΆ/μ΄λ²€νΈ κ²½λ‘ λλ½ λ±μΌλ‘ μΌκ΄μ±μ΄ κΉ¨μ§λλ€.
- νμ€μ ν€λ(
x-correlation-id) + μλ² μΈ‘ 컨ν μ€νΈ(ALS)μ λλ€. - νμ μ μ κ·Ό νΈμλ₯Ό μν΄
req.correlationIdκ°μ μ½κΈ°μ© μμ±λ§ μΈν°μ ν°μμ 볡μ¬ν΄ λκ³ , λΉμ¦λμ€ λ‘μ§μRequestContextService.getCorrelationId()λ‘ μ‘°ννμΈμ.
π ꡬν μν€ν μ²β
After (ꡬν ν) λ°μ΄ν° νλ¦β
1. HTTP Request
ββ x-correlation-id: abc-123
2. HttpRequestLoggingInterceptor
ββ RequestContext { correlationId: 'abc-123' }
ββ AsyncLocalStorage.enterWith(context) π
3. Controller β Service β EventPublisher
ββ AsyncLocalStorageμμ μλμΌλ‘ 컨ν
μ€νΈ μ μ§ π
4. CrossEventPublisher.publishXXX()
ββ requestContextService.getCorrelationId() β 'abc-123' π
ββ StandardEvent { correlationId: 'abc-123', ... } π
5. EventBridgeService
ββ Pub/Sub.publish({
data: event,
attributes: { correlationId: 'abc-123' } π
})
6. /api/events (Pub/Sub Push)
ββ x-correlation-id: abc-123 (ν€λ) π
ββ λ€μ λλ©μΈμΌλ‘ μ ν π
μ»΄ν¬λνΈ λ€μ΄μ΄κ·Έλ¨β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β HTTP Request Layer β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β HttpRequestLoggingInterceptor β β
β β - Extract correlationId from headers β β
β β - Create RequestContext β β
β β - Inject with AsyncLocalStorage.enterWith() β β
β ββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββΌββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Request Context Layer β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β RequestContextService β β
β β - AsyncLocalStorage<RequestContext> β β
β β - getCorrelationId(): string β β
β β - getRequestId(): string β β
β β - run<T>(context, callback): T β β
β ββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββΌββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Business Logic Layer β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Controllers β Services β Handlers β β
β β (μλμΌλ‘ 컨ν
μ€νΈ μ μ§, μμ λΆνμ) β β
β ββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββΌββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Event Publishing Layer β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β BaseCrossEventPublisher (Abstract) β β
β β - Inject RequestContextService β β
β β - createStandardEvent() β includes correlationId β β
β ββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββ β
β β β
β ββββββββββββββββββ΄ββββββββββββββββββββββββββββββββββββββ β
β β Concrete Publishers (extends Base) β β
β β - UserCrossEventPublisher β β
β β - SleepCrossEventPublisher β β
β β - MobileCrossEventPublisher β β
β β - ... (10+ publishers) β β
β ββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββΌββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Event Bridge Layer β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β EventBridgeService β β
β β - Listen to 'event' channel β β
β β - Publish to GCP Pub/Sub with correlationId β β
β ββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββΌββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
GCP Pub/Sub (events topic)
β
βΌ
Other Domains (continue tracing)
π§ ꡬν μμ½β
μ ν λ©μ»€λμ¦β
AsyncLocalStorage κΈ°λ° μλ μ ν:
HTTP Request
β Interceptorμμ AsyncLocalStorage.enterWith(context) μ£Όμ
β λΉμ¦λμ€ λ‘μ§ μ 체μμ μλμΌλ‘ 컨ν
μ€νΈ μ μ§
β Publisherμμ getCorrelationId()λ‘ μ‘°ν ν μ΄λ²€νΈμ ν¬ν¨
β Pub/Sub attributesλ‘ μ ν
β λ€μ λλ©μΈμμ μμ λ° κ³μ μΆμ
ꡬν λ¨κ³β
Phase 1: RequestContextService μμ±
@Injectable()
export class RequestContextService {
private readonly asyncLocalStorage = new AsyncLocalStorage<RequestContext>();
enterWith(context: RequestContext): void { ... }
getCorrelationId(): string | undefined { ... }
}
Phase 2: HTTP Interceptor ν΅ν©
// Interceptorμμ 컨ν
μ€νΈ μ£Όμ
this.requestContextService.enterWith({
requestId: 'req-123',
correlationId: 'cor-abc', // ν€λμμ μΆμΆ
timestamp: new Date(),
});
Phase 3: StandardEvent νμ₯
export interface StandardEvent<T = any> {
// ... κΈ°μ‘΄ νλλ€
correlationId?: string; // π μΆκ°
}
Phase 4: BaseCrossEventPublisher
protected createStandardEvent<T>(type: string, payload: T): StandardEvent<T> {
return {
eventId: uuidv4(),
type,
payload,
correlationId: this.requestContext.getCorrelationId(), // π μλ μ‘°ν
};
}
Phase 5: Publisher λ§μ΄κ·Έλ μ΄μ
@Injectable()
export class UserCrossEventPublisher extends BaseCrossEventPublisher {
async publishUserCreated(userId: string) {
// createStandardEventκ° μλμΌλ‘ correlationId ν¬ν¨
const event = this.createStandardEvent('user.created', { userId });
await this.publishCrossEvent(event);
}
}
Phase 6: Pub/Sub ν΅ν©
// λ°ν μΈ‘
await topic.publishMessage({
data: Buffer.from(JSON.stringify(event)),
attributes: { correlationId: event.correlationId }, // π attributesμ ν¬ν¨
});
// μμ μΈ‘
const correlationId = pubSubMessage.message.attributes?.correlationId;