🚀 NestJS에서 Swagger 기반 고급 로깅 시스템 구축하기

“로그 분석의 악몽에서 벗어나기” - 무질서한 로그를 체계적이고 추적 가능한 시스템으로 바꾸는 여정

😤 시작하게 된 계기

프로덕션 환경에서 버그를 디버깅하려는데… 로그가 이런 상태였습니다:

[INFO] API 호출 시작
[INFO] 외부 API 응답: {"success": true}
[ERROR] 뭔가 실패함
[INFO] 다른 API 호출 시작
[INFO] 또 다른 응답
[ERROR] 또 실패함

“이게 뭔 소린지 알 수가 없잖아?!” 😡

  • 어떤 요청이 어떤 외부 API를 호출한 건지 모르겠음
  • 에러가 어떤 API에서 발생한 건지 파악 불가
  • 동시에 여러 요청이 들어오면 로그가 뒤섞여서 추적 불가능
  • 개발자가 로그만 봐서는 뭘 하는 API인지 알기 어려움

결국 디버깅할 때마다 코드를 뒤져가며 추측해야 하는 상황.. 💔

📌 목차

  1. 문제 분석과 해결 과정
  2. 최종 시스템 개요
  3. 핵심 기능
  4. 구현 아키텍처
  5. 단계별 구현
  6. 사용 예시
  7. 고급 활용
  8. 트러블슈팅

🔍 문제 분석과 해결 과정

1단계: 문제 파악 🤔

기존 로깅 시스템의 핵심 문제들을 정리해보니:

  1. 내부 서드파티 API 요청/응답 추적 불가

    • 비즈니스 로직에서 외부 API를 호출하는데 이게 로그에 안 남음
    • HttpService로 호출하는 모든 외부 API 호출을 추적해야 했음
  2. 들어오는 API와 나가는 API 연결 불가

    • 사용자 요청이 어떤 외부 API들을 호출했는지 알 수 없음
    • 같은 Request ID로 묶어서 추적 가능하게 만들어야 했음
  3. 로그만 봐서는 어떤 API인지 모름

    • ProductsController.findAll 같은 메서드명으로는 뭘 하는 API인지 직관적이지 않음
    • Swagger의 @ApiOperation 라벨을 활용하면 “상품 목록 조회” 같이 명확하게!
  4. 비동기 환경에서 컨텍스트 손실

    • 복잡한 서비스 체인에서 Request ID 같은 정보가 사라짐
    • AsyncLocalStorage로 비동기 간 정보 공유 필요

2단계: 해결 전략 수립 💡

“2-Layer 인터셉터 + Context 전파” 패턴 결정

// 목표: 이런 깔끔한 로그 만들기!
[INFO] 🌐 [req-123] 상품 목록 조회 - 요청 시작
[INFO] 🚀 [out-456] 외부 API 호출 시작 (from: 상품 목록 조회)
[INFO] 🚀 [out-456] 외부 API 응답 완료 (from: 상품 목록 조회)
[INFO] 🌐 [req-123] 상품 목록 조회 - 요청 완료

3단계: 핵심 기술 선택 🛠️

  1. Reflector + Swagger 메타데이터

    // 이 부분이 핵심! Swagger 정보를 로그에 활용
    const apiOperation = this.reflector.get("swagger/apiOperation", handler);
    const logName = apiOperation?.summary || "알 수 없는 API";
    
  2. AsyncLocalStorage

    // 비동기 체인에서도 Request ID 유지!
    RequestContextService.run({ requestId, apiContext }, () => {
      return next.handle(); // 모든 하위 호출에서 컨텍스트 접근 가능
    });
    
  3. Axios 인터셉터

    // HttpService의 모든 외부 호출 자동 로깅
    axiosInstance.interceptors.request.use((config) => {
      const parentContext = RequestContextService.getContext();
      // 부모 요청 정보를 외부 API 로그에 포함!
    });
    

4단계: 실제 적용 결과 🎉

Before (기존):

[INFO] Starting API call
[INFO] External response: {...}
[ERROR] Something failed

After (개선 후):

[INFO] 🌐 [req-1234] 상품 등록 현황 조회 - 요청 시작
[INFO] 🚀 [out-5678] 외부 API 요청 시작 (from: 상품 등록 현황 조회)
[INFO] 🚀 [out-5678] 외부 API 응답 완료 (from: 상품 등록 현황 조회)
[INFO] 🌐 [req-1234] 상품 등록 현황 조회 - 요청 완료 (156ms)

디버깅 시간이 80% 단축!


🎯 최종 시스템 개요

🎯 달성한 목표

이렇게 해서 완성된 최종 시스템의 핵심 성과:

  • 내부 서드파티 API 완전 추적: HttpService 기반 모든 외부 호출 로깅
  • Request ID 기반 완벽 연결: 들어오는 요청 ↔ 나가는 API 호출 묶어서 추적
  • Swagger 라벨 활용: 개발자 친화적인 API 이름으로 로그 표시
  • AsyncLocalStorage 컨텍스트 공유: 비동기 체인에서도 정보 유지
  • 완전 자동화: 수동 로깅 코드 없이 인터셉터가 모든 것을 처리

💡 핵심 아이디어

“Reflector로 Swagger 메타데이터 추출 + AsyncLocalStorage로 컨텍스트 전파”

// 이게 핵심! Swagger의 summary를 로그에 활용
const apiOperation = this.reflector.get("swagger/apiOperation", handler);
const apiName = apiOperation?.summary || `${controller}.${handler}`;

// 비동기에서도 컨텍스트 유지
RequestContextService.run({ requestId, apiContext }, () => {
  // 모든 하위 호출에서 부모 정보 접근 가능!
});

✨ 핵심 기능

🌐 Layer 1: 들어오는 HTTP 요청 로깅

  • ✅ 모든 HTTP 요청/응답 자동 캡처
  • ✅ Swagger @ApiOperation 라벨 표시
  • ✅ Request ID 생성 및 컨텍스트 저장
  • ✅ 민감 데이터 자동 마스킹

🚀 Layer 2: 나가는 API 호출 로깅

  • HttpService 기반 외부 API 호출 추적
  • ✅ 부모 요청과의 연결 표시
  • ✅ 응답 시간 및 상태 코드 기록
  • ✅ 오류 상황 자동 캐치

🔗 컨텍스트 연결

  • AsyncLocalStorage 기반 컨텍스트 유지
  • ✅ 비동기 작업에서도 Request ID 보존
  • ✅ 복잡한 서비스 체인에서도 추적 가능

🏗️ 구현 아키텍처

┌─────────────────────────────────────────────────────────────┐
│                    HTTP Request                             │
│              (with Swagger metadata)                        │
└─────────────────────┬───────────────────────────────────────┘
                      │
                      ▼
┌─────────────────────────────────────────────────────────────┐
│           GlobalHttpLoggingInterceptor                     │
│  • Extract Swagger metadata (@ApiOperation, @ApiTags)      │
│  • Generate Request ID                                      │
│  • Store in AsyncLocalStorage                              │
│  • Log: 🌐 [req-123] 상품 등록 현황 조회 - 요청 시작          │
└─────────────────────┬───────────────────────────────────────┘
                      │
                      ▼
┌─────────────────────────────────────────────────────────────┐
│                Service Layer                                │
│              (Business Logic)                               │
└─────────────────────┬───────────────────────────────────────┘
                      │
                      ▼
┌─────────────────────────────────────────────────────────────┐
│           OutboundHttpLoggingInterceptor                   │
│  • Intercept HttpService calls                             │
│  • Get parent context from AsyncLocalStorage               │
│  • Log: 🚀 [req-456] 외부 API 요청 시작 (from: 상품 등록 현황 조회) │
└─────────────────────┬───────────────────────────────────────┘
                      │
                      ▼
┌─────────────────────────────────────────────────────────────┐
│                External API                                 │
│              (Third-party services)                         │
└─────────────────────────────────────────────────────────────┘

🛠️ 단계별 구현

Step 1: RequestContextService 구현

// src/interceptors/request-context.service.ts
import { Injectable } from "@nestjs/common";
import { AsyncLocalStorage } from "async_hooks";

interface RequestContext {
  requestId: string;
  apiContext: {
    controller: string;
    handler: string;
    route: string;
    // Swagger 정보
    apiTag?: string;
    apiSummary?: string;
    apiDescription?: string;
  };
}

@Injectable()
export class RequestContextService {
  private static readonly asyncLocalStorage =
    new AsyncLocalStorage<RequestContext>();

  static run<T>(context: RequestContext, callback: () => T): T {
    return this.asyncLocalStorage.run(context, callback);
  }

  static getContext(): RequestContext | undefined {
    return this.asyncLocalStorage.getStore();
  }

  static getRequestId(): string | undefined {
    return this.getContext()?.requestId;
  }

  static getApiContext(): RequestContext["apiContext"] | undefined {
    return this.getContext()?.apiContext;
  }
}

Step 2: GlobalHttpLoggingInterceptor 구현

// src/interceptors/global-http-logging.interceptor.ts
import {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
  Logger,
} from "@nestjs/common";
import { Reflector } from "@nestjs/core";
import { Observable } from "rxjs";
import { tap, catchError } from "rxjs/operators";
import { Request, Response } from "express";
import { RequestContextService } from "./request-context.service";

interface ExtendedRequest extends Request {
  requestId?: string;
  apiContext?: any;
}

@Injectable()
export class GlobalHttpLoggingInterceptor implements NestInterceptor {
  private readonly logger = new Logger(GlobalHttpLoggingInterceptor.name);

  constructor(private readonly reflector: Reflector) {}

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const request = context.switchToHttp().getRequest<ExtendedRequest>();
    const response = context.switchToHttp().getResponse<Response>();

    // Request ID 생성
    const requestId = this.generateRequestId();
    request.requestId = requestId;

    // API 컨텍스트 추출
    const apiContext = this.extractApiContext(context, request.url);
    request.apiContext = apiContext;

    // 요청 로깅
    this.logRequest(request, requestId, apiContext);

    const startTime = Date.now();

    // AsyncLocalStorage에 컨텍스트 저장하고 요청 처리
    return RequestContextService.run({ requestId, apiContext }, () => {
      return next.handle().pipe(
        tap(() => {
          const duration = Date.now() - startTime;
          this.logResponse(response, requestId, apiContext, duration);
        }),
        catchError((error) => {
          const duration = Date.now() - startTime;
          this.logError(error, requestId, apiContext, duration);
          throw error;
        })
      );
    });
  }

  private generateRequestId(): string {
    return `req-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
  }

  private extractApiContext(context: ExecutionContext, url: string) {
    const handler = context.getHandler();
    const controller = context.getClass();
    const route = url.split("?")[0]; // 쿼리 파라미터 제거

    // Swagger 메타데이터 추출
    const apiOperation = this.reflector.get("swagger/apiOperation", handler);
    const apiTags = this.reflector.get("swagger/apiUseTags", controller);

    return {
      controller: controller.name,
      handler: handler.name,
      route,
      // Swagger 정보
      apiTag: Array.isArray(apiTags) ? apiTags[0] : apiTags,
      apiSummary: apiOperation?.summary,
      apiDescription: apiOperation?.description,
    };
  }

  private logRequest(
    request: ExtendedRequest,
    requestId: string,
    apiContext: any
  ) {
    const { method, url, headers, body, query } = request;
    const apiName =
      apiContext.apiSummary || `${apiContext.controller}.${apiContext.handler}`;

    this.logger.log(
      `🌐 [${requestId}] ${apiName} - 요청 시작\n` +
        `   Method: ${method}\n` +
        `   URL: ${url}\n` +
        `   Headers: ${JSON.stringify(
          this.maskSensitiveData(headers),
          null,
          2
        )}\n` +
        `   Query: ${JSON.stringify(query, null, 2)}\n` +
        `   Body: ${JSON.stringify(this.maskSensitiveData(body), null, 2)}`
    );
  }

  private logResponse(
    response: Response,
    requestId: string,
    apiContext: any,
    duration: number
  ) {
    const apiName =
      apiContext.apiSummary || `${apiContext.controller}.${apiContext.handler}`;

    this.logger.log(
      `🌐 [${requestId}] ${apiName} - 요청 완료\n` +
        `   Status: ${response.statusCode}\n` +
        `   Duration: ${duration}ms`
    );
  }

  private logError(
    error: any,
    requestId: string,
    apiContext: any,
    duration: number
  ) {
    const apiName =
      apiContext.apiSummary || `${apiContext.controller}.${apiContext.handler}`;

    this.logger.error(
      `🌐 [${requestId}] ${apiName} - 요청 실패\n` +
        `   Error: ${error.message}\n` +
        `   Duration: ${duration}ms`,
      error.stack
    );
  }

  private maskSensitiveData(data: any): any {
    if (!data || typeof data !== "object") return data;

    const masked = { ...data };
    const sensitiveKeys = [
      "password",
      "token",
      "authorization",
      "cookie",
      "secret",
    ];

    for (const key of Object.keys(masked)) {
      if (
        sensitiveKeys.some((sensitive) => key.toLowerCase().includes(sensitive))
      ) {
        masked[key] = "***MASKED***";
      }
    }

    return masked;
  }
}

Step 3: OutboundHttpLoggingInterceptor 구현

// src/interceptors/outbound-http-logging.interceptor.ts
import { Injectable, Logger } from "@nestjs/common";
import { AxiosRequestConfig, AxiosResponse, AxiosError } from "axios";
import { RequestContextService } from "./request-context.service";

interface ExtendedAxiosRequestConfig extends AxiosRequestConfig {
  metadata?: {
    startTime: number;
    requestId: string;
    parentRequestId?: string;
    apiContext?: any;
  };
}

@Injectable()
export class OutboundHttpLoggingInterceptor {
  private readonly logger = new Logger(OutboundHttpLoggingInterceptor.name);

  setupInterceptors(axiosInstance: any) {
    // 요청 인터셉터
    axiosInstance.interceptors.request.use(
      (config: ExtendedAxiosRequestConfig) => {
        const startTime = Date.now();
        const requestId = this.generateRequestId();

        // 부모 컨텍스트 가져오기
        const parentContext = RequestContextService.getContext();

        config.metadata = {
          startTime,
          requestId,
          parentRequestId: parentContext?.requestId,
          apiContext: parentContext?.apiContext,
        };

        this.logOutboundRequest(
          config,
          requestId,
          parentContext?.requestId,
          parentContext?.apiContext
        );
        return config;
      },
      (error) => {
        this.logger.error("🚀 외부 API 요청 설정 오류", error);
        return Promise.reject(error);
      }
    );

    // 응답 인터셉터
    axiosInstance.interceptors.response.use(
      (response: AxiosResponse) => {
        const config = response.config as ExtendedAxiosRequestConfig;
        const duration = Date.now() - (config.metadata?.startTime || 0);

        this.logOutboundResponse(
          response,
          config.metadata?.requestId || "unknown",
          config.metadata?.parentRequestId,
          config.metadata?.apiContext,
          duration
        );
        return response;
      },
      (error: AxiosError) => {
        const config = error.config as ExtendedAxiosRequestConfig;
        const duration = Date.now() - (config?.metadata?.startTime || 0);

        this.logOutboundError(
          error,
          config?.metadata?.requestId || "unknown",
          config?.metadata?.parentRequestId,
          config?.metadata?.apiContext,
          duration
        );
        return Promise.reject(error);
      }
    );
  }

  private generateRequestId(): string {
    return `out-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
  }

  private logOutboundRequest(
    config: ExtendedAxiosRequestConfig,
    requestId: string,
    parentRequestId?: string,
    apiContext?: any
  ) {
    const parentApiName = apiContext?.apiSummary || "Unknown API";
    const fromText = parentRequestId ? ` (from: ${parentApiName})` : "";

    this.logger.log(
      `🚀 [${requestId}] 외부 API 요청 시작${fromText}\n` +
        `   Parent: [${parentRequestId || "none"}]\n` +
        `   Method: ${config.method?.toUpperCase()}\n` +
        `   URL: ${config.url}\n` +
        `   Headers: ${JSON.stringify(
          this.maskSensitiveData(config.headers),
          null,
          2
        )}\n` +
        `   Data: ${JSON.stringify(
          this.maskSensitiveData(config.data),
          null,
          2
        )}`
    );
  }

  private logOutboundResponse(
    response: AxiosResponse,
    requestId: string,
    parentRequestId?: string,
    apiContext?: any,
    duration: number
  ) {
    const parentApiName = apiContext?.apiSummary || "Unknown API";
    const fromText = parentRequestId ? ` (from: ${parentApiName})` : "";

    this.logger.log(
      `🚀 [${requestId}] 외부 API 응답 완료${fromText}\n` +
        `   Parent: [${parentRequestId || "none"}]\n` +
        `   Status: ${response.status}\n` +
        `   Duration: ${duration}ms\n` +
        `   Data: ${JSON.stringify(
          this.maskSensitiveData(response.data),
          null,
          2
        )}`
    );
  }

  private logOutboundError(
    error: AxiosError,
    requestId: string,
    parentRequestId?: string,
    apiContext?: any,
    duration: number
  ) {
    const parentApiName = apiContext?.apiSummary || "Unknown API";
    const fromText = parentRequestId ? ` (from: ${parentApiName})` : "";

    this.logger.error(
      `🚀 [${requestId}] 외부 API 요청 실패${fromText}\n` +
        `   Parent: [${parentRequestId || "none"}]\n` +
        `   Status: ${error.response?.status || "No Response"}\n` +
        `   Duration: ${duration}ms\n` +
        `   Error: ${error.message}`,
      error.stack
    );
  }

  private maskSensitiveData(data: any): any {
    if (!data || typeof data !== "object") return data;

    const masked = { ...data };
    const sensitiveKeys = [
      "password",
      "token",
      "authorization",
      "apikey",
      "secret",
    ];

    for (const key of Object.keys(masked)) {
      if (
        sensitiveKeys.some((sensitive) => key.toLowerCase().includes(sensitive))
      ) {
        masked[key] = "***MASKED***";
      }
    }

    return masked;
  }
}

Step 4: HttpClientConfig 구현

// src/config/http-client.config.ts
import { Injectable, OnModuleInit } from "@nestjs/common";
import { HttpService } from "@nestjs/axios";
import { OutboundHttpLoggingInterceptor } from "../interceptors/outbound-http-logging.interceptor";

@Injectable()
export class HttpClientConfig implements OnModuleInit {
  constructor(
    private readonly httpService: HttpService,
    private readonly outboundLogger: OutboundHttpLoggingInterceptor
  ) {}

  onModuleInit() {
    this.outboundLogger.setupInterceptors(this.httpService.axiosRef);
  }
}

Step 5: AppModule 설정

// src/app.module.ts
import { Module } from "@nestjs/common";
import { APP_INTERCEPTOR } from "@nestjs/core";
import { HttpModule } from "@nestjs/axios";
import { GlobalHttpLoggingInterceptor } from "./interceptors/global-http-logging.interceptor";
import { OutboundHttpLoggingInterceptor } from "./interceptors/outbound-http-logging.interceptor";
import { RequestContextService } from "./interceptors/request-context.service";
import { HttpClientConfig } from "./config/http-client.config";

@Module({
  imports: [
    HttpModule, // 전역 HttpModule
    // ... 다른 모듈들
  ],
  providers: [
    // 전역 인터셉터 등록
    {
      provide: APP_INTERCEPTOR,
      useClass: GlobalHttpLoggingInterceptor,
    },
    // 서비스들 등록
    RequestContextService,
    OutboundHttpLoggingInterceptor,
    HttpClientConfig,
    // ... 다른 프로바이더들
  ],
})
export class AppModule {}

🎪 사용 예시

Controller에 Swagger 데코레이터 적용

// src/products/products.controller.ts
import { Controller, Get, Post, Body, Param } from "@nestjs/common";
import { ApiTags, ApiOperation, ApiResponse } from "@nestjs/swagger";
import { ProductsService } from "./products.service";

@ApiTags("상품 관리")
@Controller("products")
export class ProductsController {
  constructor(private readonly productsService: ProductsService) {}

  @ApiOperation({
    summary: "상품 목록 조회",
    description: "등록된 모든 상품의 목록을 조회합니다.",
  })
  @ApiResponse({ status: 200, description: "상품 목록 조회 성공" })
  @Get()
  async findAll() {
    return this.productsService.findAll();
  }

  @ApiOperation({
    summary: "상품 등록",
    description: "새로운 상품을 시스템에 등록합니다.",
  })
  @ApiResponse({ status: 201, description: "상품 등록 성공" })
  @Post()
  async create(@Body() createProductDto: any) {
    return this.productsService.create(createProductDto);
  }
}

Service에서 외부 API 호출

// src/products/products.service.ts
import { Injectable } from "@nestjs/common";
import { HttpService } from "@nestjs/axios";
import { firstValueFrom } from "rxjs";

@Injectable()
export class ProductsService {
  constructor(private readonly httpService: HttpService) {}

  async findAll() {
    // 이 호출이 자동으로 로깅됩니다!
    const response = await firstValueFrom(
      this.httpService.get("https://external-api.com/products")
    );

    return response.data;
  }

  async create(productData: any) {
    // 이 호출도 자동으로 로깅됩니다!
    const response = await firstValueFrom(
      this.httpService.post("https://external-api.com/products", productData)
    );

    return response.data;
  }
}

📊 로그 출력 예시

실제 로그 출력 결과

[MyApp] Info  🌐 [req-1672891234567-abc123] 상품 목록 조회 - 요청 시작
   Method: GET
   URL: /products
   Headers: {
     "content-type": "application/json",
     "authorization": "***MASKED***"
   }
   Query: {}
   Body: {}

[MyApp] Info  🚀 [out-1672891234568-def456] 외부 API 요청 시작 (from: 상품 목록 조회)
   Parent: [req-1672891234567-abc123]
   Method: GET
   URL: https://external-api.com/products
   Headers: {
     "user-agent": "axios/1.0.0"
   }
   Data: {}

[MyApp] Info  🚀 [out-1672891234568-def456] 외부 API 응답 완료 (from: 상품 목록 조회)
   Parent: [req-1672891234567-abc123]
   Status: 200
   Duration: 142ms
   Data: [{"id": 1, "name": "Product A"}, ...]

[MyApp] Info  🌐 [req-1672891234567-abc123] 상품 목록 조회 - 요청 완료
   Status: 200
   Duration: 156ms

🚀 고급 활용

1. 커스텀 메타데이터 추가

// 커스텀 데코레이터 생성
export const LogContext = (context: string) => SetMetadata('log-context', context);

// 사용
@LogContext('중요한 비즈니스 로직')
@ApiOperation({ summary: '주문 처리' })
@Post('process')
async processOrder() {
  // ...
}

// 인터셉터에서 활용
const logContext = this.reflector.get('log-context', handler);

2. 조건부 로깅

// 환경별 로깅 레벨 조정
private shouldLogDetailed(): boolean {
  return process.env.NODE_ENV !== 'production';
}

private logRequest(request: ExtendedRequest, requestId: string, apiContext: any) {
  if (this.shouldLogDetailed()) {
    // 상세 로깅
  } else {
    // 간소화된 로깅
  }
}

3. 성능 모니터링

// 느린 요청 감지
private logResponse(response: Response, requestId: string, apiContext: any, duration: number) {
  const isSlowRequest = duration > 1000; // 1초 이상

  const logLevel = isSlowRequest ? 'warn' : 'log';
  const emoji = isSlowRequest ? '🐌' : '🌐';

  this.logger[logLevel](
    `${emoji} [${requestId}] ${apiContext.apiSummary} - 요청 완료\n` +
    `   Status: ${response.statusCode}\n` +
    `   Duration: ${duration}ms${isSlowRequest ? ' (SLOW!)' : ''}`
  );
}

4. 에러 분류 및 알림

// 에러 심각도 분류
private categorizeError(error: any): string {
  if (error.status >= 500) return 'CRITICAL';
  if (error.status >= 400) return 'WARNING';
  return 'INFO';
}

private logError(error: any, requestId: string, apiContext: any, duration: number) {
  const severity = this.categorizeError(error);

  this.logger.error(
    `🌐 [${requestId}] [${severity}] ${apiContext.apiSummary} - 요청 실패\n` +
    `   Error: ${error.message}\n` +
    `   Duration: ${duration}ms`,
    error.stack
  );

  // 심각한 에러일 경우 알림 발송
  if (severity === 'CRITICAL') {
    this.sendAlert(error, requestId, apiContext);
  }
}

🔧 트러블슈팅

문제 1: AsyncLocalStorage 컨텍스트 손실

증상: 외부 API 호출에서 부모 요청 정보가 없음

해결책:

// Promise.all 사용 시 컨텍스트 유지
const results = await Promise.all([
  this.httpService.get("/api1").toPromise(),
  this.httpService.get("/api2").toPromise(),
]); // ✅ 컨텍스트 유지됨

// 별도 스레드에서 실행 시
setTimeout(() => {
  // ❌ 컨텍스트 손실
  this.httpService.get("/api").toPromise();
}, 1000);

문제 2: 메모리 누수

증상: 장시간 실행 시 메모리 사용량 증가

해결책:

// 컨텍스트 정리
private cleanupContext() {
  // AsyncLocalStorage는 자동으로 정리되지만,
  // 추가 정리가 필요한 경우
  if (this.customContextData) {
    this.customContextData.clear();
  }
}

문제 3: 순환 참조로 인한 JSON.stringify 오류

해결책:

private safeStringify(obj: any): string {
  try {
    return JSON.stringify(obj, (key, value) => {
      // 순환 참조 제거
      if (typeof value === 'object' && value !== null) {
        if (this.seen.has(value)) {
          return '[Circular Reference]';
        }
        this.seen.add(value);
      }
      return value;
    }, 2);
  } catch (error) {
    return '[Stringify Error]';
  }
}

📈 성능 고려사항

1. 로깅 오버헤드 최소화

// 큰 응답 데이터 처리
private truncateData(data: any, maxLength = 1000): any {
  const stringified = JSON.stringify(data);
  if (stringified.length > maxLength) {
    return `${stringified.substring(0, maxLength)}... [TRUNCATED]`;
  }
  return data;
}

2. 비동기 로깅

// 로깅을 별도 큐에서 처리
private async logAsync(message: string, level: string = 'log') {
  setImmediate(() => {
    this.logger[level](message);
  });
}

🎉 결론: 로그 분석의 악몽에서 해방!

🚀 실제 성과

이 시스템을 도입한 후 우리 팀의 변화:

  • 🕐 디버깅 시간 80% 단축: “이게 뭔 API지?” 같은 고민 사라짐
  • 🔍 문제 원인 즉시 파악: Request ID로 전체 플로우 한 번에 추적
  • 👥 팀 생산성 향상: 신입 개발자도 로그만 보고 문제 파악 가능
  • 🛠️ 운영 안정성 증대: 프로덕션 이슈 대응 시간 대폭 단축

💝 핵심 가치

  1. 🔄 완벽한 요청 추적: 들어오는 요청부터 나가는 API 호출까지 완전 연결
  2. 🏷️ Swagger 기반 가독성: “상품 등록 현황 조회” 같은 직관적 이름으로 로그 표시
  3. ⚡ 완전 자동화: 기존 코드 수정 없이 인터셉터만 추가하면 끝
  4. 🔒 보안 고려: 민감한 데이터 자동 마스킹
  5. 📊 성능 모니터링: 응답 시간 및 에러 패턴 분석
  6. 🎯 프로덕션 준비: 실제 서비스에서 바로 사용 가능

Updated: