본문으둜 κ±΄λ„ˆλ›°κΈ°

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;

πŸ“š μ°Έκ³ μžλ£Œβ€‹