쿠팡프록시서버 로그시스템 구현
🚀 NestJS에서 Swagger 기반 고급 로깅 시스템 구축하기
“로그 분석의 악몽에서 벗어나기” - 무질서한 로그를 체계적이고 추적 가능한 시스템으로 바꾸는 여정
😤 시작하게 된 계기
프로덕션 환경에서 버그를 디버깅하려는데… 로그가 이런 상태였습니다:
[INFO] API 호출 시작
[INFO] 외부 API 응답: {"success": true}
[ERROR] 뭔가 실패함
[INFO] 다른 API 호출 시작
[INFO] 또 다른 응답
[ERROR] 또 실패함
“이게 뭔 소린지 알 수가 없잖아?!” 😡
- 어떤 요청이 어떤 외부 API를 호출한 건지 모르겠음
- 에러가 어떤 API에서 발생한 건지 파악 불가
- 동시에 여러 요청이 들어오면 로그가 뒤섞여서 추적 불가능
- 개발자가 로그만 봐서는 뭘 하는 API인지 알기 어려움
결국 디버깅할 때마다 코드를 뒤져가며 추측해야 하는 상황.. 💔
📌 목차
🔍 문제 분석과 해결 과정
1단계: 문제 파악 🤔
기존 로깅 시스템의 핵심 문제들을 정리해보니:
-
내부 서드파티 API 요청/응답 추적 불가
- 비즈니스 로직에서 외부 API를 호출하는데 이게 로그에 안 남음
HttpService로 호출하는 모든 외부 API 호출을 추적해야 했음
-
들어오는 API와 나가는 API 연결 불가
- 사용자 요청이 어떤 외부 API들을 호출했는지 알 수 없음
- 같은 Request ID로 묶어서 추적 가능하게 만들어야 했음
-
로그만 봐서는 어떤 API인지 모름
ProductsController.findAll같은 메서드명으로는 뭘 하는 API인지 직관적이지 않음- Swagger의
@ApiOperation라벨을 활용하면 “상품 목록 조회” 같이 명확하게!
-
비동기 환경에서 컨텍스트 손실
- 복잡한 서비스 체인에서 Request ID 같은 정보가 사라짐
AsyncLocalStorage로 비동기 간 정보 공유 필요
2단계: 해결 전략 수립 💡
“2-Layer 인터셉터 + Context 전파” 패턴 결정
// 목표: 이런 깔끔한 로그 만들기!
[INFO] 🌐 [req-123] 상품 목록 조회 - 요청 시작
[INFO] 🚀 [out-456] 외부 API 호출 시작 (from: 상품 목록 조회)
[INFO] 🚀 [out-456] 외부 API 응답 완료 (from: 상품 목록 조회)
[INFO] 🌐 [req-123] 상품 목록 조회 - 요청 완료
3단계: 핵심 기술 선택 🛠️
-
Reflector + Swagger 메타데이터
// 이 부분이 핵심! Swagger 정보를 로그에 활용 const apiOperation = this.reflector.get("swagger/apiOperation", handler); const logName = apiOperation?.summary || "알 수 없는 API"; -
AsyncLocalStorage
// 비동기 체인에서도 Request ID 유지! RequestContextService.run({ requestId, apiContext }, () => { return next.handle(); // 모든 하위 호출에서 컨텍스트 접근 가능 }); -
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로 전체 플로우 한 번에 추적
- 👥 팀 생산성 향상: 신입 개발자도 로그만 보고 문제 파악 가능
- 🛠️ 운영 안정성 증대: 프로덕션 이슈 대응 시간 대폭 단축
💝 핵심 가치
- 🔄 완벽한 요청 추적: 들어오는 요청부터 나가는 API 호출까지 완전 연결
- 🏷️ Swagger 기반 가독성: “상품 등록 현황 조회” 같은 직관적 이름으로 로그 표시
- ⚡ 완전 자동화: 기존 코드 수정 없이 인터셉터만 추가하면 끝
- 🔒 보안 고려: 민감한 데이터 자동 마스킹
- 📊 성능 모니터링: 응답 시간 및 에러 패턴 분석
- 🎯 프로덕션 준비: 실제 서비스에서 바로 사용 가능