마이크로서비스 디버깅: Correlation IDs가 디버그 시간을 몇 시간에서 몇 분으로 단축

발행: (2026년 1월 14일 오후 05:03 GMT+9)
12 min read
원문: Dev.to

Source: Dev.to

최근에 배운 내용을 공유하기 위해 새로운 글을 가져왔습니다. 오늘은 마이크로서비스 로깅에 대해 이야기하고, 적절한 로깅을 구현함으로써 디버깅 워크플로우가 악몽에서 실제로 관리 가능한 수준으로 바뀐 사례를 소개합니다. 🚀

마이크로서비스를 다루고 있다면 그 고통을 잘 아실 겁니다: 프로덕션에서 뭔가가 깨지고, 어떤 일이 일어났는지 파악하려고 여러 서비스 사이를 오가야 하죠. 익숙한 상황인가요? 이제 해결해봅시다!

🤔 문제

이 상황을 상상해 보세요: 5개의 마이크로서비스가 실행 중입니다. 사용자가 오류를 보고합니다. 여러분은 조사를 시작합니다.

  • 서비스 A를 확인 → 눈에 띄는 것이 없음.
  • 서비스 B를 확인 → 뭔가 있을까?
  • 서비스 C를 확인 → 아직 확신이 안 서요.

명확한 답변 대신, 흩어진 로그 파일들을 조합해 퍼즐을 맞추고 타임스탬프를 맞추며 연결 고리를 찾으려 합니다. 시간이 흐릅니다. 😰

이것은 흔한 현실입니다. 많은 팀이 로그를 가지고 있지만 실용적이지 않습니다. 각 서비스가 독립적으로 로그를 남겨 요청이 시스템을 통해 이동하는 과정을 추적할 방법이 없습니다.

💡 해결책: Correlation IDs

게임 체인저? Correlation IDs.

Correlation ID를 당신의 소포 추적 번호 📦와 같이 생각해 보세요. 소포가 거치는 모든 배송 센터를 추적할 수 있듯이, Correlation ID는 요청이 거치는 모든 마이크로서비스를 추적할 수 있게 해줍니다.

모든 요청은 시스템 전체를 따라다니는 고유한 ID를 부여받습니다. 문제가 발생했을 때, 당신은:

  1. 해당 ID를 검색한다
  2. 전체 요청 흐름을 확인한다
  3. 정확한 실패 지점을 식별한다

간단하고, 강력하며, 효과적입니다.

🛠️ 이 시스템 구축 방법

다음과 같이 마이크로서비스를 위한 중앙 집중식 로깅 시스템을 구현할 수 있습니다:

  • 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 실천에 관한 더 많은 글을 위해 저를 팔로우해주세요. 함께 배우자 ❤️

Back to Blog

관련 글

더 보기 »

ASP.NET Core에서 Worldpay 웹훅 디버깅

소개 Worldpay를 프로덕션 ASP.NET Core 애플리케이션에 통합할 때, webhook 실패를 디버깅하는 것은 가장 어려운 문제 중 하나가 될 수 있습니다. 이벤트는 …

Logger 모듈을 사용한 FastAPI 기본 로깅

Application이 Production에서 충돌할 때 Production에서 애플리케이션이 충돌했으며 사용자는 다음에 무엇을 해야 할지 확신하지 못했습니다. 나는 작년에 이와 같은 상황을 프로젝트에서 경험했습니다.