Next.js 프로덕션 로깅 전략과 아키텍처 설계




지금까지 6편에 걸쳐 다양한 로깅 라이브러리들을 살펴봤습니다. 하지만 실제 프로덕션 환경에서는 단일 라이브러리만으로는 부족합니다.

이번 포스트에서는 앞서 다룬 모든 라이브러리들을 종합하여 실제 서비스에서 사용할 수 있는 완전한 로깅 아키텍처를 설계해보겠습니다. 수만 명의 사용자를 서비스하는 Next.js 애플리케이션에서 로깅을 어떻게 구축하고 운영해야 하는지 실전 경험을 바탕으로 설명드리겠습니다.

프로덕션 로깅의 핵심 요구사항

1. 성능 영향 최소화

로깅 자체가 애플리케이션 성능에 부정적인 영향을 주어서는 안 됩니다.

주요 고려사항:

  • 로그 생성 오버헤드 최소화
  • 비동기 로그 처리
  • 적절한 로그 레벨 관리
  • 메모리 사용량 제어

2. 확장성과 안정성

서비스 규모가 커져도 로깅 시스템이 안정적으로 동작해야 합니다.

주요 고려사항:

  • 로그 볼륨 증가에 대응
  • 로그 시스템 장애가 메인 서비스에 영향 없음
  • 로그 손실 방지
  • 백업과 복구 전략

3. 운영 효율성

개발팀이 효율적으로 문제를 진단하고 해결할 수 있어야 합니다.

주요 고려사항:

  • 빠른 로그 검색과 필터링
  • 알림과 모니터링
  • 대시보드와 시각화
  • 로그 데이터 분석

하이브리드 로깅 아키텍처 설계

실제 프로덕션에서는 각 라이브러리의 장점을 조합한 하이브리드 접근법이 효과적입니다.

환경별 로깅 전략

// lib/logger-factory.js
import pino from 'pino';
import winston from 'winston';
import { consola } from 'consola';
import { Signale } from 'signale';

export class LoggerFactory {
  static createLogger(context = 'default') {
    const environment = process.env.NODE_ENV || 'development';
    
    switch (environment) {
      case 'development':
        return this.createDevelopmentLogger(context);
      case 'test':
        return this.createTestLogger(context);
      case 'production':
        return this.createProductionLogger(context);
      default:
        return this.createDevelopmentLogger(context);
    }
  }
  
  static createDevelopmentLogger(context) {
    // 개발 환경: 시각적 피드백 중심
    return {
      debug: consola.debug.bind(consola),
      info: consola.info.bind(consola),
      warn: consola.warn.bind(consola),
      error: consola.error.bind(consola),
      success: consola.success.bind(consola),
      
      // CLI 작업용 Signale
      cli: new Signale({ scope: context }),
      
      // 성능 측정용
      time: (label) => console.time(label),
      timeEnd: (label) => console.timeEnd(label)
    };
  }
  
  static createTestLogger() {
    // 테스트 환경: 최소한의 로깅
    const noop = () => {};
    return {
      debug: noop,
      info: noop,
      warn: noop,
      error: noop,
      success: noop,
      cli: { info: noop, success: noop, error: noop },
      time: noop,
      timeEnd: noop
    };
  }
  
  static createProductionLogger(context) {
    // 프로덕션: 성능과 구조화 중심
    const pinoLogger = pino({
      name: 'nextjs-app',
      level: process.env.LOG_LEVEL || 'info',
      formatters: {
        level: (label) => ({ level: label }),
        bindings: (bindings) => ({
          pid: bindings.pid,
          hostname: bindings.hostname,
          context
        })
      }
    });
    
    const winstonLogger = winston.createLogger({
      level: 'info',
      format: winston.format.combine(
        winston.format.timestamp(),
        winston.format.errors({ stack: true }),
        winston.format.json()
      ),
      defaultMeta: { service: 'nextjs-app', context },
      transports: [
        new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
        new winston.transports.File({ filename: 'logs/combined.log' })
      ]
    });
    
    return {
      // 일반 로깅은 Pino (성능)
      debug: pinoLogger.debug.bind(pinoLogger),
      info: pinoLogger.info.bind(pinoLogger),
      warn: pinoLogger.warn.bind(pinoLogger),
      error: pinoLogger.error.bind(pinoLogger),
      
      // 중요한 로그는 Winston으로도 (분석용)
      critical: (message, meta) => {
        pinoLogger.error(meta, message);
        winstonLogger.error(message, meta);
      },
      
      // 성능 측정
      time: (label) => {
        const start = process.hrtime.bigint();
        return () => {
          const duration = Number(process.hrtime.bigint() - start) / 1000000;
          pinoLogger.info({ duration, label }, 'Performance measurement');
          return duration;
        };
      }
    };
  }
}

// 글로벌 로거 인스턴스
export const logger = LoggerFactory.createLogger();

계층별 로깅 전략

1. API 레이어 로깅

// lib/api-logger.js
import { LoggerFactory } from './logger-factory';

export class APILogger {
  constructor(apiName) {
    this.logger = LoggerFactory.createLogger(`api.${apiName}`);
    this.requestCount = 0;
    this.errorCount = 0;
  }
  
  logRequest(req, requestId) {
    this.requestCount++;
    
    const requestData = {
      requestId,
      method: req.method,
      url: req.url,
      userAgent: req.headers['user-agent'],
      ip: this.getClientIP(req),
      timestamp: new Date().toISOString(),
      requestNumber: this.requestCount
    };
    
    this.logger.info('API request started', requestData);
    
    return {
      logResponse: (statusCode, duration, responseSize = 0) => {
        this.logger.info('API request completed', {
          ...requestData,
          statusCode,
          duration: `${duration}ms`,
          responseSize,
          success: statusCode < 400
        });
      },
      
      logError: (error, duration) => {
        this.errorCount++;
        
        this.logger.error('API request failed', {
          ...requestData,
          error: {
            name: error.name,
            message: error.message,
            stack: error.stack
          },
          duration: `${duration}ms`,
          errorNumber: this.errorCount
        });
      }
    };
  }
  
  logStats() {
    this.logger.info('API statistics', {
      totalRequests: this.requestCount,
      totalErrors: this.errorCount,
      errorRate: this.requestCount > 0 ? (this.errorCount / this.requestCount * 100).toFixed(2) + '%' : '0%',
      uptime: process.uptime()
    });
  }
  
  getClientIP(req) {
    return req.headers['x-forwarded-for'] || 
           req.connection.remoteAddress || 
           req.socket.remoteAddress ||
           (req.connection.socket ? req.connection.socket.remoteAddress : null);
  }
}

// API 라우트에서 사용
export function withAPILogging(handler, apiName = 'unknown') {
  const apiLogger = new APILogger(apiName);
  
  return async (req, res) => {
    const requestId = generateRequestId();
    const startTime = Date.now();
    
    const requestLogger = apiLogger.logRequest(req, requestId);
    
    try {
      const result = await handler(req, res);
      const duration = Date.now() - startTime;
      
      requestLogger.logResponse(res.statusCode, duration);
      
      return result;
    } catch (error) {
      const duration = Date.now() - startTime;
      
      requestLogger.logError(error, duration);
      
      // 에러 레벨에 따른 알림 처리
      if (error.statusCode >= 500) {
        await sendCriticalAlert(error, req, requestId);
      }
      
      throw error;
    }
  };
}

function generateRequestId() {
  return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}

2. 비즈니스 로직 레이어 로깅

// lib/business-logger.js
import { LoggerFactory } from './logger-factory';

export class BusinessLogger {
  constructor(domain) {
    this.logger = LoggerFactory.createLogger(`business.${domain}`);
    this.operations = new Map();
  }
  
  startOperation(operationName, context = {}) {
    const operationId = `${operationName}-${Date.now()}-${Math.random().toString(36).substr(2, 6)}`;
    
    const operation = {
      id: operationId,
      name: operationName,
      startTime: Date.now(),
      context,
      steps: []
    };
    
    this.operations.set(operationId, operation);
    
    this.logger.info('Business operation started', {
      operationId,
      operationName,
      context
    });
    
    return new OperationLogger(this.logger, operation, () => {
      this.operations.delete(operationId);
    });
  }
  
  logBusinessEvent(event, data) {
    this.logger.info('Business event', {
      event,
      data,
      timestamp: new Date().toISOString()
    });
  }
  
  logBusinessMetric(metricName, value, tags = {}) {
    this.logger.info('Business metric', {
      metric: metricName,
      value,
      tags,
      timestamp: new Date().toISOString()
    });
  }
}

class OperationLogger {
  constructor(logger, operation, cleanup) {
    this.logger = logger;
    this.operation = operation;
    this.cleanup = cleanup;
  }
  
  step(stepName, data = {}) {
    this.operation.steps.push({
      name: stepName,
      timestamp: Date.now(),
      data
    });
    
    this.logger.debug('Operation step', {
      operationId: this.operation.id,
      stepName,
      data
    });
  }
  
  success(result = {}) {
    const duration = Date.now() - this.operation.startTime;
    
    this.logger.info('Business operation completed', {
      operationId: this.operation.id,
      operationName: this.operation.name,
      duration: `${duration}ms`,
      steps: this.operation.steps.length,
      result
    });
    
    this.cleanup();
  }
  
  failure(error, partialResult = {}) {
    const duration = Date.now() - this.operation.startTime;
    
    this.logger.error('Business operation failed', {
      operationId: this.operation.id,
      operationName: this.operation.name,
      duration: `${duration}ms`,
      steps: this.operation.steps.length,
      error: {
        name: error.name,
        message: error.message,
        stack: error.stack
      },
      partialResult,
      context: this.operation.context
    });
    
    this.cleanup();
  }
}

// 사용 예시
const userLogger = new BusinessLogger('user');

export async function registerUser(userData) {
  const operation = userLogger.startOperation('user-registration', { 
    email: userData.email 
  });
  
  try {
    operation.step('validation', { fields: Object.keys(userData) });
    await validateUserData(userData);
    
    operation.step('duplicate-check');
    const existingUser = await User.findByEmail(userData.email);
    if (existingUser) {
      throw new Error('Email already exists');
    }
    
    operation.step('create-user');
    const user = await User.create(userData);
    
    operation.step('send-welcome-email');
    await EmailService.sendWelcomeEmail(user);
    
    userLogger.logBusinessEvent('user-registered', {
      userId: user.id,
      email: user.email,
      registrationMethod: 'email'
    });
    
    operation.success({ userId: user.id });
    
    return user;
  } catch (error) {
    operation.failure(error);
    throw error;
  }
}

3. 인프라 레이어 로깅

// lib/infrastructure-logger.js
import { LoggerFactory } from './logger-factory';

export class InfrastructureLogger {
  constructor() {
    this.logger = LoggerFactory.createLogger('infrastructure');
    this.healthChecks = new Map();
    this.startSystemMonitoring();
  }
  
  logDatabaseOperation(operation, query, params, duration, error = null) {
    const logData = {
      operation,
      query: query.replace(/\s+/g, ' ').trim(),
      params: this.sanitizeParams(params),
      duration: `${duration}ms`,
      timestamp: new Date().toISOString()
    };
    
    if (error) {
      this.logger.error('Database operation failed', {
        ...logData,
        error: {
          message: error.message,
          code: error.code,
          detail: error.detail
        }
      });
    } else {
      if (duration > 1000) {
        this.logger.warn('Slow database operation', logData);
      } else {
        this.logger.debug('Database operation completed', logData);
      }
    }
  }
  
  logCacheOperation(operation, key, hit, duration) {
    this.logger.debug('Cache operation', {
      operation,
      key: this.sanitizeKey(key),
      hit,
      duration: `${duration}ms`,
      timestamp: new Date().toISOString()
    });
  }
  
  logExternalAPICall(service, endpoint, method, statusCode, duration, error = null) {
    const logData = {
      service,
      endpoint,
      method,
      statusCode,
      duration: `${duration}ms`,
      timestamp: new Date().toISOString()
    };
    
    if (error || statusCode >= 400) {
      this.logger.error('External API call failed', {
        ...logData,
        error: error ? error.message : `HTTP ${statusCode}`
      });
    } else {
      this.logger.info('External API call completed', logData);
    }
  }
  
  startSystemMonitoring() {
    // 시스템 메트릭 수집
    setInterval(() => {
      const memUsage = process.memoryUsage();
      const cpuUsage = process.cpuUsage();
      
      this.logger.info('System metrics', {
        memory: {
          rss: Math.round(memUsage.rss / 1024 / 1024),
          heapTotal: Math.round(memUsage.heapTotal / 1024 / 1024),
          heapUsed: Math.round(memUsage.heapUsed / 1024 / 1024),
          external: Math.round(memUsage.external / 1024 / 1024)
        },
        cpu: {
          user: cpuUsage.user,
          system: cpuUsage.system
        },
        uptime: Math.round(process.uptime()),
        timestamp: new Date().toISOString()
      });
    }, 60000); // 1분마다
  }
  
  registerHealthCheck(name, checkFunction) {
    this.healthChecks.set(name, checkFunction);
  }
  
  async runHealthChecks() {
    const results = {};
    
    for (const [name, checkFunction] of this.healthChecks) {
      try {
        const start = Date.now();
        const result = await checkFunction();
        const duration = Date.now() - start;
        
        results[name] = {
          status: 'healthy',
          duration: `${duration}ms`,
          details: result
        };
        
        this.logger.debug('Health check passed', { 
          service: name, 
          duration: `${duration}ms` 
        });
      } catch (error) {
        results[name] = {
          status: 'unhealthy',
          error: error.message
        };
        
        this.logger.error('Health check failed', {
          service: name,
          error: error.message
        });
      }
    }
    
    const overallHealth = Object.values(results).every(r => r.status === 'healthy');
    
    this.logger.info('Health check summary', {
      overall: overallHealth ? 'healthy' : 'unhealthy',
      services: results
    });
    
    return { overall: overallHealth, services: results };
  }
  
  sanitizeParams(params) {
    if (!params) return params;
    
    // 민감한 정보 마스킹
    const sensitiveFields = ['password', 'token', 'secret', 'key'];
    
    if (Array.isArray(params)) {
      return params.map(param => 
        typeof param === 'object' ? this.sanitizeObject(param) : param
      );
    }
    
    return this.sanitizeObject(params);
  }
  
  sanitizeObject(obj) {
    if (!obj || typeof obj !== 'object') return obj;
    
    const sanitized = { ...obj };
    const sensitiveFields = ['password', 'token', 'secret', 'key'];
    
    Object.keys(sanitized).forEach(key => {
      if (sensitiveFields.some(field => key.toLowerCase().includes(field))) {
        sanitized[key] = '[REDACTED]';
      }
    });
    
    return sanitized;
  }
  
  sanitizeKey(key) {
    // 캐시 키에서 민감한 정보 제거
    return key.replace(/token[=:][^&\s]+/gi, 'token=***');
  }
}

// 글로벌 인스턴스
export const infraLogger = new InfrastructureLogger();

// 데이터베이스 래퍼 예시
export async function executeQuery(query, params = []) {
  const start = Date.now();
  
  try {
    const result = await db.query(query, params);
    const duration = Date.now() - start;
    
    infraLogger.logDatabaseOperation('SELECT', query, params, duration);
    
    return result;
  } catch (error) {
    const duration = Date.now() - start;
    infraLogger.logDatabaseOperation('SELECT', query, params, duration, error);
    throw error;
  }
}

로그 수집과 분석 시스템

1. ELK 스택 통합

// lib/elk-integration.js
import { LoggerFactory } from './logger-factory';

export class ELKIntegration {
  constructor() {
    this.logger = LoggerFactory.createLogger('elk');
    this.buffer = [];
    this.bufferSize = 100;
    this.flushInterval = 5000; // 5초
    
    this.startPeriodicFlush();
    this.setupProcessHandlers();
  }
  
  logToELK(level, message, metadata = {}) {
    const logEntry = {
      '@timestamp': new Date().toISOString(),
      level,
      message,
      service: 'nextjs-app',
      environment: process.env.NODE_ENV || 'development',
      version: process.env.APP_VERSION || '1.0.0',
      hostname: require('os').hostname(),
      pid: process.pid,
      ...metadata
    };
    
    this.buffer.push(logEntry);
    
    // 버퍼가 가득 차면 즉시 플러시
    if (this.buffer.length >= this.bufferSize) {
      this.flush();
    }
  }
  
  async flush() {
    if (this.buffer.length === 0) return;
    
    const logs = [...this.buffer];
    this.buffer = [];
    
    try {
      await this.sendToElasticsearch(logs);
      this.logger.debug(`Flushed ${logs.length} logs to Elasticsearch`);
    } catch (error) {
      this.logger.error('Failed to send logs to Elasticsearch', {
        error: error.message,
        logCount: logs.length
      });
      
      // 실패한 로그를 다시 버퍼에 추가 (재시도)
      this.buffer.unshift(...logs.slice(0, 50)); // 최대 50개만 재시도
    }
  }
  
  async sendToElasticsearch(logs) {
    const bulkBody = logs.flatMap(log => [
      { index: { _index: `nextjs-logs-${new Date().toISOString().slice(0, 7)}` } },
      log
    ]);
    
    const response = await fetch(`${process.env.ELASTICSEARCH_URL}/_bulk`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${process.env.ELASTICSEARCH_TOKEN}`
      },
      body: bulkBody.map(item => JSON.stringify(item)).join('\n') + '\n'
    });
    
    if (!response.ok) {
      throw new Error(`Elasticsearch responded with ${response.status}: ${response.statusText}`);
    }
    
    const result = await response.json();
    
    if (result.errors) {
      this.logger.warn('Some logs failed to index', {
        errors: result.items.filter(item => item.index.error).length
      });
    }
  }
  
  startPeriodicFlush() {
    this.flushTimer = setInterval(() => {
      this.flush();
    }, this.flushInterval);
  }
  
  setupProcessHandlers() {
    // 프로세스 종료 시 남은 로그 플러시
    const gracefulShutdown = async () => {
      clearInterval(this.flushTimer);
      await this.flush();
      process.exit(0);
    };
    
    process.on('SIGINT', gracefulShutdown);
    process.on('SIGTERM', gracefulShutdown);
    process.on('exit', () => {
      clearInterval(this.flushTimer);
    });
  }
}

// 글로벌 ELK 통합 인스턴스
export const elkIntegration = process.env.NODE_ENV === 'production' 
  ? new ELKIntegration() 
  : null;

// Winston Transport로 ELK 통합
import winston from 'winston';

export class ElasticsearchTransport extends winston.Transport {
  constructor(options = {}) {
    super(options);
    this.elk = elkIntegration;
  }
  
  log(info, callback) {
    if (this.elk) {
      this.elk.logToELK(info.level, info.message, info);
    }
    
    callback();
  }
}

2. 실시간 모니터링과 알림

// lib/monitoring.js
import { LoggerFactory } from './logger-factory';

export class MonitoringSystem {
  constructor() {
    this.logger = LoggerFactory.createLogger('monitoring');
    this.alerts = new Map();
    this.metrics = new Map();
    this.thresholds = {
      errorRate: 5, // 5% 이상 에러율
      responseTime: 2000, // 2초 이상 응답시간
      memoryUsage: 80, // 80% 이상 메모리 사용률
      diskUsage: 90 // 90% 이상 디스크 사용률
    };
    
    this.startMonitoring();
  }
  
  recordMetric(name, value, tags = {}) {
    const timestamp = Date.now();
    
    if (!this.metrics.has(name)) {
      this.metrics.set(name, []);
    }
    
    const metricData = this.metrics.get(name);
    metricData.push({ value, timestamp, tags });
    
    // 최근 1시간 데이터만 유지
    const oneHourAgo = timestamp - (60 * 60 * 1000);
    this.metrics.set(name, metricData.filter(d => d.timestamp > oneHourAgo));
    
    // 임계값 체크
    this.checkThresholds(name, value, tags);
  }
  
  checkThresholds(metricName, value, tags) {
    const threshold = this.thresholds[metricName];
    if (!threshold) return;
    
    if (value > threshold) {
      this.triggerAlert(metricName, value, threshold, tags);
    }
  }
  
  triggerAlert(metricName, currentValue, threshold, tags) {
    const alertKey = `${metricName}_${JSON.stringify(tags)}`;
    const now = Date.now();
    
    // 중복 알림 방지 (5분 내 같은 알림 무시)
    const lastAlert = this.alerts.get(alertKey);
    if (lastAlert && (now - lastAlert) < 300000) return;
    
    this.alerts.set(alertKey, now);
    
    const alert = {
      metric: metricName,
      currentValue,
      threshold,
      tags,
      timestamp: new Date().toISOString(),
      severity: this.calculateSeverity(metricName, currentValue, threshold)
    };
    
    this.logger.error('Threshold exceeded', alert);
    
    // 외부 알림 서비스로 전송
    this.sendAlert(alert);
  }
  
  calculateSeverity(metricName, value, threshold) {
    const ratio = value / threshold;
    
    if (ratio > 2) return 'critical';
    if (ratio > 1.5) return 'high';
    if (ratio > 1.2) return 'medium';
    return 'low';
  }
  
  async sendAlert(alert) {
    try {
      // Slack 알림
      if (process.env.SLACK_WEBHOOK_URL) {
        await this.sendSlackAlert(alert);
      }
      
      // PagerDuty 알림 (심각한 경우)
      if (alert.severity === 'critical' && process.env.PAGERDUTY_INTEGRATION_KEY) {
        await this.sendPagerDutyAlert(alert);
      }
      
      // 이메일 알림
      if (alert.severity !== 'low' && process.env.ALERT_EMAIL) {
        await this.sendEmailAlert(alert);
      }
      
    } catch (error) {
      this.logger.error('Failed to send alert', {
        alert,
        error: error.message
      });
    }
  }
  
  async sendSlackAlert(alert) {
    const color = {
      low: 'warning',
      medium: 'warning', 
      high: 'danger',
      critical: 'danger'
    }[alert.severity];
    
    const emoji = {
      low: '⚠️',
      medium: '🔥',
      high: '🚨',
      critical: '💥'
    }[alert.severity];
    
    const payload = {
      text: `${emoji} Production Alert: ${alert.metric}`,
      attachments: [{
        color,
        fields: [
          { title: 'Metric', value: alert.metric, short: true },
          { title: 'Current Value', value: alert.currentValue, short: true },
          { title: 'Threshold', value: alert.threshold, short: true },
          { title: 'Severity', value: alert.severity.toUpperCase(), short: true },
          { title: 'Tags', value: JSON.stringify(alert.tags), short: false },
          { title: 'Timestamp', value: alert.timestamp, short: false }
        ]
      }]
    };
    
    await fetch(process.env.SLACK_WEBHOOK_URL, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(payload)
    });
  }
  
  getMetricSummary(metricName, duration = 3600000) { // 기본 1시간
    const metricData = this.metrics.get(metricName) || [];
    const cutoff = Date.now() - duration;
    const recentData = metricData.filter(d => d.timestamp > cutoff);
    
    if (recentData.length === 0) return null;
    
    const values = recentData.map(d => d.value);
    
    return {
      count: values.length,
      min: Math.min(...values),
      max: Math.max(...values),
      avg: values.reduce((a, b) => a + b, 0) / values.length,
      median: this.calculateMedian(values),
      p95: this.calculatePercentile(values, 95),
      p99: this.calculatePercentile(values, 99)
    };
  }
  
  calculateMedian(values) {
    const sorted = [...values].sort((a, b) => a - b);
    const mid = Math.floor(sorted.length / 2);
    
    return sorted.length % 2 === 0
      ? (sorted[mid - 1] + sorted[mid]) / 2
      : sorted[mid];
  }
  
  calculatePercentile(values, percentile) {
    const sorted = [...values].sort((a, b) => a - b);
    const index = Math.ceil((percentile / 100) * sorted.length) - 1;
    return sorted[Math.max(0, index)];
  }
  
  startMonitoring() {
    // 시스템 메트릭 수집
    setInterval(() => {
      const memUsage = process.memoryUsage();
      const memPercent = (memUsage.heapUsed / memUsage.heapTotal) * 100;
      
      this.recordMetric('memoryUsage', memPercent);
      this.recordMetric('heapSize', memUsage.heapTotal / 1024 / 1024); // MB
      
    }, 30000); // 30초마다
    
    // 프로세스 상태 모니터링
    setInterval(() => {
      this.recordMetric('uptime', process.uptime());
      this.recordMetric('processCount', 1); // 단일 프로세스의 경우
      
    }, 60000); // 1분마다
  }
}

// 글로벌 모니터링 인스턴스
export const monitoring = new MonitoringSystem();

// Express/Next.js 미들웨어
export function withMonitoring(handler) {
  return async (req, res) => {
    const start = Date.now();
    
    try {
      const result = await handler(req, res);
      
      const duration = Date.now() - start;
      monitoring.recordMetric('responseTime', duration, {
        method: req.method,
        endpoint: req.url,
        status: res.statusCode
      });
      
      // 성공률 기록
      monitoring.recordMetric('requestSuccess', 1, {
        endpoint: req.url
      });
      
      return result;
    } catch (error) {
      const duration = Date.now() - start;
      
      monitoring.recordMetric('responseTime', duration, {
        method: req.method,
        endpoint: req.url,
        status: 500
      });
      
      // 에러율 기록
      monitoring.recordMetric('requestError', 1, {
        endpoint: req.url,
        errorType: error.name
      });
      
      throw error;
    }
  };
}

3. 로그 분석과 인사이트

// lib/log-analytics.js
import { LoggerFactory } from './logger-factory';

export class LogAnalytics {
  constructor() {
    this.logger = LoggerFactory.createLogger('analytics');
    this.patterns = new Map();
    this.anomalies = [];
    this.insights = [];
  }
  
  analyzeErrorPatterns(logs) {
    const errorLogs = logs.filter(log => log.level === 'error');
    const patterns = new Map();
    
    errorLogs.forEach(log => {
      // 에러 타입별 패턴 분석
      const errorType = log.error?.name || 'Unknown';
      const key = `${errorType}_${log.context || 'global'}`;
      
      if (!patterns.has(key)) {
        patterns.set(key, {
          errorType,
          context: log.context,
          count: 0,
          firstSeen: log.timestamp,
          lastSeen: log.timestamp,
          samples: []
        });
      }
      
      const pattern = patterns.get(key);
      pattern.count++;
      pattern.lastSeen = log.timestamp;
      
      if (pattern.samples.length < 5) {
        pattern.samples.push({
          message: log.message,
          timestamp: log.timestamp,
          stack: log.error?.stack
        });
      }
    });
    
    // 주요 에러 패턴 식별
    const significantPatterns = Array.from(patterns.values())
      .filter(p => p.count >= 5) // 5회 이상 발생
      .sort((a, b) => b.count - a.count);
    
    this.logger.info('Error pattern analysis completed', {
      totalErrors: errorLogs.length,
      uniquePatterns: patterns.size,
      significantPatterns: significantPatterns.length
    });
    
    return significantPatterns;
  }
  
  detectAnomalies(metrics) {
    const anomalies = [];
    
    for (const [metricName, data] of metrics.entries()) {
      const anomaly = this.detectMetricAnomalies(metricName, data);
      if (anomaly) {
        anomalies.push(anomaly);
      }
    }
    
    this.anomalies.push(...anomalies);
    
    return anomalies;
  }
  
  detectMetricAnomalies(metricName, data) {
    if (data.length < 10) return null; // 충분한 데이터 필요
    
    const values = data.map(d => d.value);
    const mean = values.reduce((a, b) => a + b, 0) / values.length;
    const variance = values.reduce((a, b) => a + Math.pow(b - mean, 2), 0) / values.length;
    const stdDev = Math.sqrt(variance);
    
    // Z-score를 사용한 이상값 감지
    const threshold = 2.5; // 2.5 표준편차 이상
    const recentValue = values[values.length - 1];
    const zScore = Math.abs((recentValue - mean) / stdDev);
    
    if (zScore > threshold) {
      return {
        metric: metricName,
        value: recentValue,
        expectedRange: [mean - threshold * stdDev, mean + threshold * stdDev],
        zScore,
        timestamp: new Date().toISOString(),
        severity: zScore > 3 ? 'high' : 'medium'
      };
    }
    
    return null;
  }
  
  generateInsights(logs, timeframe = 3600000) { // 1시간
    const insights = [];
    const cutoff = Date.now() - timeframe;
    const recentLogs = logs.filter(log => new Date(log.timestamp).getTime() > cutoff);
    
    // 성능 인사이트
    const performanceInsight = this.analyzePerformance(recentLogs);
    if (performanceInsight) insights.push(performanceInsight);
    
    // 에러 트렌드 인사이트
    const errorTrendInsight = this.analyzeErrorTrends(recentLogs);
    if (errorTrendInsight) insights.push(errorTrendInsight);
    
    // 사용자 행동 인사이트
    const userBehaviorInsight = this.analyzeUserBehavior(recentLogs);
    if (userBehaviorInsight) insights.push(userBehaviorInsight);
    
    this.insights.push(...insights);
    
    return insights;
  }
  
  analyzePerformance(logs) {
    const apiLogs = logs.filter(log => log.context?.startsWith('api.'));
    if (apiLogs.length === 0) return null;
    
    const responseTimes = apiLogs
      .filter(log => log.duration)
      .map(log => parseInt(log.duration));
    
    if (responseTimes.length === 0) return null;
    
    const avgResponseTime = responseTimes.reduce((a, b) => a + b, 0) / responseTimes.length;
    const slowRequests = responseTimes.filter(time => time > 2000).length;
    const slowRequestRate = (slowRequests / responseTimes.length) * 100;
    
    if (slowRequestRate > 10) { // 10% 이상이 느린 요청
      return {
        type: 'performance',
        title: 'High Response Time Detected',
        description: `${slowRequestRate.toFixed(1)}% of API requests are slower than 2 seconds`,
        data: {
          avgResponseTime: Math.round(avgResponseTime),
          slowRequestRate: slowRequestRate.toFixed(1),
          totalRequests: responseTimes.length
        },
        severity: slowRequestRate > 25 ? 'high' : 'medium',
        recommendations: [
          'Check database query performance',
          'Review external API dependencies',
          'Consider implementing caching',
          'Optimize heavy computations'
        ]
      };
    }
    
    return null;
  }
  
  analyzeErrorTrends(logs) {
    const errorLogs = logs.filter(log => log.level === 'error');
    if (errorLogs.length === 0) return null;
    
    // 시간대별 에러 분포 분석
    const hourlyErrors = new Map();
    errorLogs.forEach(log => {
      const hour = new Date(log.timestamp).getHours();
      hourlyErrors.set(hour, (hourlyErrors.get(hour) || 0) + 1);
    });
    
    const peakErrorHour = Array.from(hourlyErrors.entries())
      .sort((a, b) => b[1] - a[1])[0];
    
    if (peakErrorHour && peakErrorHour[1] > errorLogs.length * 0.3) {
      return {
        type: 'error_trend',
        title: 'Error Spike Detected',
        description: `Unusually high error rate at ${peakErrorHour[0]}:00`,
        data: {
          peakHour: peakErrorHour[0],
          errorCount: peakErrorHour[1],
          totalErrors: errorLogs.length,
          errorRate: ((peakErrorHour[1] / errorLogs.length) * 100).toFixed(1)
        },
        severity: 'medium',
        recommendations: [
          'Check system load during peak hours',
          'Review scheduled jobs and cron tasks',
          'Monitor external service availability',
          'Consider load balancing adjustments'
        ]
      };
    }
    
    return null;
  }
  
  generateDailyReport() {
    const report = {
      date: new Date().toISOString().split('T')[0],
      summary: {
        totalLogs: this.getTotalLogCount(),
        errorCount: this.getErrorCount(),
        anomalies: this.anomalies.length,
        insights: this.insights.length
      },
      topErrors: this.getTopErrors(),
      performanceMetrics: this.getPerformanceMetrics(),
      insights: this.insights.slice(-10), // 최근 10개 인사이트
      recommendations: this.generateRecommendations()
    };
    
    this.logger.info('Daily report generated', report);
    
    return report;
  }
  
  generateRecommendations() {
    const recommendations = [];
    
    // 에러율 기반 추천
    const errorRate = this.getErrorRate();
    if (errorRate > 1) {
      recommendations.push({
        category: 'reliability',
        priority: 'high',
        title: 'Reduce Error Rate',
        description: `Current error rate is ${errorRate.toFixed(2)}%, which is above the recommended threshold of 1%`,
        actions: [
          'Implement better error handling',
          'Add input validation',
          'Improve external service integration',
          'Set up proactive monitoring alerts'
        ]
      });
    }
    
    // 성능 기반 추천
    const avgResponseTime = this.getAverageResponseTime();
    if (avgResponseTime > 1000) {
      recommendations.push({
        category: 'performance',
        priority: 'medium',
        title: 'Optimize Response Time',
        description: `Average response time is ${avgResponseTime}ms, consider optimization`,
        actions: [
          'Implement database query optimization',
          'Add Redis caching layer',
          'Use CDN for static assets',
          'Consider code splitting and lazy loading'
        ]
      });
    }
    
    return recommendations;
  }
  
  // 헬퍼 메서드들
  getTotalLogCount() {
    return this.patterns.size || 0;
  }
  
  getErrorCount() {
    return this.anomalies.filter(a => a.severity === 'high').length;
  }
  
  getTopErrors() {
    return Array.from(this.patterns.values())
      .sort((a, b) => b.count - a.count)
      .slice(0, 5);
  }
  
  getPerformanceMetrics() {
    return {
      avgResponseTime: this.getAverageResponseTime(),
      errorRate: this.getErrorRate(),
      uptime: process.uptime()
    };
  }
  
  getErrorRate() {
    // 실제 구현에서는 메트릭 데이터에서 계산
    return Math.random() * 2; // 임시 값
  }
  
  getAverageResponseTime() {
    // 실제 구현에서는 메트릭 데이터에서 계산
    return Math.random() * 2000; // 임시 값
  }
}

// 글로벌 분석 인스턴스
export const analytics = new LogAnalytics();

운영 자동화

1. 로그 로테이션과 아카이빙

// lib/log-rotation.js
import fs from 'fs-extra';
import path from 'path';
import { createGzip } from 'zlib';
import { pipeline } from 'stream/promises';

export class LogRotationManager {
  constructor(options = {}) {
    this.logDir = options.logDir || './logs';
    this.maxFileSize = options.maxFileSize || 100 * 1024 * 1024; // 100MB
    this.maxFiles = options.maxFiles || 10;
    this.compressOldLogs = options.compressOldLogs !== false;
    this.archiveOldLogs = options.archiveOldLogs || false;
    this.archiveDir = options.archiveDir || './logs/archive';
    
    this.startRotationSchedule();
  }
  
  async rotateLogs() {
    const logFiles = await this.getLogFiles();
    
    for (const file of logFiles) {
      await this.rotateLogFile(file);
    }
  }
  
  async getLogFiles() {
    try {
      const files = await fs.readdir(this.logDir);
      return files
        .filter(file => file.endsWith('.log'))
        .map(file => path.join(this.logDir, file));
    } catch (error) {
      console.error('Failed to read log directory:', error);
      return [];
    }
  }
  
  async rotateLogFile(filePath) {
    try {
      const stats = await fs.stat(filePath);
      
      if (stats.size > this.maxFileSize) {
        await this.performRotation(filePath);
      }
    } catch (error) {
      console.error(`Failed to rotate log file ${filePath}:`, error);
    }
  }
  
  async performRotation(filePath) {
    const fileName = path.basename(filePath, '.log');
    const dir = path.dirname(filePath);
    const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
    
    // 기존 파일 백업
    const backupPath = path.join(dir, `${fileName}-${timestamp}.log`);
    await fs.move(filePath, backupPath);
    
    // 새 로그 파일 생성
    await fs.writeFile(filePath, '');
    
    // 압축
    if (this.compressOldLogs) {
      await this.compressFile(backupPath);
    }
    
    // 아카이브
    if (this.archiveOldLogs) {
      await this.archiveFile(backupPath);
    }
    
    // 오래된 파일 정리
    await this.cleanupOldFiles(dir, fileName);
  }
  
  async compressFile(filePath) {
    const compressedPath = `${filePath}.gz`;
    
    try {
      await pipeline(
        fs.createReadStream(filePath),
        createGzip(),
        fs.createWriteStream(compressedPath)
      );
      
      await fs.remove(filePath);
      console.log(`Compressed ${filePath} to ${compressedPath}`);
    } catch (error) {
      console.error(`Failed to compress ${filePath}:`, error);
    }
  }
  
  async archiveFile(filePath) {
    try {
      await fs.ensureDir(this.archiveDir);
      const fileName = path.basename(filePath);
      const archivePath = path.join(this.archiveDir, fileName);
      
      await fs.move(filePath, archivePath);
      console.log(`Archived ${filePath} to ${archivePath}`);
    } catch (error) {
      console.error(`Failed to archive ${filePath}:`, error);
    }
  }
  
  async cleanupOldFiles(dir, baseName) {
    try {
      const files = await fs.readdir(dir);
      const logFiles = files
        .filter(file => file.startsWith(`${baseName}-`) && (file.endsWith('.log') || file.endsWith('.log.gz')))
        .map(file => ({
          name: file,
          path: path.join(dir, file),
          stats: null
        }));
      
      // 파일 정보 수집
      for (const file of logFiles) {
        try {
          file.stats = await fs.stat(file.path);
        } catch (error) {
          console.error(`Failed to stat ${file.path}:`, error);
        }
      }
      
      // 수정 시간순 정렬 (최신순)
      const validFiles = logFiles
        .filter(file => file.stats)
        .sort((a, b) => b.stats.mtime - a.stats.mtime);
      
      // 최대 파일 수를 초과하는 파일들 삭제
      const filesToDelete = validFiles.slice(this.maxFiles);
      
      for (const file of filesToDelete) {
        await fs.remove(file.path);
        console.log(`Deleted old log file: ${file.path}`);
      }
    } catch (error) {
      console.error('Failed to cleanup old files:', error);
    }
  }
  
  startRotationSchedule() {
    // 매일 자정에 로그 로테이션 실행
    const now = new Date();
    const tomorrow = new Date(now);
    tomorrow.setDate(tomorrow.getDate() + 1);
    tomorrow.setHours(0, 0, 0, 0);
    
    const msUntilMidnight = tomorrow.getTime() - now.getTime();
    
    setTimeout(() => {
      this.rotateLogs();
      
      // 이후 24시간마다 실행
      setInterval(() => {
        this.rotateLogs();
      }, 24 * 60 * 60 * 1000);
    }, msUntilMidnight);
  }
}

// 사용법
export const logRotation = new LogRotationManager({
  logDir: './logs',
  maxFileSize: 50 * 1024 * 1024, // 50MB
  maxFiles: 7, // 7일치 보관
  compressOldLogs: true,
  archiveOldLogs: process.env.NODE_ENV === 'production'
});

2. 자동 복구 시스템

// lib/auto-recovery.js
import { LoggerFactory } from './logger-factory';
import { monitoring } from './monitoring';

export class AutoRecoverySystem {
  constructor() {
    this.logger = LoggerFactory.createLogger('auto-recovery');
    this.recoveryStrategies = new Map();
    this.recoveryHistory = [];
    this.maxRecoveryAttempts = 3;
    this.cooldownPeriod = 300000; // 5분
    
    this.setupDefaultStrategies();
    this.startMonitoring();
  }
  
  setupDefaultStrategies() {
    // 메모리 사용량 과다 시 복구
    this.registerStrategy('highMemoryUsage', {
      condition: (metrics) => metrics.memoryUsage > 90,
      actions: [
        this.forceGarbageCollection,
        this.clearInternalCaches,
        this.restartWorkerProcesses
      ],
      cooldown: 600000 // 10분
    });
    
    // 높은 에러율 시 복구
    this.registerStrategy('highErrorRate', {
      condition: (metrics) => metrics.errorRate > 10,
      actions: [
        this.enableCircuitBreaker,
        this.switchToBackupServices,
        this.activateMaintenanceMode
      ],
      cooldown: 300000 // 5분
    });
    
    // 응답 시간 과다 시 복구
    this.registerStrategy('slowResponseTime', {
      condition: (metrics) => metrics.avgResponseTime > 5000,
      actions: [
        this.optimizeConnections,
        this.enableAggressive Caching,
        this.scaleUpResources
      ],
      cooldown: 600000 // 10분
    });
  }
  
  registerStrategy(name, strategy) {
    this.recoveryStrategies.set(name, {
      ...strategy,
      lastExecuted: 0,
      executionCount: 0
    });
  }
  
  async checkAndRecover() {
    const currentMetrics = await this.getCurrentMetrics();
    
    for (const [strategyName, strategy] of this.recoveryStrategies) {
      if (await this.shouldExecuteStrategy(strategyName, strategy, currentMetrics)) {
        await this.executeRecoveryStrategy(strategyName, strategy);
      }
    }
  }
  
  async shouldExecuteStrategy(name, strategy, metrics) {
    // 조건 체크
    if (!strategy.condition(metrics)) return false;
    
    // 쿨다운 체크
    const timeSinceLastExecution = Date.now() - strategy.lastExecuted;
    if (timeSinceLastExecution < strategy.cooldown) return false;
    
    // 최대 시도 횟수 체크
    const recentAttempts = this.recoveryHistory
      .filter(h => h.strategy === name && Date.now() - h.timestamp < 3600000) // 1시간 내
      .length;
    
    return recentAttempts < this.maxRecoveryAttempts;
  }
  
  async executeRecoveryStrategy(strategyName, strategy) {
    this.logger.warn(`Executing recovery strategy: ${strategyName}`);
    
    const recoveryRecord = {
      strategy: strategyName,
      timestamp: Date.now(),
      success: false,
      actions: [],
      error: null
    };
    
    try {
      for (const action of strategy.actions) {
        const actionName = action.name;
        
        this.logger.info(`Executing recovery action: ${actionName}`);
        
        const actionStart = Date.now();
        await action();
        const actionDuration = Date.now() - actionStart;
        
        recoveryRecord.actions.push({
          name: actionName,
          duration: actionDuration,
          success: true
        });
        
        this.logger.success(`Recovery action completed: ${actionName} (${actionDuration}ms)`);
        
        // 각 액션 후 잠시 대기
        await this.sleep(5000);
      }
      
      recoveryRecord.success = true;
      this.logger.success(`Recovery strategy completed successfully: ${strategyName}`);
      
    } catch (error) {
      recoveryRecord.error = error.message;
      this.logger.error(`Recovery strategy failed: ${strategyName}`, {
        error: error.message,
        stack: error.stack
      });
      
      // 실패 시 알림
      await this.sendRecoveryFailureAlert(strategyName, error);
    } finally {
      strategy.lastExecuted = Date.now();
      strategy.executionCount++;
      this.recoveryHistory.push(recoveryRecord);
      
      // 기록 정리 (최근 24시간 것만 유지)
      const dayAgo = Date.now() - 86400000;
      this.recoveryHistory = this.recoveryHistory.filter(r => r.timestamp > dayAgo);
    }
  }
  
  // 복구 액션들
  async forceGarbageCollection() {
    if (global.gc) {
      global.gc();
      this.logger.info('Forced garbage collection completed');
    } else {
      this.logger.warn('Garbage collection not available (run with --expose-gc)');
    }
  }
  
  async clearInternalCaches() {
    // 애플리케이션별 캐시 정리 로직
    if (global.appCache) {
      global.appCache.clear();
      this.logger.info('Internal caches cleared');
    }
  }
  
  async restartWorkerProcesses() {
    // 워커 프로세스 재시작 로직 (cluster 사용 시)
    if (process.send) {
      process.send({ cmd: 'restart_workers' });
      this.logger.info('Worker process restart requested');
    }
  }
  
  async enableCircuitBreaker() {
    // 서킷 브레이커 활성화
    global.circuitBreakerEnabled = true;
    this.logger.info('Circuit breaker enabled');
  }
  
  async switchToBackupServices() {
    // 백업 서비스로 전환
    global.useBackupServices = true;
    this.logger.info('Switched to backup services');
  }
  
  async activateMaintenanceMode() {
    // 유지보수 모드 활성화
    global.maintenanceMode = true;
    this.logger.warn('Maintenance mode activated');
  }
  
  async optimizeConnections() {
    // 연결 최적화 (연결 풀 재설정 등)
    this.logger.info('Connection optimization completed');
  }
  
  async enableAggressiveCaching() {
    // 적극적 캐싱 활성화
    global.aggressiveCaching = true;
    this.logger.info('Aggressive caching enabled');
  }
  
  async scaleUpResources() {
    // 리소스 스케일업 (클라우드 환경에서)
    this.logger.info('Resource scale-up requested');
  }
  
  async getCurrentMetrics() {
    const memUsage = process.memoryUsage();
    const memPercent = (memUsage.heapUsed / memUsage.heapTotal) * 100;
    
    return {
      memoryUsage: memPercent,
      errorRate: monitoring.getMetricSummary('requestError')?.avg || 0,
      avgResponseTime: monitoring.getMetricSummary('responseTime')?.avg || 0,
      uptime: process.uptime()
    };
  }
  
  async sendRecoveryFailureAlert(strategyName, error) {
    // 복구 실패 알림 (Slack, 이메일 등)
    this.logger.critical('Auto-recovery failed', {
      strategy: strategyName,
      error: error.message,
      timestamp: new Date().toISOString()
    });
  }
  
  sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
  
  startMonitoring() {
    // 5분마다 복구 체크 실행
    setInterval(() => {
      this.checkAndRecover().catch(error => {
        this.logger.error('Recovery check failed', {
          error: error.message,
          stack: error.stack
        });
      });
    }, 300000);
  }
  
  getRecoveryReport() {
    const now = Date.now();
    const dayAgo = now - 86400000;
    const recentRecoveries = this.recoveryHistory.filter(r => r.timestamp > dayAgo);
    
    const successRate = recentRecoveries.length > 0 
      ? (recentRecoveries.filter(r => r.success).length / recentRecoveries.length * 100).toFixed(1)
      : '100';
    
    return {
      period: '24 hours',
      totalRecoveries: recentRecoveries.length,
      successfulRecoveries: recentRecoveries.filter(r => r.success).length,
      failedRecoveries: recentRecoveries.filter(r => !r.success).length,
      successRate: `${successRate}%`,
      strategiesUsed: [...new Set(recentRecoveries.map(r => r.strategy))],
      recentRecoveries: recentRecoveries.slice(-5) // 최근 5개
    };
  }
}

// 글로벌 자동 복구 시스템
export const autoRecovery = new AutoRecoverySystem();

보안과 컴플라이언스

1. 로그 보안 관리

// lib/secure-logging.js
import crypto from 'crypto';
import { LoggerFactory } from './logger-factory';

export class SecureLogging {
  constructor() {
    this.logger = LoggerFactory.createLogger('security');
    this.encryptionKey = process.env.LOG_ENCRYPTION_KEY || this.generateKey();
    this.sensitiveFields = [
      'password', 'token', 'apiKey', 'secret', 'ssn', 'creditCard',
      'email', 'phone', 'address', 'personalId', 'bankAccount'
    ];
    this.auditLog = [];
  }
  
  // 민감한 데이터 마스킹
  maskSensitiveData(data) {
    if (typeof data !== 'object' || data === null) {
      return data;
    }
    
    const masked = Array.isArray(data) ? [...data] : { ...data };
    
    for (const key in masked) {
      const lowerKey = key.toLowerCase();
      
      if (this.sensitiveFields.some(field => lowerKey.includes(field))) {
        masked[key] = this.maskValue(key, masked[key]);
      } else if (typeof masked[key] === 'object') {
        masked[key] = this.maskSensitiveData(masked[key]);
      }
    }
    
    return masked;
  }
  
  maskValue(key, value) {
    if (typeof value !== 'string') {
      return '[REDACTED]';
    }
    
    const lowerKey = key.toLowerCase();
    
    // 이메일 부분 마스킹
    if (lowerKey.includes('email')) {
      const [local, domain] = value.split('@');
      if (local && domain) {
        const maskedLocal = local.length > 2 
          ? local.slice(0, 2) + '*'.repeat(local.length - 2)
          : '**';
        return `${maskedLocal}@${domain}`;
      }
    }
    
    // 전화번호 부분 마스킹
    if (lowerKey.includes('phone')) {
      return value.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2');
    }
    
    // 기본 마스킹
    if (value.length <= 4) {
      return '*'.repeat(value.length);
    } else {
      return value.slice(0, 2) + '*'.repeat(value.length - 4) + value.slice(-2);
    }
  }
  
  // 로그 암호화
  encryptLog(logData) {
    try {
      const iv = crypto.randomBytes(16);
      const cipher = crypto.createCipher('aes-256-cbc', this.encryptionKey);
      
      let encrypted = cipher.update(JSON.stringify(logData), 'utf8', 'hex');
      encrypted += cipher.final('hex');
      
      return {
        encrypted: true,
        iv: iv.toString('hex'),
        data: encrypted
      };
    } catch (error) {
      this.logger.error('Log encryption failed', { error: error.message });
      return logData;
    }
  }
  
  // 로그 복호화
  decryptLog(encryptedData) {
    try {
      const decipher = crypto.createDecipher('aes-256-cbc', this.encryptionKey);
      
      let decrypted = decipher.update(encryptedData.data, 'hex', 'utf8');
      decrypted += decipher.final('utf8');
      
      return JSON.parse(decrypted);
    } catch (error) {
      this.logger.error('Log decryption failed', { error: error.message });
      return null;
    }
  }
  
  // 감사 로그 기록
  logAuditEvent(event, actor, resource, action, result) {
    const auditEntry = {
      id: crypto.randomUUID(),
      timestamp: new Date().toISOString(),
      event,
      actor: {
        id: actor.id,
        type: actor.type,
        ip: actor.ip,
        userAgent: actor.userAgent
      },
      resource: {
        type: resource.type,
        id: resource.id,
        attributes: this.maskSensitiveData(resource.attributes)
      },
      action,
      result: {
        status: result.status,
        message: result.message,
        details: this.maskSensitiveData(result.details)
      }
    };
    
    this.auditLog.push(auditEntry);
    
    // 중요한 감사 이벤트는 즉시 로깅
    if (this.isCriticalAuditEvent(event)) {
      this.logger.warn('Critical audit event', auditEntry);
    } else {
      this.logger.info('Audit event', auditEntry);
    }
    
    // 감사 로그 크기 관리 (최근 10000개만 유지)
    if (this.auditLog.length > 10000) {
      this.auditLog = this.auditLog.slice(-10000);
    }
  }
  
  isCriticalAuditEvent(event) {
    const criticalEvents = [
      'authentication_failure',
      'authorization_failure',
      'data_access_violation',
      'privilege_escalation',
      'suspicious_activity',
      'security_breach'
    ];
    
    return criticalEvents.includes(event);
  }
  
  // 컴플라이언스 보고서 생성
  generateComplianceReport(startDate, endDate) {
    const start = new Date(startDate);
    const end = new Date(endDate);
    
    const relevantLogs = this.auditLog.filter(log => {
      const logDate = new Date(log.timestamp);
      return logDate >= start && logDate <= end;
    });
    
    const report = {
      period: {
        start: startDate,
        end: endDate
      },
      summary: {
        totalEvents: relevantLogs.length,
        criticalEvents: relevantLogs.filter(log => this.isCriticalAuditEvent(log.event)).length,
        uniqueActors: new Set(relevantLogs.map(log => log.actor.id)).size,
        eventTypes: this.groupBy(relevantLogs, 'event')
      },
      violations: this.identifyViolations(relevantLogs),
      recommendations: this.generateSecurityRecommendations(relevantLogs)
    };
    
    this.logger.info('Compliance report generated', {
      period: report.period,
      summary: report.summary
    });
    
    return report;
  }
  
  identifyViolations(logs) {
    const violations = [];
    
    // 반복적인 인증 실패 감지
    const authFailures = logs.filter(log => log.event === 'authentication_failure');
    const failuresByActor = this.groupBy(authFailures, 'actor.id');
    
    for (const [actorId, failures] of Object.entries(failuresByActor)) {
      if (failures.length >= 5) {
        violations.push({
          type: 'excessive_auth_failures',
          severity: 'high',
          actor: actorId,
          count: failures.length,
          description: `Actor ${actorId} had ${failures.length} authentication failures`
        });
      }
    }
    
    // 의심스러운 데이터 접근 패턴 감지
    const dataAccess = logs.filter(log => log.action === 'data_access');
    const accessByActor = this.groupBy(dataAccess, 'actor.id');
    
    for (const [actorId, accesses] of Object.entries(accessByActor)) {
      const uniqueResources = new Set(accesses.map(a => a.resource.id)).size;
      
      if (uniqueResources > 100) { // 100개 이상의 서로 다른 리소스 접근
        violations.push({
          type: 'excessive_data_access',
          severity: 'medium',
          actor: actorId,
          resourceCount: uniqueResources,
          description: `Actor ${actorId} accessed ${uniqueResources} different resources`
        });
      }
    }
    
    return violations;
  }
  
  generateSecurityRecommendations(logs) {
    const recommendations = [];
    
    const criticalEvents = logs.filter(log => this.isCriticalAuditEvent(log.event));
    
    if (criticalEvents.length > 0) {
      recommendations.push({
        category: 'monitoring',
        priority: 'high',
        title: 'Enhance Security Monitoring',
        description: `${criticalEvents.length} critical security events detected`,
        actions: [
          'Implement real-time security alerts',
          'Review and strengthen access controls',
          'Conduct security audit',
          'Update incident response procedures'
        ]
      });
    }
    
    return recommendations;
  }
  
  // 유틸리티 메서드
  groupBy(array, key) {
    return array.reduce((groups, item) => {
      const value = this.getNestedValue(item, key);
      groups[value] = groups[value] || [];
      groups[value].push(item);
      return groups;
    }, {});
  }
  
  getNestedValue(obj, path) {
    return path.split('.').reduce((current, key) => current?.[key], obj);
  }
  
  generateKey() {
    return crypto.randomBytes(32).toString('hex');
  }
}

// 글로벌 보안 로깅 인스턴스
export const secureLogging = new SecureLogging();

// Express 미들웨어
export function securityAuditMiddleware(req, res, next) {
  const originalSend = res.send;
  
  res.send = function(data) {
    // 응답 완료 후 감사 로그 기록
    secureLogging.logAuditEvent(
      'api_request',
      {
        id: req.user?.id || 'anonymous',
        type: 'user',
        ip: req.ip,
        userAgent: req.get('User-Agent')
      },
      {
        type: 'api_endpoint',
        id: `${req.method} ${req.path}`,
        attributes: {
          method: req.method,
          path: req.path,
          query: req.query
        }
      },
      'api_access',
      {
        status: res.statusCode < 400 ? 'success' : 'failure',
        message: res.statusCode < 400 ? 'Request completed successfully' : 'Request failed',
        details: {
          statusCode: res.statusCode,
          responseSize: data ? data.length : 0
        }
      }
    );
    
    originalSend.call(this, data);
  };
  
  next();
}

실전 배포 가이드

1. 프로덕션 체크리스트

## 로깅 시스템 프로덕션 배포 체크리스트

### 환경 설정
- [ ] 로그 레벨이 적절히 설정됨 (production: warn 이상)
- [ ] 환경 변수가 모두 설정됨
- [ ] 로그 디렉토리 권한이 올바르게 설정됨
- [ ] 로그 로테이션이 활성화됨

### 성능 최적화
- [ ] 비동기 로깅 활성화
- [ ] 로그 버퍼링 설정 확인
- [ ] 메모리 사용량 모니터링 설정
- [ ] 로그 볼륨 테스트 완료

### 보안
- [ ] 민감한 정보 마스킹 적용
- [ ] 로그 파일 접근 권한 제한
- [ ] 로그 암호화 설정 (필요 시)
- [ ] 감사 로그 활성화

### 모니터링
- [ ] 알림 시스템 연결 (Slack, PagerDuty 등)
- [ ] 대시보드 설정 (Grafana, Kibana 등)
- [ ] 자동 복구 시스템 활성화
- [ ] 헬스 체크 엔드포인트 구현

### 백업 및 복구
- [ ] 로그 백업 전략 수립
- [ ] 재해 복구 계획 수립
- [ ] 로그 아카이빙 프로세스 구현
- [ ] 복구 테스트 완료

### 컴플라이언스
- [ ] 데이터 보호 규정 준수 확인
- [ ] 로그 보존 정책 수립
- [ ] 접근 로그 기록 활성화
- [ ] 정기 감사 프로세스 수립

2. 배포 스크립트

// scripts/deploy-logging.js
import { Signale } from 'signale';
import fs from 'fs-extra';
import path from 'path';

const deployer = new Signale({ scope: 'deploy-logging' });

export async function deployLoggingSystem() {
  deployer.start('로깅 시스템 배포 시작');
  
  try {
    // 1. 환경 검증
    await validateEnvironment();
    
    // 2. 로그 디렉토리 생성
    await createLogDirectories();
    
    // 3. 설정 파일 배포
    await deployConfigurations();
    
    // 4. 권한 설정
    await setPermissions();
    
    // 5. 서비스 시작
    await startLoggingServices();
    
    // 6. 검증
    await validateDeployment();
    
    deployer.complete('로깅 시스템 배포 완료!');
    
  } catch (error) {
    deployer.error('배포 실패:', error.message);
    throw error;
  }
}

async function validateEnvironment() {
  deployer.pending('환경 검증 중...');
  
  const requiredVars = [
    'NODE_ENV',
    'LOG_LEVEL',
    'ELASTICSEARCH_URL',
    'SLACK_WEBHOOK_URL'
  ];
  
  const missing = requiredVars.filter(varName => !process.env[varName]);
  
  if (missing.length > 0) {
    throw new Error(`필수 환경 변수가 누락됨: ${missing.join(', ')}`);
  }
  
  deployer.success('환경 검증 완료');
}

async function createLogDirectories() {
  deployer.pending('로그 디렉토리 생성 중...');
  
  const directories = [
    './logs',
    './logs/archive',
    './logs/backup'
  ];
  
  for (const dir of directories) {
    await fs.ensureDir(dir);
    deployer.info(`디렉토리 생성: ${dir}`);
  }
  
  deployer.success('로그 디렉토리 생성 완료');
}

async function deployConfigurations() {
  deployer.pending('설정 파일 배포 중...');
  
  const configs = {
    'logging-config.json': createLoggingConfig(),
    'log-rotation.conf': createRotationConfig(),
    'monitoring-config.json': createMonitoringConfig()
  };
  
  for (const [filename, content] of Object.entries(configs)) {
    const filepath = path.join('./config', filename);
    await fs.writeJSON(filepath, content, { spaces: 2 });
    deployer.info(`설정 파일 배포: ${filename}`);
  }
  
  deployer.success('설정 파일 배포 완료');
}

function createLoggingConfig() {
  return {
    level: process.env.LOG_LEVEL || 'warn',
    outputs: {
      console: process.env.NODE_ENV === 'development',
      file: true,
      elasticsearch: !!process.env.ELASTICSEARCH_URL
    },
    rotation: {
      maxSize: '50MB',
      maxFiles: 10,
      compress: true
    },
    security: {
      maskSensitiveData: true,
      auditLogging: true,
      encryption: process.env.LOG_ENCRYPTION_ENABLED === 'true'
    }
  };
}

// 배포 실행
if (import.meta.url === `file://${process.argv[1]}`) {
  deployLoggingSystem().catch(console.error);
}

마무리

프로덕션 환경에서의 로깅은 단순한 디버깅 도구를 넘어서 서비스의 안정성, 성능, 보안을 책임지는 핵심 인프라입니다. 이번 시리즈를 통해 살펴본 모든 요소들이 조화롭게 동작할 때 비로소 완전한 로깅 시스템이 완성됩니다.

핵심 포인트 요약:

1. 하이브리드 접근법: 개발 환경에서는 Consola/Signale로 시각적 피드백을, 프로덕션에서는 Pino/Winston으로 성능과 구조화를 추구하세요.

2. 계층별 전략: API, 비즈니스 로직, 인프라 각 계층마다 적절한 로깅 전략을 수립하세요.

3. 자동화가 핵심: 모니터링, 알림, 복구까지 모든 과정을 자동화해야 운영 부담을 줄일 수 있습니다.

4. 보안과 컴플라이언스: 로그 자체가 보안 위험이 되지 않도록 민감 정보 처리와 접근 제어에 주의하세요.

5. 지속적인 개선: 로깅 시스템도 서비스와 함께 진화해야 합니다. 정기적인 리뷰와 최적화를 잊지 마세요.

앞으로의 트렌드:

  • OpenTelemetry: 로깅, 메트릭, 트레이싱의 통합 표준
  • 구조화된 로깅: JSON 기반 로그의 일반화
  • AI 기반 분석: 로그 패턴 분석과 이상 징후 감지
  • 클라우드 네이티브: 컨테이너 환경에 최적화된 로깅

이번 시리즈가 여러분의 Next.js 프로젝트에서 로깅 시스템을 구축하는 데 도움이 되었기를 바랍니다. 작은 프로젝트부터 시작해서 점진적으로 고도화해 나가세요. 좋은 로깅은 하루아침에 만들어지지 않지만, 한 번 제대로 구축하면 개발팀의 생산성과 서비스 품질을 크게 향상시킬 것입니다.




Leave a Comment