icon
10장 : 보안과 모범 사례

로깅과 감사 추적


지난 절에서는 NestJS 애플리케이션의 환경 변수를 안전하게 관리하고 민감한 정보를 보호하는 방법에 대해 알아보았습니다. 이제 10장의 세 번째 절로, 시스템의 보안과 운영 효율성을 높이는 데 필수적인 로깅(Logging)감사 추적(Audit Trailing) 에 대해 살펴보겠습니다.

애플리케이션은 예상치 못한 오류, 보안 이벤트, 사용자 활동 등 다양한 정보를 생성합니다. 이러한 정보를 체계적으로 기록하고 분석하는 것은 문제 해결, 보안 위협 감지, 규정 준수, 그리고 시스템 성능 개선에 결정적인 역할을 합니다.


로깅(Logging)이란?

로깅은 애플리케이션이 실행되는 동안 발생하는 이벤트를 기록하는 과정입니다. 이 로그들은 텍스트 파일, 데이터베이스, 중앙 집중식 로그 관리 시스템 등 다양한 형태로 저장될 수 있습니다.

로깅의 주요 목적

  • 디버깅 및 문제 해결: 오류 발생 시 로그를 통해 원인을 파악하고 해결책을 찾는 데 가장 중요한 정보원입니다.
  • 성능 분석: 애플리케이션의 동작 흐름과 특정 작업의 처리 시간을 기록하여 성능 병목을 식별하는 데 도움을 줍니다.
  • 보안 감지: 비정상적인 접근 시도, 인증 실패, 데이터 조작 시도 등 보안 관련 이벤트를 기록하여 침입을 감지합니다.
  • 운영 모니터링: 시스템의 전반적인 상태를 파악하고, 경고(Alert) 시스템과 연동하여 문제 발생 시 즉각적인 알림을 받습니다.
  • 감사 추적: 누가, 언제, 무엇을 했는지 기록하여 규정 준수 및 책임 추적을 가능하게 합니다. (감사 추적은 로깅의 특정 목적)

로그 레벨 (Log Levels)

로그는 중요도에 따라 여러 레벨로 나뉘며, 개발 환경과 운영 환경에서 필요한 로그 레벨을 다르게 설정하여 효율적으로 관리합니다.

  • DEBUG: 개발 및 디버깅 목적으로 상세한 정보를 기록합니다. 운영 환경에서는 비활성화하는 경우가 많습니다.
  • INFO: 애플리케이션의 정상적인 동작 흐름을 나타내는 정보를 기록합니다 (예: "서버 시작", "사용자 로그인 성공").
  • WARN: 잠재적인 문제를 나타내지만, 애플리케이션 동작에는 즉각적인 영향을 주지 않는 경고입니다 (예: "리소스 부족 경고", "deprecated API 호출").
  • ERROR: 오류가 발생했지만, 애플리케이션의 전체 중단으로 이어지지는 않는 문제를 기록합니다 (예: "데이터베이스 연결 실패").
  • FATAL: 애플리케이션이 더 이상 동작할 수 없는 심각한 오류를 기록합니다 (예: "치명적인 예외로 서버 종료").

NestJS에서 로깅 구현

NestJS는 자체 로깅 시스템을 제공하며, @nestjs/commonLogger 클래스를 통해 편리하게 로깅을 수행할 수 있습니다. 또한, Winston, Pino 등 강력한 외부 로깅 라이브러리와도 쉽게 통합할 수 있습니다.

NestJS Logger 기본 사용법

NestJS는 기본적으로 console.log와 유사한 기능을 하는 내장 로거를 제공하며, DI(Dependency Injection) 시스템과 통합됩니다.

// src/app.service.ts
import { Injectable, Logger } from '@nestjs/common';

@Injectable()
export class AppService {
  private readonly logger = new Logger(AppService.name); // 클래스 이름을 로거 컨텍스트로 사용

  getHello(): string {
    this.logger.log('Hello World API가 호출되었습니다.'); // INFO 레벨
    this.logger.warn('데이터베이스 연결에 문제가 있을 수 있습니다.'); // WARN 레벨
    this.logger.error('치명적인 오류 발생!', '스택 트레이스'); // ERROR 레벨
    this.logger.debug('디버그 메시지'); // DEBUG 레벨 (환경 설정에 따라 출력 여부 결정)
    return 'Hello World!';
  }
}
  • new Logger(AppService.name): Logger 인스턴스를 생성할 때 컨텍스트 이름(일반적으로 클래스 이름)을 전달합니다. 이는 로그가 어디서 발생했는지 추적하는 데 유용합니다.
  • logger.log(), logger.warn(), logger.error(), logger.debug(): 각 로그 레벨에 해당하는 메서드를 사용하여 메시지를 기록합니다.

전역 로거 설정

main.ts에서 NestJS 애플리케이션의 전역 로거를 설정할 수 있습니다.

// src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { Logger } from '@nestjs/common'; // Logger 임포트

async function bootstrap() {
  const app = await NestFactory.create(AppModule, {
    logger: ['error', 'warn', 'log', 'debug', 'verbose'], // 기본적으로 출력할 로그 레벨
    // 또는 환경 변수에 따라 다르게 설정:
    // logger: process.env.NODE_ENV === 'production' ? ['error', 'warn', 'log'] : ['error', 'warn', 'log', 'debug'],
  });

  // NestJS의 기본 로거 인스턴스에 접근하여 설정 변경
  // app.useLogger(new Logger()); // 커스텀 로거를 주입할 수도 있습니다.

  await app.listen(3000);
  Logger.log(`Application is running on: ${await app.getUrl()}`, 'Bootstrap');
}
bootstrap();

외부 로깅 라이브러리 통합

실제 운영 환경에서는 NestJS의 기본 로거보다 더 강력한 기능을 제공하는 Winston이나 Pino 같은 라이브러리를 사용합니다. 이들은 로그를 파일, 외부 서비스(예: ELK Stack, CloudWatch Logs) 등으로 전송하고, 로그 형식을 제어하는 등의 고급 기능을 제공합니다.

단계 1: 필요한 패키지 설치

npm install winston winston-daily-rotate-file
npm install --save-dev @types/winston @types/winston-daily-rotate-file
  • winston: Node.js의 인기 있는 로깅 라이브러리.
  • winston-daily-rotate-file: 날짜별로 로그 파일을 자동으로 순환시키는 Winston 트랜스포트.

단계 2: WinstonLogger 서비스 생성

NestJS의 LoggerService 인터페이스를 구현하여 Winston을 통합합니다.

// src/config/winston.logger.ts
import { LoggerService } from '@nestjs/common';
import { createLogger, format, transports, Logger as WinstonLoggerType } from 'winston';
import * as DailyRotateFile from 'winston-daily-rotate-file';

export class WinstonLogger implements LoggerService {
  private readonly logger: WinstonLoggerType;

  constructor() {
    // 콘솔에 컬러풀하게 출력하는 포맷
    const consoleFormat = format.combine(
      format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
      format.colorize(),
      format.printf(info => `${info.timestamp} [${info.level}] [${info.context || 'App'}] ${info.message}`),
    );

    // 파일에 JSON 형식으로 저장하는 포맷
    const fileFormat = format.combine(
      format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
      format.json(),
    );

    this.logger = createLogger({
      level: process.env.NODE_ENV === 'production' ? 'info' : 'debug', // 운영은 info, 개발은 debug 레벨
      format: fileFormat, // 파일에는 JSON 포맷 적용
      transports: [
        new transports.Console({
          format: consoleFormat, // 콘솔에는 컬러 포맷 적용
        }),
        new DailyRotateFile({ // 날짜별 로그 파일 생성
          filename: 'logs/application-%DATE%.log',
          datePattern: 'YYYY-MM-DD',
          zippedArchive: true,
          maxSize: '20m', // 20MB 이상 시 새 파일 생성
          maxFiles: '14d', // 14일치 로그 보관
          level: 'info', // 파일에는 info 레벨 이상만 기록
        }),
        new DailyRotateFile({ // 에러 로그만 따로 파일 생성
          filename: 'logs/error-%DATE%.log',
          datePattern: 'YYYY-MM-DD',
          zippedArchive: true,
          maxSize: '20m',
          maxFiles: '14d',
          level: 'error', // error 레벨만 기록
        }),
      ],
      exceptionHandlers: [ // 처리되지 않은 예외를 파일에 기록
        new DailyRotateFile({
          filename: 'logs/exceptions-%DATE%.log',
          datePattern: 'YYYY-MM-DD',
          zippedArchive: true,
          maxSize: '20m',
          maxFiles: '14d',
        }),
      ],
      rejectionHandlers: [ // Promise 예외를 파일에 기록
        new DailyRotateFile({
          filename: 'logs/rejections-%DATE%.log',
          datePattern: 'YYYY-MM-DD',
          zippedArchive: true,
          maxSize: '20m',
          maxFiles: '14d',
        }),
      ],
    });
  }

  log(message: any, context?: string) {
    this.logger.info(message, { context });
  }

  error(message: any, trace?: string, context?: string) {
    this.logger.error(message, { trace, context });
  }

  warn(message: any, context?: string) {
    this.logger.warn(message, { context });
  }

  debug(message: any, context?: string) {
    this.logger.debug(message, { context });
  }

  verbose(message: any, context?: string) {
    this.logger.verbose(message, { context });
  }
}

단계 3: main.ts에서 Winston 로거 사용

// src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { WinstonLogger } from './config/winston.logger'; // 커스텀 로거 임포트

async function bootstrap() {
  const app = await NestFactory.create(AppModule, {
    logger: new WinstonLogger(), // 커스텀 로거 인스턴스 주입
  });

  await app.listen(3000);
  new WinstonLogger().log(`Application is running on: ${await app.getUrl()}`, 'Bootstrap');
}
bootstrap();

이제 NestJS의 모든 로깅 호출은 Winston을 통해 처리되며, 설정에 따라 콘솔, 파일 등에 기록됩니다.


감사 추적(Audit Trailing)

감사 추적은 특정 사용자 또는 시스템이 수행한 중요한 작업을 시간 순서대로 기록하여, 나중에 해당 활동을 검토하고 추적할 수 있도록 하는 기능입니다. 이는 보안 감사, 규제 준수, 문제 발생 시 책임 추적 등에 사용됩니다.

감사 추적에 포함되어야 할 정보

  • 누가(Who): 작업을 수행한 사용자 또는 시스템의 식별자 (사용자 ID, IP 주소 등)
  • 무엇을(What): 수행된 작업의 종류 (로그인, 데이터 생성/수정/삭제, 설정 변경 등)
  • 언제(When): 작업이 수행된 정확한 시간 (타임스탬프)
  • 어디서(Where): 작업이 발생한 소스 (IP 주소, API 엔드포인트 등)
  • 어떻게(How): 작업의 성공/실패 여부 및 관련 상세 정보 (예: 변경된 데이터의 이전 값/새로운 값)

감사 추적 구현 방법

전용 테이블/컬렉션 사용

  • 감사 로그를 일반 애플리케이션 로그와 분리하여 데이터베이스의 별도 테이블이나 컬렉션에 저장합니다. 이는 검색 및 분석에 용이하고, 애플리케이션 로그와 독립적으로 관리할 수 있습니다.
  • 각 중요한 작업(예: 사용자 생성, 게시글 수정, 결제 완료)에 대해 감사 로그 엔트리를 생성합니다.
// src/audit/audit.entity.ts (TypeORM 예시)
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn } from 'typeorm';

@Entity('audit_logs')
export class AuditLog {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  userId: number; // 작업 수행 사용자 ID

  @Column()
  action: string; // 수행된 작업 (예: 'USER_CREATED', 'POST_UPDATED', 'LOGIN_SUCCESS')

  @Column({ nullable: true })
  targetId: number; // 작업 대상 ID (예: 게시글 ID)

  @Column({ type: 'jsonb', nullable: true })
  details: object; // 변경된 데이터, IP 주소 등 상세 정보

  @Column({ nullable: true })
  ipAddress: string;

  @CreateDateColumn()
  timestamp: Date;
}
// src/user/user.service.ts (감사 로그 기록 예시)
import { Injectable } from '@nestjs/common';
import { Repository } from 'typeorm';
import { InjectRepository } from '@nestjs/typeorm';
import { User } from './user.entity';
import { AuditLog } from '../audit/audit.entity'; // 감사 로그 엔티티 임포트

@Injectable()
export class UserService {
  constructor(
    @InjectRepository(User)
    private usersRepository: Repository<User>,
    @InjectRepository(AuditLog) // AuditLog Repository 주입
    private auditLogRepository: Repository<AuditLog>,
  ) {}

  async createUser(userData: any): Promise<User> {
    const newUser = this.usersRepository.create(userData);
    const savedUser = await this.usersRepository.save(newUser);

    // 사용자 생성 감사 로그 기록
    await this.auditLogRepository.save({
      userId: savedUser.id, // 누가
      action: 'USER_CREATED', // 무엇을
      targetId: savedUser.id, // 어떤 대상을
      details: { email: savedUser.email }, // 상세 정보
      ipAddress: '127.0.0.1', // 요청 IP (실제로는 req.ip 등에서 가져옴)
    });

    return savedUser;
  }
}

로그 관리 시스템 활용

  • ELK Stack (Elasticsearch, Logstash, Kibana), Splunk, Datadog 등 중앙 집중식 로그 관리 시스템을 사용하여 감사 로그를 수집, 저장, 검색, 시각화합니다.
  • 이러한 시스템은 방대한 양의 로그를 효율적으로 처리하고, 실시간 대시보드 및 알림 기능을 제공하여 보안 이벤트 감지에 매우 효과적입니다.

감사 추적 시 고려사항

  • 기록 범위: 모든 활동을 기록할 필요는 없으며, 보안 또는 비즈니스적으로 중요한 작업에 집중합니다.
  • 성능 영향: 감사 로그 기록은 추가적인 I/O 작업을 유발하므로, 애플리케이션의 성능에 미치는 영향을 최소화하도록 최적화해야 합니다 (비동기 처리 등).
  • 보안: 감사 로그 자체도 민감한 정보가 포함될 수 있으므로, 접근 제어 및 암호화 등 보안 조치를 적용해야 합니다.
  • 보관 정책: 규정 준수 요구사항에 따라 감사 로그의 보관 기간을 설정하고 관리해야 합니다.

로깅과 감사 추적은 애플리케이션 운영의 핵심적인 부분이며, 보안 전략의 중요한 기둥입니다. NestJS는 유연한 로깅 시스템을 제공하여 Winston과 같은 외부 라이브러리와 쉽게 통합할 수 있습니다. 체계적인 로깅은 문제 해결과 성능 분석에 필수적이며, 정교한 감사 추적은 보안 위협을 감지하고 규정 준수를 보장하는 데 결정적인 역할을 합니다. 이러한 모범 사례들을 적용하여 견고하고 신뢰할 수 있는 애플리케이션을 구축하시길 바랍니다.