마이크로서비스 디버깅: Correlation IDs가 디버그 시간을 몇 시간에서 몇 분으로 단축
Source: Dev.to
최근에 배운 내용을 공유하기 위해 새로운 글을 가져왔습니다. 오늘은 마이크로서비스 로깅에 대해 이야기하고, 적절한 로깅을 구현함으로써 디버깅 워크플로우가 악몽에서 실제로 관리 가능한 수준으로 바뀐 사례를 소개합니다. 🚀
마이크로서비스를 다루고 있다면 그 고통을 잘 아실 겁니다: 프로덕션에서 뭔가가 깨지고, 어떤 일이 일어났는지 파악하려고 여러 서비스 사이를 오가야 하죠. 익숙한 상황인가요? 이제 해결해봅시다!
🤔 문제
이 상황을 상상해 보세요: 5개의 마이크로서비스가 실행 중입니다. 사용자가 오류를 보고합니다. 여러분은 조사를 시작합니다.
- 서비스 A를 확인 → 눈에 띄는 것이 없음.
- 서비스 B를 확인 → 뭔가 있을까?
- 서비스 C를 확인 → 아직 확신이 안 서요.
명확한 답변 대신, 흩어진 로그 파일들을 조합해 퍼즐을 맞추고 타임스탬프를 맞추며 연결 고리를 찾으려 합니다. 시간이 흐릅니다. 😰
이것은 흔한 현실입니다. 많은 팀이 로그를 가지고 있지만 실용적이지 않습니다. 각 서비스가 독립적으로 로그를 남겨 요청이 시스템을 통해 이동하는 과정을 추적할 방법이 없습니다.
💡 해결책: Correlation IDs
게임 체인저? Correlation IDs.
Correlation ID를 당신의 소포 추적 번호 📦와 같이 생각해 보세요. 소포가 거치는 모든 배송 센터를 추적할 수 있듯이, Correlation ID는 요청이 거치는 모든 마이크로서비스를 추적할 수 있게 해줍니다.
모든 요청은 시스템 전체를 따라다니는 고유한 ID를 부여받습니다. 문제가 발생했을 때, 당신은:
- 해당 ID를 검색한다
- 전체 요청 흐름을 확인한다
- 정확한 실패 지점을 식별한다
간단하고, 강력하며, 효과적입니다.
🛠️ 이 시스템 구축 방법
다음과 같이 마이크로서비스를 위한 중앙 집중식 로깅 시스템을 구현할 수 있습니다:
- Pino를 사용한 구조화된 고성능 로깅
- Sentry를 이용한 오류 추적 및 실시간 알림
- Custom NestJS providers를 통한 서비스 간 일관성 확보
- Correlation IDs로 요청을 엔드‑투‑엔드 추적
단계별로 직접 구현하는 방법을 보여드릴게요! 👇
📝 Step 1: NestJS에서 Pino 설정하기
먼저, 필요한 패키지를 설치합니다:
npm install pino pino-pretty pino-http
npm install --save-dev @types/pino
왜 Pino인가? Pino는 빠릅니다 (정말 빠릅니다!), 구조화된 JSON 로그를 생성하고, NestJS 지원이 뛰어납니다.
🎯 Step 2: Custom Logger Provider 만들기
여기서 마법이 시작됩니다. 모든 로그에 correlation ID를 포함하는 커스텀 로거 프로바이더를 생성합니다:
// logger.service.ts
import { Injectable, Scope } from '@nestjs/common';
import * as pino from 'pino';
@Injectable({ scope: Scope.TRANSIENT })
export class LoggerService {
private logger: pino.Logger;
private context: string;
private correlationId: string;
constructor() {
this.logger = pino({
level: process.env.LOG_LEVEL || 'info',
transport: {
target: 'pino-pretty',
options: {
colorize: true,
translateTime: 'SYS:standard',
ignore: 'pid,hostname',
},
},
});
}
setContext(context: string) {
this.context = context;
}
setCorrelationId(correlationId: string) {
this.correlationId = correlationId;
}
private formatMessage(message: string, data?: any) {
return {
message,
context: this.context,
correlationId: this.correlationId,
timestamp: new Date().toISOString(),
...data,
};
}
log(message: string, data?: any) {
this.logger.info(this.formatMessage(message, data));
}
error(message: string, trace?: string, data?: any) {
this.logger.error(this.formatMessage(message, { trace, ...data }));
}
warn(message: string, data?: any) {
this.logger.warn(this.formatMessage(message, data));
}
debug(message: string, data?: any) {
this.logger.debug(this.formatMessage(message, data));
}
}
여기서 무슨 일이 일어나고 있나요?
- 트랜지언트 스코프 서비스(요청마다 새로운 인스턴스)를 생성합니다.
- 각 로그에
message,context,correlationId,timestamp가 포함됩니다. - 다양한 로그 레벨을 지원합니다:
info,error,warn,debug - 모든 로그가 JSON 형태로 구조화되어 검색이 용이합니다.
🔑 Step 3: Correlation ID 생성
이제 Correlation ID를 생성하고 전달해야 합니다. 이를 위해 미들웨어를 사용합니다:
// correlation-id.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { v4 as uuidv4 } from 'uuid';
@Injectable()
export class CorrelationIdMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
// 이전 서비스에서 이미 Correlation ID가 있는지 확인
const correlationId = (req.headers['x-correlation-id'] as string) || uuidv4();
// 요청에 첨부
(req as any).correlationId = correlationId;
// 응답 헤더에 추가
res.setHeader('x-correlation-id', correlationId);
next();
}
}
핵심 포인트:
- 다른 서비스에서 Correlation ID가 존재한다면 그 값을 사용합니다.
- 존재하지 않을 경우 UUID를 이용해 새로 생성합니다.
- 요청과 응답 모두에 ID를 첨부합니다.
🔗 단계 4: 서비스에서 로거 사용하기
메인 모듈에 미들웨어를 등록합니다:
// app.module.ts
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { CorrelationIdMiddleware } from './correlation-id.middleware';
import { LoggerService } from './logger.service';
@Module({
providers: [LoggerService],
exports: [LoggerService],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(CorrelationIdMiddleware).forRoutes('*');
}
}
이제 컨트롤러에서 사용합니다:
// user.controller.ts
import { Controller, Get, Req } from '@nestjs/common';
import { Request } from 'express';
import { LoggerService } from './logger.service';
@Controller('users')
export class UserController {
constructor(private readonly logger: LoggerService) {
this.logger.setContext('UserController');
}
@Get()
async getUsers(@Req() req: Request) {
const correlationId = req['correlationId'];
this.logger.setCorrelationId(correlationId);
this.logger.log('Fetching users');
// Your business logic here
return users;
}
}
즐거운 로깅 되세요! 🎉
🚨 Step 5: 오류 추적을 위한 Sentry 통합
Sentry 설치
npm install @sentry/node
메인 파일에서 구성하기
// main.ts
import * as Sentry from '@sentry/node';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV,
tracesSampleRate: 1.0,
});
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Add Sentry error handler
app.use(Sentry.Handlers.requestHandler());
app.use(Sentry.Handlers.errorHandler());
await app.listen(3000);
}
bootstrap();
로거를 업데이트하여 Sentry에 오류 전송
// logger.service.ts
import * as Sentry from '@sentry/node';
import { Logger } from 'pino';
export class LoggerService {
private logger: Logger;
// ... other methods ...
error(message: string, trace?: string, data?: any) {
const errorData = this.formatMessage(message, { trace, ...data });
this.logger.error(errorData);
// Also send to Sentry
Sentry.captureException(new Error(message), {
extra: errorData,
});
}
}
🌐 Step 6: 서비스 간 Correlation ID 전달
다른 마이크로서비스에 HTTP 호출을 할 때, correlation ID를 전달합니다:
// some.service.ts
import { HttpService } from '@nestjs/axios';
import { Injectable } from '@nestjs/common';
import { LoggerService } from './logger.service';
@Injectable()
export class SomeService {
constructor(
private readonly httpService: HttpService,
private readonly logger: LoggerService,
) {}
async callAnotherService(correlationId: string, data: any) {
this.logger.log('Calling Service B', { data });
return this.httpService
.post('http://service-b/endpoint', data, {
headers: {
'x-correlation-id': correlationId, // ← Pass it along!
},
})
.toPromise();
}
}
📊 실제 예시
전
[2025-01-15 10:30:45] Request received
[2025-01-15 10:30:46] Processing data
[2025-01-15 10:30:47] Error: Operation failed
어떤 요청인가요? 어떤 사용자인가요? 전혀 모릅니다. 🤷♀️
후
{
"level": "info",
"message": "Request received",
"context": "ApiController",
"correlationId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2025-01-15T10:30:45.123Z"
}
{
"level": "info",
"message": "Processing data",
"context": "DataService",
"correlationId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2025-01-15T10:30:46.456Z"
}
{
"level": "error",
"message": "Operation failed: Validation error",
"context": "DataService",
"correlationId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2025-01-15T10:30:47.789Z"
}
이제 550e8400-e29b-41d4-a716-446655440000 를 검색하면 모든 서비스에서 전체 요청 흐름을 확인할 수 있습니다! 🎯
✨ 영향
| 구현 전 | 구현 후 | |
|---|---|---|
| 디버그 워크플로 | 각 서비스를 수동으로 확인하고, 타임스탬프를 연관시켜, 여러 시간을 소요 | 상관관계 ID로 검색하여 전체 요청 흐름을 즉시 확인하고, 몇 분 안에 디버그 |
| 일반적인 시나리오 | 사용자가 오류를 보고 → 서비스를 가로질러 로그를 수동으로 검사 → 수시간 작업 | 상관관계 ID로 검색 → Service A → Service B → Service C 순서의 요청 경로 확인 → 실패 지점 파악 → < 10 분 |
🎯 주요 요점
- 구조 – 일반 텍스트 대신 JSON(검색 가능하고 파싱 가능) 을 사용합니다.
- 컨텍스트 – 상관관계 ID는 서비스 전반에 걸친 모든 요청을 추적합니다.
- 적절한 레벨 –
debug/info/error를 적절히 사용하고, 모든 것을info로 기록하지 마세요. - 중앙화 – 모든 것을 한 곳에서 검색할 수 있게 합니다(예: Loki, Elastic, CloudWatch).
- 실시간 알림 – Sentry(또는 유사 도구)가 사용자가 불만을 제기하기 전에 오류를 포착합니다.
💭 최종 생각
적절한 로깅을 설정하는 데는 초기 며칠이 걸리지만, 이후 발생하는 모든 운영 사고에서 시간 혹은 며칠을 절약할 수 있습니다. 마이크로서비스를 구축하고 있다면, 로깅을 사후 고려사항이 아닌 핵심 기능으로 다루세요. 미래의 당신이 고마워할 것입니다. 😊
🔗 리소스
당신은 어떠신가요? 🤔 마이크로서비스에 correlation ID를 구현해 보셨나요? 어떤 어려움을 겪으셨나요? 아래에 경험을 공유해주세요!
이 글이 도움이 되었다면, 웹 개발, NestJS, DevOps 실천에 관한 더 많은 글을 위해 저를 팔로우해주세요. 함께 배우자 ❤️