Theory
중급 2-3시간 이론 + 실습

Spring 18: 로깅

효과적인 로깅 전략

LogbackSLF4JMDC로그 레벨

1. 로깅 개념

1.1 로깅의 필요성

로깅은 애플리케이션의 실행 상태를 기록하고 추적하는 핵심 메커니즘입니다.

로깅이 필요한 이유:

  • 디버깅: 프로덕션 환경에서 발생한 문제의 원인 파악
  • 모니터링: 시스템 상태와 성능 지표 실시간 추적
  • 감사(Audit): 보안 및 규정 준수를 위한 사용자 활동 기록
  • 분석: 비즈니스 인사이트 도출 및 사용자 행동 패턴 분석
  • 알림: 임계값 초과 시 자동 알림 및 대응

System.out.println vs 로깅 프레임워크

// ❌ 나쁜 예: System.out.println 사용
public void processOrder(Order order) {
    System.out.println("Processing order: " + order.getId());
    // 문제점:
    // 1. 로그 레벨 제어 불가
    // 2. 출력 대상 변경 불가 (파일, 네트워크 등)
    // 3. 성능 저하 (동기 I/O)
    // 4. 구조화된 정보 부족
    // 5. 프로덕션 환경에서 제거 어려움
}

// ✅ 좋은 예: 로깅 프레임워크 사용
@Slf4j
public class OrderService {
    public void processOrder(Order order) {
        log.info("Processing order: orderId={}, userId={}, amount={}", 
                 order.getId(), order.getUserId(), order.getAmount());
        // 장점:
        // 1. 로그 레벨로 중요도 구분
        // 2. 다양한 출력 대상 설정 가능
        // 3. 비동기 처리로 성능 최적화
        // 4. 구조화된 로그 생성
        // 5. 환경별 설정으로 유연한 제어
    }
}

1.2 로그 레벨 (Log Levels)

로그 레벨은 로그 메시지의 중요도와 심각도를 나타냅니다. 적절한 로그 레벨 사용은 효과적인 로깅의 핵심입니다.

로그 레벨 계층 구조 (낮음 → 높음)

TRACE

가장 상세한 정보. 코드 실행 경로 추적용

사용 시기: 매우 세밀한 디버깅이 필요할 때

DEBUG

개발 중 디버깅 정보. 변수 값, 메서드 호출 등

사용 시기: 개발/테스트 환경에서 문제 진단

INFO

일반적인 정보성 메시지. 주요 비즈니스 이벤트

사용 시기: 애플리케이션의 정상 동작 흐름 기록

WARN

잠재적 문제 상황. 즉시 처리 불필요하지만 주의 필요

사용 시기: 예상치 못한 상황이지만 복구 가능한 경우

ERROR

오류 발생. 기능 실패했지만 애플리케이션은 계속 실행

사용 시기: 예외 발생, 비즈니스 로직 실패

로그 레벨 사용 예제

@Slf4j
@Service
public class PaymentService {
    
    public PaymentResult processPayment(PaymentRequest request) {
        // TRACE: 메서드 진입/종료, 상세한 실행 흐름
        log.trace("Entering processPayment: request={}", request);
        
        // DEBUG: 개발 시 필요한 상세 정보
        log.debug("Validating payment request: amount={}, currency={}", 
                  request.getAmount(), request.getCurrency());
        
        try {
            // INFO: 중요한 비즈니스 이벤트
            log.info("Payment processing started: userId={}, amount={}, method={}", 
                     request.getUserId(), request.getAmount(), request.getPaymentMethod());
            
            PaymentResult result = paymentGateway.charge(request);
            
            if (result.isSuccess()) {
                // INFO: 성공적인 비즈니스 트랜잭션
                log.info("Payment completed successfully: transactionId={}, amount={}", 
                         result.getTransactionId(), request.getAmount());
            } else {
                // WARN: 실패했지만 예상 가능한 상황
                log.warn("Payment declined: userId={}, reason={}", 
                         request.getUserId(), result.getDeclineReason());
            }
            
            return result;
            
        } catch (PaymentGatewayException e) {
            // ERROR: 예외 발생, 스택 트레이스 포함
            log.error("Payment gateway error: userId={}, amount={}", 
                      request.getUserId(), request.getAmount(), e);
            throw new PaymentProcessingException("Failed to process payment", e);
            
        } catch (Exception e) {
            // ERROR: 예상치 못한 오류
            log.error("Unexpected error during payment processing: userId={}", 
                      request.getUserId(), e);
            throw e;
        } finally {
            // TRACE: 메서드 종료
            log.trace("Exiting processPayment");
        }
    }
    
    // 로그 레벨 동적 체크로 성능 최적화
    public void processLargeData(List<Data> dataList) {
        if (log.isDebugEnabled()) {
            // 비용이 큰 연산은 DEBUG 레벨이 활성화된 경우에만 수행
            String detailedInfo = dataList.stream()
                .map(Data::toString)
                .collect(Collectors.joining(", "));
            log.debug("Processing data: {}", detailedInfo);
        }
        
        // 실제 처리 로직
        dataList.forEach(this::process);
    }
}

로그 레벨 선택 가이드

상황레벨예시
애플리케이션 시작/종료INFOApplication started on port 8080
사용자 로그인/로그아웃INFOUser logged in: userId=123
주문 생성/완료INFOOrder created: orderId=456
캐시 미스DEBUGCache miss for key: user:123
재시도 로직 실행WARNRetrying API call (attempt 2/3)
Deprecated API 사용WARNUsing deprecated method
데이터베이스 연결 실패ERRORFailed to connect to database
외부 API 호출 실패ERRORPayment API returned 500

1.3 SLF4J (Simple Logging Facade for Java)

SLF4J는 다양한 로깅 프레임워크에 대한 추상화 레이어를 제공하는 파사드 패턴 구현체입니다.

SLF4J의 장점

  • 추상화: 구체적인 로깅 구현체와 분리되어 유연성 확보
  • 교체 용이: 로깅 구현체 변경 시 코드 수정 불필요
  • 성능: 파라미터화된 메시지로 불필요한 문자열 연산 방지
  • 표준화: 일관된 로깅 API 제공

SLF4J 아키텍처

애플리케이션 코드
      ↓
  SLF4J API (slf4j-api.jar)
      ↓
  바인딩 레이어 (Binding)
      ↓
실제 로깅 구현체
  - Logback (slf4j-logback)
  - Log4j2 (slf4j-log4j2)
  - JUL (slf4j-jdk14)

의존성 예시:
implementation 'org.slf4j:slf4j-api:2.0.9'        // SLF4J API
implementation 'ch.qos.logback:logback-classic:1.4.11'  // Logback 구현체

SLF4J 기본 사용법

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class UserService {
    // 1. Logger 인스턴스 생성 (클래스별로 하나)
    private static final Logger log = LoggerFactory.getLogger(UserService.class);
    
    public void createUser(String username, String email) {
        // 2. 파라미터화된 메시지 사용 (성능 최적화)
        log.info("Creating user: username={}, email={}", username, email);
        
        // ❌ 나쁜 예: 문자열 연결 (항상 연산 수행)
        log.info("Creating user: " + username + ", " + email);
        
        // ✅ 좋은 예: 파라미터 사용 (로그 레벨이 활성화된 경우에만 연산)
        log.info("Creating user: username={}, email={}", username, email);
    }
}

// Lombok 사용 시 더 간단하게
import lombok.extern.slf4j.Slf4j;

@Slf4j  // Logger 자동 생성
public class UserService {
    public void createUser(String username, String email) {
        log.info("Creating user: username={}, email={}", username, email);
    }
}

SLF4J 고급 기능

@Slf4j
public class AdvancedLoggingExample {
    
    // 1. 마커(Marker)를 사용한 로그 분류
    private static final Marker SECURITY_MARKER = MarkerFactory.getMarker("SECURITY");
    private static final Marker PERFORMANCE_MARKER = MarkerFactory.getMarker("PERFORMANCE");
    
    public void login(String username) {
        // 보안 관련 로그는 SECURITY 마커 사용
        log.info(SECURITY_MARKER, "User login attempt: username={}", username);
    }
    
    public void measurePerformance() {
        long startTime = System.currentTimeMillis();
        // ... 작업 수행
        long duration = System.currentTimeMillis() - startTime;
        
        // 성능 관련 로그는 PERFORMANCE 마커 사용
        log.info(PERFORMANCE_MARKER, "Operation completed in {}ms", duration);
    }
    
    // 2. 예외 로깅
    public void processData(String data) {
        try {
            // 처리 로직
        } catch (Exception e) {
            // 예외 객체를 마지막 파라미터로 전달
            log.error("Failed to process data: data={}", data, e);
            
            // ❌ 나쁜 예: 예외를 문자열로 변환 (스택 트레이스 손실)
            log.error("Failed to process data: " + e.getMessage());
            
            // ✅ 좋은 예: 예외 객체 전달 (전체 스택 트레이스 포함)
            log.error("Failed to process data", e);
        }
    }
    
    // 3. 조건부 로깅 (성능 최적화)
    public void processLargeDataset(List<ComplexObject> objects) {
        // 비용이 큰 연산은 로그 레벨 체크 후 수행
        if (log.isDebugEnabled()) {
            String summary = objects.stream()
                .map(obj -> obj.expensiveToString())  // 비용이 큰 연산
                .collect(Collectors.joining(", "));
            log.debug("Processing objects: {}", summary);
        }
        
        // 실제 처리
        objects.forEach(this::process);
    }
    
    // 4. Fluent API (SLF4J 2.0+)
    public void fluentLogging(String userId, String action) {
        log.atInfo()
            .addKeyValue("userId", userId)
            .addKeyValue("action", action)
            .addKeyValue("timestamp", System.currentTimeMillis())
            .log("User action performed");
    }
}

SLF4J vs 다른 로깅 프레임워크

특징SLF4J + LogbackLog4j2JUL
성능우수매우 우수보통
설정 방식XML, GroovyXML, JSON, YAMLProperties
비동기 지원지원강력한 지원제한적
Spring Boot 기본✅ 기본 채택선택 가능사용 안 함
학습 곡선낮음중간낮음

Spring Boot에서 SLF4J 설정

// build.gradle
dependencies {
    // Spring Boot Starter에 이미 포함됨
    implementation 'org.springframework.boot:spring-boot-starter'
    // 내부적으로 다음을 포함:
    // - slf4j-api
    // - logback-classic
    // - logback-core
    
    // Lombok 사용 시
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
}

// application.yml
logging:
  level:
    root: INFO
    com.example.myapp: DEBUG
    org.springframework.web: DEBUG
    org.hibernate.SQL: DEBUG
  pattern:
    console: "%d{yyyy-MM-dd HH:mm:ss} - %msg%n"
  file:
    name: logs/application.log

2. Logback 설정

2.1 Logback 개요

Logback은 SLF4J의 네이티브 구현체로, Spring Boot의 기본 로깅 프레임워크입니다.

Logback의 주요 구성 요소

  • Logger: 로그 이벤트를 생성하는 컴포넌트
  • Appender: 로그를 출력하는 대상 (콘솔, 파일, 네트워크 등)
  • Layout/Encoder: 로그 메시지의 형식을 정의
  • Filter: 로그 이벤트를 필터링하는 조건

Logback 설정 파일 우선순위

1. logback-test.xml (테스트 클래스패스)
2. logback-spring.xml (Spring Boot 권장)
3. logback.xml (표준 Logback)
4. application.yml/properties의 logging 설정

Spring Boot에서는 logback-spring.xml 사용을 권장:
- Spring Profile 지원
- Spring Boot 확장 기능 사용 가능
- 환경별 설정 분리 용이

2.2 logback-spring.xml 기본 구조

기본 logback-spring.xml 예제

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!-- 변수 정의 -->
    <property name="LOG_DIR" value="logs"/>
    <property name="LOG_FILE" value="application"/>
    
    <!-- 콘솔 Appender -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>
    
    <!-- 파일 Appender -->
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOG_DIR}/${LOG_FILE}.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${LOG_DIR}/${LOG_FILE}.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy 
                class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>100MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <maxHistory>30</maxHistory>
            <totalSizeCap>3GB</totalSizeCap>
        </rollingPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>
    
    <!-- 루트 로거 -->
    <root level="INFO">
        <appender-ref ref="CONSOLE"/>
        <appender-ref ref="FILE"/>
    </root>
    
    <!-- 패키지별 로그 레벨 설정 -->
    <logger name="com.example.myapp" level="DEBUG"/>
    <logger name="org.springframework.web" level="DEBUG"/>
    <logger name="org.hibernate.SQL" level="DEBUG"/>
</configuration>

2.3 Appender 상세 설정

1. ConsoleAppender - 콘솔 출력

<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
    <!-- 출력 대상: System.out (기본) 또는 System.err -->
    <target>System.out</target>
    
    <!-- 인코더 설정 -->
    <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
        <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        <charset>UTF-8</charset>
    </encoder>
    
    <!-- 필터 적용 (선택사항) -->
    <filter class="ch.qos.logback.classic.filter.LevelFilter">
        <level>ERROR</level>
        <onMatch>ACCEPT</onMatch>
        <onMismatch>DENY</onMismatch>
    </filter>
</appender>

<!-- 컬러 출력을 위한 콘솔 Appender -->
<appender name="CONSOLE_COLOR" class="ch.qos.logback.core.ConsoleAppender">
    <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
        <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %highlight([%thread]) %highlight(%-5level) %cyan(%logger{36}) - %msg%n</pattern>
    </encoder>
</appender>

2. FileAppender - 파일 출력

<!-- 단순 파일 Appender -->
<appender name="FILE_SIMPLE" class="ch.qos.logback.core.FileAppender">
    <file>logs/application.log</file>
    <append>true</append>
    <encoder>
        <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
</appender>

<!-- 롤링 파일 Appender (시간 기반) -->
<appender name="FILE_TIME_ROLLING" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <file>logs/application.log</file>
    
    <!-- 시간 기반 롤링 정책 -->
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
        <!-- 일별 롤링 -->
        <fileNamePattern>logs/application.%d{yyyy-MM-dd}.log</fileNamePattern>
        <!-- 시간별 롤링 -->
        <!-- <fileNamePattern>logs/application.%d{yyyy-MM-dd-HH}.log</fileNamePattern> -->
        
        <!-- 보관 기간 (일 단위) -->
        <maxHistory>30</maxHistory>
        
        <!-- 전체 로그 파일 크기 제한 -->
        <totalSizeCap>1GB</totalSizeCap>
        
        <!-- 애플리케이션 시작 시 오래된 파일 정리 -->
        <cleanHistoryOnStart>true</cleanHistoryOnStart>
    </rollingPolicy>
    
    <encoder>
        <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
</appender>

<!-- 크기와 시간 기반 롤링 -->
<appender name="FILE_SIZE_TIME_ROLLING" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <file>logs/application.log</file>
    
    <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
        <!-- %i는 같은 날짜 내에서 파일 인덱스 -->
        <fileNamePattern>logs/application.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
        
        <!-- 파일당 최대 크기 -->
        <maxFileSize>100MB</maxFileSize>
        
        <!-- 보관 기간 -->
        <maxHistory>30</maxHistory>
        
        <!-- 전체 크기 제한 -->
        <totalSizeCap>3GB</totalSizeCap>
    </rollingPolicy>
    
    <encoder>
        <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
</appender>

3. AsyncAppender - 비동기 로깅

<!-- 비동기 파일 Appender -->
<appender name="ASYNC_FILE" class="ch.qos.logback.classic.AsyncAppender">
    <!-- 참조할 실제 Appender -->
    <appender-ref ref="FILE"/>
    
    <!-- 큐 크기 (기본: 256) -->
    <queueSize>512</queueSize>
    
    <!-- 큐가 가득 찰 때 버릴 로그 레벨 (기본: TRACE, DEBUG, INFO) -->
    <discardingThreshold>20</discardingThreshold>
    
    <!-- 애플리케이션 종료 시 대기 시간 (밀리초) -->
    <maxFlushTime>1000</maxFlushTime>
    
    <!-- 호출자 정보 포함 여부 (성능에 영향) -->
    <includeCallerData>false</includeCallerData>
    
    <!-- WARN 이상 레벨은 버리지 않음 -->
    <neverBlock>true</neverBlock>
</appender>

<!-- 비동기 콘솔 Appender -->
<appender name="ASYNC_CONSOLE" class="ch.qos.logback.classic.AsyncAppender">
    <appender-ref ref="CONSOLE"/>
    <queueSize>256</queueSize>
    <includeCallerData>false</includeCallerData>
</appender>

3. 구조화 로깅

3.1 구조화 로깅 개념

구조화 로깅은 로그를 일관된 형식으로 기록하여 검색, 분석, 모니터링을 용이하게 하는 방법입니다.

구조화 로깅의 장점

  • 검색 용이성: 특정 필드로 빠른 검색 가능
  • 분석 효율성: 자동화된 로그 분석 및 집계
  • 모니터링: 실시간 알림 및 대시보드 구성
  • 표준화: 팀 간 일관된 로그 형식

JSON 로깅 설정

// build.gradle
dependencies {
    implementation 'net.logstash.logback:logstash-logback-encoder:7.4'
}

// logback-spring.xml
<appender name="JSON_CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
    <encoder class="net.logstash.logback.encoder.LogstashEncoder">
        <includeContext>true</includeContext>
        <includeMdc>true</includeMdc>
        <customFields>{"service":"my-application"}</customFields>
    </encoder>
</appender>

3.2 MDC (Mapped Diagnostic Context)

MDC 필터 구현

@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class MDCFilter implements Filter {
    
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, 
                        FilterChain chain) throws IOException, ServletException {
        
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        
        try {
            // 요청 ID 설정
            String requestId = httpRequest.getHeader("X-Request-ID");
            if (requestId == null) {
                requestId = UUID.randomUUID().toString();
            }
            MDC.put("requestId", requestId);
            
            // 사용자 ID 설정
            String userId = httpRequest.getHeader("X-User-ID");
            if (userId != null) {
                MDC.put("userId", userId);
            }
            
            // HTTP 정보 설정
            MDC.put("httpMethod", httpRequest.getMethod());
            MDC.put("requestUri", httpRequest.getRequestURI());
            MDC.put("clientIp", getClientIpAddress(httpRequest));
            
            chain.doFilter(request, response);
            
        } finally {
            MDC.clear();
        }
    }
    
    private String getClientIpAddress(HttpServletRequest request) {
        String xForwardedFor = request.getHeader("X-Forwarded-For");
        if (xForwardedFor != null && !xForwardedFor.isEmpty()) {
            return xForwardedFor.split(",")[0].trim();
        }
        return request.getRemoteAddr();
    }
}

3.3 요청 추적

분산 추적 구현

@Component
@Order(Ordered.HIGHEST_PRECEDENCE + 1)
public class TracingFilter implements Filter {
    
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, 
                        FilterChain chain) throws IOException, ServletException {
        
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        
        try {
            // Trace ID 설정
            String traceId = httpRequest.getHeader("X-Trace-ID");
            if (traceId == null) {
                traceId = UUID.randomUUID().toString().replace("-", "");
            }
            
            String spanId = Long.toHexString(System.nanoTime());
            
            MDC.put("traceId", traceId);
            MDC.put("spanId", spanId);
            
            // 응답 헤더에 추적 정보 추가
            httpResponse.setHeader("X-Trace-ID", traceId);
            httpResponse.setHeader("X-Span-ID", spanId);
            
            chain.doFilter(request, response);
            
        } finally {
            MDC.remove("traceId");
            MDC.remove("spanId");
        }
    }
}

// 외부 API 호출 시 Trace ID 전파
@Service
public class ExternalApiService {
    
    public void callExternalApi() {
        String traceId = MDC.get("traceId");
        
        HttpHeaders headers = new HttpHeaders();
        if (traceId != null) {
            headers.set("X-Trace-ID", traceId);
        }
        
        HttpEntity<String> entity = new HttpEntity<>(headers);
        
        log.info("Calling external API with traceId: {}", traceId);
        restTemplate.exchange("https://api.example.com/data", 
                            HttpMethod.GET, entity, String.class);
    }
}

Spring Cloud Sleuth 통합

// build.gradle
dependencies {
    implementation 'org.springframework.cloud:spring-cloud-starter-sleuth'
}

// application.yml
spring:
  sleuth:
    sampler:
      probability: 1.0
    web:
      skip-pattern: /health|/metrics

// 자동으로 Trace ID, Span ID가 MDC에 설정됨
@RestController
public class UserController {
    
    @GetMapping("/users/{id}")
    public User getUser(@PathVariable Long id) {
        log.info("Getting user: id={}", id);
        return userService.getUser(id);
    }
}

4. 로그 수집

4.1 로그 수집 아키텍처

대규모 시스템에서는 중앙화된 로그 수집 및 분석 시스템이 필요합니다.

로그 수집 시스템의 구성 요소

  • 수집(Collection): 애플리케이션에서 로그 수집
  • 전송(Shipping): 로그를 중앙 저장소로 전송
  • 저장(Storage): 로그 데이터 저장 및 인덱싱
  • 검색(Search): 로그 검색 및 분석
  • 시각화(Visualization): 대시보드 및 알림

4.2 ELK Stack

ELK Stack 구성

# docker-compose.yml
version: '3.8'
services:
  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:8.11.0
    environment:
      - discovery.type=single-node
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
      - xpack.security.enabled=false
    ports:
      - "9200:9200"

  logstash:
    image: docker.elastic.co/logstash/logstash:8.11.0
    ports:
      - "5044:5044"
    volumes:
      - ./logstash/config:/usr/share/logstash/pipeline

  kibana:
    image: docker.elastic.co/kibana/kibana:8.11.0
    ports:
      - "5601:5601"
    environment:
      - ELASTICSEARCH_HOSTS=http://elasticsearch:9200

Logstash 설정

# logstash/config/logstash.conf
input {
  beats {
    port => 5044
  }
  
  tcp {
    port => 5000
    codec => json_lines
  }
}

filter {
  if [message] =~ /^{.*}$/ {
    json {
      source => "message"
    }
  }
  
  date {
    match => [ "timestamp", "yyyy-MM-dd'T'HH:mm:ss.SSSZ" ]
    target => "@timestamp"
  }
  
  mutate {
    uppercase => [ "level" ]
  }
}

output {
  elasticsearch {
    hosts => ["elasticsearch:9200"]
    index => "application-logs-%{+YYYY.MM.dd}"
  }
}

4.3 Loki

Loki + Promtail 설정

# docker-compose.yml
services:
  loki:
    image: grafana/loki:2.9.0
    ports:
      - "3100:3100"

  promtail:
    image: grafana/promtail:2.9.0
    volumes:
      - /var/log:/var/log:ro
      - ./promtail-config.yaml:/etc/promtail/config.yml

  grafana:
    image: grafana/grafana:10.2.0
    ports:
      - "3000:3000"

Spring Boot Loki 통합

// build.gradle
dependencies {
    implementation 'com.github.loki4j:loki-logback-appender:1.4.2'
}

// logback-spring.xml
<appender name="LOKI" class="com.github.loki4j.logback.Loki4jAppender">
    <http>
        <url>http://loki:3100/loki/api/v1/push</url>
    </http>
    <format>
        <label>
            <pattern>service=myapp,environment=prod,level=%level</pattern>
        </label>
        <message>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{traceId:-}] %logger{36} - %msg%n</pattern>
        </message>
    </format>
    <batchSize>100</batchSize>
    <batchTimeoutMs>10000</batchTimeoutMs>
</appender>

4.4 CloudWatch Logs

CloudWatch Logs 설정

// build.gradle
dependencies {
    implementation 'ca.pjer:logback-awslogs-appender:1.6.0'
}

// logback-spring.xml
<springProfile name="aws">
    <appender name="AWS_LOGS" class="ca.pjer.logback.AwsLogsAppender">
        <layout>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{traceId:-}] %logger{36} - %msg%n</pattern>
        </layout>
        <logGroupName>/aws/ec2/myapp</logGroupName>
        <logStreamUuidPrefix>myapp-</logStreamUuidPrefix>
        <logRegion>us-west-2</logRegion>
        <maxBatchLogEvents>50</maxBatchLogEvents>
        <maxFlushTimeMillis>30000</maxFlushTimeMillis>
    </appender>
    
    <root level="INFO">
        <appender-ref ref="AWS_LOGS"/>
    </root>
</springProfile>

CloudWatch Logs Insights 쿼리

# 에러 로그 검색
fields @timestamp, @message
| filter @message like /ERROR/
| sort @timestamp desc
| limit 100

# 특정 사용자의 활동 로그
fields @timestamp, @message, userId, traceId
| filter userId = "user123"
| sort @timestamp desc

# 응답 시간 분석
fields @timestamp, @message
| filter @message like /completed in/
| parse @message /completed in (?<duration>\d+)ms/
| stats avg(duration), max(duration) by bin(5m)

로그 수집 시스템 비교

특징ELK StackLokiCloudWatch
설치 복잡도높음중간낮음 (관리형)
비용인프라 비용인프라 비용사용량 기반
확장성수동 확장수동 확장자동 확장
검색 성능매우 우수우수우수

5. 성능 최적화

5.1 로깅 성능 이슈

부적절한 로깅은 애플리케이션 성능에 심각한 영향을 미칠 수 있습니다.

로깅 성능 문제점

  • 동기 I/O: 로그 쓰기 시 스레드 블로킹
  • 문자열 연산: 불필요한 문자열 생성 및 연결
  • 과도한 로깅: 너무 많은 로그 생성
  • 디스크 I/O: 파일 시스템 병목
  • 메모리 사용: 로그 버퍼링으로 인한 메모리 증가

5.2 비동기 로깅

AsyncAppender 설정

<!-- logback-spring.xml -->
<configuration>
    <!-- 실제 파일 Appender -->
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>logs/application.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <fileNamePattern>logs/application.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <maxFileSize>100MB</maxFileSize>
            <maxHistory>30</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>
    
    <!-- 비동기 Appender -->
    <appender name="ASYNC_FILE" class="ch.qos.logback.classic.AsyncAppender">
        <appender-ref ref="FILE"/>
        <queueSize>1024</queueSize>
        <discardingThreshold>20</discardingThreshold>
        <maxFlushTime>2000</maxFlushTime>
        <includeCallerData>false</includeCallerData>
        <neverBlock>true</neverBlock>
    </appender>
    
    <springProfile name="prod">
        <root level="INFO">
            <appender-ref ref="ASYNC_FILE"/>
        </root>
    </springProfile>
</configuration>

5.3 로그 레벨 조정

환경별 로그 레벨 최적화

# application-dev.yml (개발 환경)
logging:
  level:
    root: DEBUG
    com.example.myapp: TRACE
    org.springframework.web: DEBUG
    org.hibernate.SQL: DEBUG

# application-prod.yml (프로덕션 환경)
logging:
  level:
    root: WARN
    com.example.myapp: INFO
    org.springframework: ERROR
    org.hibernate: ERROR

동적 로그 레벨 조정

@RestController
@RequestMapping("/admin/logging")
public class LoggingController {
    
    @PostMapping("/level")
    public ResponseEntity<String> changeLogLevel(
            @RequestParam String loggerName,
            @RequestParam String level) {
        
        LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
        ch.qos.logback.classic.Logger logger = loggerContext.getLogger(loggerName);
        
        Level newLevel = Level.valueOf(level.toUpperCase());
        logger.setLevel(newLevel);
        
        return ResponseEntity.ok(
            String.format("Logger '%s' level changed to '%s'", loggerName, level));
    }
    
    @GetMapping("/loggers")
    public ResponseEntity<List<Map<String, String>>> getAllLoggers() {
        LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
        
        List<Map<String, String>> loggers = loggerContext.getLoggerList().stream()
            .filter(logger -> logger.getLevel() != null)
            .map(logger -> {
                Map<String, String> loggerInfo = new HashMap<>();
                loggerInfo.put("name", logger.getName());
                loggerInfo.put("level", logger.getLevel().toString());
                loggerInfo.put("effectiveLevel", logger.getEffectiveLevel().toString());
                return loggerInfo;
            })
            .collect(Collectors.toList());
            
        return ResponseEntity.ok(loggers);
    }
}

5.4 로그 샘플링

조건부 로깅 최적화

@Service
@Slf4j
public class OptimizedLoggingService {
    
    // ❌ 나쁜 예: 항상 문자열 연산 수행
    public void badLogging(List<User> users) {
        log.debug("Processing users: " + users.stream()
            .map(User::toString)
            .collect(Collectors.joining(", ")));
    }
    
    // ✅ 좋은 예: 로그 레벨 체크 후 연산
    public void goodLogging(List<User> users) {
        if (log.isDebugEnabled()) {
            String userList = users.stream()
                .map(User::toString)
                .collect(Collectors.joining(", "));
            log.debug("Processing users: {}", userList);
        }
    }
    
    // 대용량 데이터 로깅 시 샘플링
    public void processLargeDataset(List<Data> dataList) {
        for (int i = 0; i < dataList.size(); i++) {
            Data data = dataList.get(i);
            
            // 100개마다 한 번씩만 로깅
            if (i % 100 == 0) {
                log.info("Processing data batch: index={}, total={}", i, dataList.size());
            }
            
            processData(data);
        }
    }
    
    // 성능 측정 로깅
    public void performanceLogging() {
        long startTime = System.currentTimeMillis();
        
        try {
            performBusinessLogic();
        } finally {
            long duration = System.currentTimeMillis() - startTime;
            
            // 임계값을 초과한 경우에만 로깅
            if (duration > 1000) {
                log.warn("Slow operation detected: duration={}ms", duration);
            } else if (log.isDebugEnabled()) {
                log.debug("Operation completed: duration={}ms", duration);
            }
        }
    }
}

5.5 메모리 및 디스크 최적화

로그 파일 관리 최적화

<!-- 최적화된 롤링 정책 -->
<appender name="OPTIMIZED_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <file>logs/application.log</file>
    
    <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
        <!-- 시간과 크기 기반 롤링 -->
        <fileNamePattern>logs/application.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
        
        <!-- 파일당 최대 크기 -->
        <maxFileSize>50MB</maxFileSize>
        
        <!-- 보관 기간 (일) -->
        <maxHistory>7</maxHistory>
        
        <!-- 전체 로그 파일 크기 제한 -->
        <totalSizeCap>1GB</totalSizeCap>
        
        <!-- 애플리케이션 시작 시 오래된 파일 정리 -->
        <cleanHistoryOnStart>true</cleanHistoryOnStart>
    </rollingPolicy>
    
    <encoder>
        <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        <!-- 즉시 플러시 비활성화 (성능 향상) -->
        <immediateFlush>false</immediateFlush>
    </encoder>
    
    <!-- 버퍼 크기 증가 -->
    <bufferSize>8192</bufferSize>
</appender>

성능 튜닝 가이드

항목권장사항성능 영향
로그 레벨프로덕션: INFO 이상높음
비동기 로깅프로덕션 환경 필수매우 높음
파일 크기50-100MB로 제한중간
압축롤링 시 gzip 압축낮음

6. 실전 활용

6.1 에러 추적

효과적인 에러 추적을 위한 로깅 전략과 구현 방법입니다.

글로벌 예외 처리기

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
    
    @ExceptionHandler(BusinessException.class)
    public ResponseEntity<ErrorResponse> handleBusinessException(
            BusinessException e, HttpServletRequest request) {
        
        String errorId = UUID.randomUUID().toString();
        
        log.warn("Business exception occurred: errorId={}, code={}, message={}, uri={}, method={}", 
                 errorId, e.getErrorCode(), e.getMessage(), 
                 request.getRequestURI(), request.getMethod());
        
        ErrorResponse response = ErrorResponse.builder()
            .errorId(errorId)
            .code(e.getErrorCode())
            .message(e.getMessage())
            .timestamp(LocalDateTime.now())
            .build();
            
        return ResponseEntity.badRequest().body(response);
    }
    
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleUnexpectedException(
            Exception e, HttpServletRequest request) {
        
        String errorId = UUID.randomUUID().toString();
        
        // 예상치 못한 에러는 ERROR 레벨로 로깅
        log.error("Unexpected exception occurred: errorId={}, uri={}, method={}, userAgent={}", 
                  errorId, request.getRequestURI(), request.getMethod(), 
                  request.getHeader("User-Agent"), e);
        
        ErrorResponse response = ErrorResponse.builder()
            .errorId(errorId)
            .code("INTERNAL_SERVER_ERROR")
            .message("An unexpected error occurred. Please contact support with error ID: " + errorId)
            .timestamp(LocalDateTime.now())
            .build();
            
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
    }
    
    @ExceptionHandler(ValidationException.class)
    public ResponseEntity<ErrorResponse> handleValidationException(
            ValidationException e, HttpServletRequest request) {
        
        String errorId = UUID.randomUUID().toString();
        
        log.info("Validation exception: errorId={}, field={}, value={}, message={}", 
                 errorId, e.getField(), e.getRejectedValue(), e.getMessage());
        
        ErrorResponse response = ErrorResponse.builder()
            .errorId(errorId)
            .code("VALIDATION_ERROR")
            .message(e.getMessage())
            .field(e.getField())
            .timestamp(LocalDateTime.now())
            .build();
            
        return ResponseEntity.badRequest().body(response);
    }
}

에러 컨텍스트 수집

@Component
@Slf4j
public class ErrorContextCollector {
    
    public void logErrorWithContext(Exception e, HttpServletRequest request) {
        Map<String, Object> context = collectErrorContext(e, request);
        
        log.error("Error occurred with context: {}", context, e);
    }
    
    private Map<String, Object> collectErrorContext(Exception e, HttpServletRequest request) {
        Map<String, Object> context = new HashMap<>();
        
        // 요청 정보
        context.put("requestUri", request.getRequestURI());
        context.put("httpMethod", request.getMethod());
        context.put("queryString", request.getQueryString());
        context.put("userAgent", request.getHeader("User-Agent"));
        context.put("referer", request.getHeader("Referer"));
        context.put("clientIp", getClientIpAddress(request));
        
        // 사용자 정보
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        if (auth != null && auth.isAuthenticated()) {
            context.put("username", auth.getName());
            context.put("authorities", auth.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority)
                .collect(Collectors.toList()));
        }
        
        // 세션 정보
        HttpSession session = request.getSession(false);
        if (session != null) {
            context.put("sessionId", session.getId());
            context.put("sessionCreationTime", new Date(session.getCreationTime()));
            context.put("sessionLastAccessedTime", new Date(session.getLastAccessedTime()));
        }
        
        // 예외 정보
        context.put("exceptionClass", e.getClass().getSimpleName());
        context.put("exceptionMessage", e.getMessage());
        
        // 시스템 정보
        context.put("timestamp", LocalDateTime.now());
        context.put("serverName", getServerName());
        context.put("jvmMemoryUsage", getMemoryUsage());
        
        return context;
    }
    
    private String getClientIpAddress(HttpServletRequest request) {
        String xForwardedFor = request.getHeader("X-Forwarded-For");
        if (xForwardedFor != null && !xForwardedFor.isEmpty()) {
            return xForwardedFor.split(",")[0].trim();
        }
        
        String xRealIp = request.getHeader("X-Real-IP");
        if (xRealIp != null && !xRealIp.isEmpty()) {
            return xRealIp;
        }
        
        return request.getRemoteAddr();
    }
    
    private Map<String, Object> getMemoryUsage() {
        MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean();
        MemoryUsage heapUsage = memoryBean.getHeapMemoryUsage();
        
        Map<String, Object> memory = new HashMap<>();
        memory.put("heapUsed", heapUsage.getUsed());
        memory.put("heapMax", heapUsage.getMax());
        memory.put("heapUsedPercent", (double) heapUsage.getUsed() / heapUsage.getMax() * 100);
        
        return memory;
    }
}

6.2 감사 로그 (Audit Log)

감사 로그 AOP

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Auditable {
    String action() default "";
    String resource() default "";
    boolean logParameters() default false;
    boolean logResult() default false;
}

@Aspect
@Component
@Slf4j
public class AuditAspect {
    
    private static final Logger auditLogger = LoggerFactory.getLogger("AUDIT");
    
    @Around("@annotation(auditable)")
    public Object audit(ProceedingJoinPoint joinPoint, Auditable auditable) throws Throwable {
        String auditId = UUID.randomUUID().toString();
        long startTime = System.currentTimeMillis();
        
        // 감사 로그 시작
        AuditLog auditLog = AuditLog.builder()
            .auditId(auditId)
            .action(auditable.action().isEmpty() ? joinPoint.getSignature().getName() : auditable.action())
            .resource(auditable.resource())
            .username(getCurrentUsername())
            .timestamp(LocalDateTime.now())
            .clientIp(getCurrentClientIp())
            .userAgent(getCurrentUserAgent())
            .build();
        
        if (auditable.logParameters()) {
            auditLog.setParameters(Arrays.toString(joinPoint.getArgs()));
        }
        
        auditLogger.info("Audit started: {}", auditLog);
        
        try {
            Object result = joinPoint.proceed();
            
            // 성공 로그
            long duration = System.currentTimeMillis() - startTime;
            auditLog.setStatus("SUCCESS");
            auditLog.setDuration(duration);
            
            if (auditable.logResult() && result != null) {
                auditLog.setResult(result.toString());
            }
            
            auditLogger.info("Audit completed: {}", auditLog);
            
            return result;
            
        } catch (Exception e) {
            // 실패 로그
            long duration = System.currentTimeMillis() - startTime;
            auditLog.setStatus("FAILURE");
            auditLog.setDuration(duration);
            auditLog.setErrorMessage(e.getMessage());
            
            auditLogger.warn("Audit failed: {}", auditLog, e);
            
            throw e;
        }
    }
    
    private String getCurrentUsername() {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        return auth != null ? auth.getName() : "anonymous";
    }
    
    private String getCurrentClientIp() {
        RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
        if (attrs instanceof ServletRequestAttributes) {
            HttpServletRequest request = ((ServletRequestAttributes) attrs).getRequest();
            return getClientIpAddress(request);
        }
        return "unknown";
    }
}

// 사용 예시
@RestController
public class UserController {
    
    @PostMapping("/users")
    @Auditable(action = "CREATE_USER", resource = "USER", logParameters = true)
    public ResponseEntity<User> createUser(@RequestBody CreateUserRequest request) {
        User user = userService.createUser(request);
        return ResponseEntity.ok(user);
    }
    
    @DeleteMapping("/users/{id}")
    @Auditable(action = "DELETE_USER", resource = "USER")
    public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
        userService.deleteUser(id);
        return ResponseEntity.noContent().build();
    }
}

6.3 보안 로그

보안 이벤트 로깅

@Component
@Slf4j
public class SecurityEventLogger {
    
    private static final Logger securityLogger = LoggerFactory.getLogger("SECURITY");
    private static final Marker SECURITY_MARKER = MarkerFactory.getMarker("SECURITY");
    
    public void logLoginAttempt(String username, String clientIp, boolean success) {
        SecurityEvent event = SecurityEvent.builder()
            .eventType("LOGIN_ATTEMPT")
            .username(username)
            .clientIp(clientIp)
            .success(success)
            .timestamp(LocalDateTime.now())
            .build();
            
        if (success) {
            securityLogger.info(SECURITY_MARKER, "Login successful: {}", event);
        } else {
            securityLogger.warn(SECURITY_MARKER, "Login failed: {}", event);
        }
    }
    
    public void logPasswordChange(String username, String clientIp) {
        SecurityEvent event = SecurityEvent.builder()
            .eventType("PASSWORD_CHANGE")
            .username(username)
            .clientIp(clientIp)
            .timestamp(LocalDateTime.now())
            .build();
            
        securityLogger.info(SECURITY_MARKER, "Password changed: {}", event);
    }
    
    public void logSuspiciousActivity(String username, String clientIp, String activity, String details) {
        SecurityEvent event = SecurityEvent.builder()
            .eventType("SUSPICIOUS_ACTIVITY")
            .username(username)
            .clientIp(clientIp)
            .activity(activity)
            .details(details)
            .timestamp(LocalDateTime.now())
            .build();
            
        securityLogger.error(SECURITY_MARKER, "Suspicious activity detected: {}", event);
    }
    
    public void logPrivilegeEscalation(String username, String fromRole, String toRole) {
        SecurityEvent event = SecurityEvent.builder()
            .eventType("PRIVILEGE_ESCALATION")
            .username(username)
            .fromRole(fromRole)
            .toRole(toRole)
            .timestamp(LocalDateTime.now())
            .build();
            
        securityLogger.warn(SECURITY_MARKER, "Privilege escalation: {}", event);
    }
    
    public void logDataAccess(String username, String resource, String action) {
        SecurityEvent event = SecurityEvent.builder()
            .eventType("DATA_ACCESS")
            .username(username)
            .resource(resource)
            .action(action)
            .timestamp(LocalDateTime.now())
            .build();
            
        securityLogger.info(SECURITY_MARKER, "Data access: {}", event);
    }
}

@Component
public class SecurityEventListener {
    
    @Autowired
    private SecurityEventLogger securityEventLogger;
    
    @EventListener
    public void handleAuthenticationSuccess(AuthenticationSuccessEvent event) {
        String username = event.getAuthentication().getName();
        String clientIp = getClientIp();
        
        securityEventLogger.logLoginAttempt(username, clientIp, true);
    }
    
    @EventListener
    public void handleAuthenticationFailure(AbstractAuthenticationFailureEvent event) {
        String username = event.getAuthentication().getName();
        String clientIp = getClientIp();
        
        securityEventLogger.logLoginAttempt(username, clientIp, false);
    }
    
    @EventListener
    public void handleLogoutSuccess(LogoutSuccessEvent event) {
        String username = event.getAuthentication().getName();
        String clientIp = getClientIp();
        
        securityEventLogger.logLogout(username, clientIp);
    }
}

6.4 성능 모니터링 로그

성능 측정 AOP

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PerformanceMonitor {
    long threshold() default 1000; // 밀리초
    boolean logParameters() default false;
}

@Aspect
@Component
@Slf4j
public class PerformanceMonitorAspect {
    
    private static final Logger performanceLogger = LoggerFactory.getLogger("PERFORMANCE");
    private static final Marker PERFORMANCE_MARKER = MarkerFactory.getMarker("PERFORMANCE");
    
    @Around("@annotation(performanceMonitor)")
    public Object monitor(ProceedingJoinPoint joinPoint, PerformanceMonitor performanceMonitor) throws Throwable {
        String methodName = joinPoint.getSignature().toShortString();
        long startTime = System.currentTimeMillis();
        
        try {
            Object result = joinPoint.proceed();
            
            long duration = System.currentTimeMillis() - startTime;
            
            PerformanceMetric metric = PerformanceMetric.builder()
                .methodName(methodName)
                .duration(duration)
                .success(true)
                .timestamp(LocalDateTime.now())
                .build();
            
            if (performanceMonitor.logParameters()) {
                metric.setParameters(Arrays.toString(joinPoint.getArgs()));
            }
            
            if (duration > performanceMonitor.threshold()) {
                performanceLogger.warn(PERFORMANCE_MARKER, "Slow method execution: {}", metric);
            } else {
                performanceLogger.debug(PERFORMANCE_MARKER, "Method execution: {}", metric);
            }
            
            return result;
            
        } catch (Exception e) {
            long duration = System.currentTimeMillis() - startTime;
            
            PerformanceMetric metric = PerformanceMetric.builder()
                .methodName(methodName)
                .duration(duration)
                .success(false)
                .errorMessage(e.getMessage())
                .timestamp(LocalDateTime.now())
                .build();
            
            performanceLogger.error(PERFORMANCE_MARKER, "Method execution failed: {}", metric, e);
            
            throw e;
        }
    }
}

// 사용 예시
@Service
public class UserService {
    
    @PerformanceMonitor(threshold = 500)
    public List<User> findUsers(UserSearchCriteria criteria) {
        return userRepository.findByCriteria(criteria);
    }
    
    @PerformanceMonitor(threshold = 2000, logParameters = true)
    public void processLargeDataset(List<Data> dataList) {
        // 대용량 데이터 처리
    }
}

실전 로깅 체크리스트

  • ✅ 모든 예외에 고유한 에러 ID 부여
  • ✅ 사용자 액션에 대한 감사 로그 기록
  • ✅ 보안 이벤트 별도 로거로 분리
  • ✅ 성능 임계값 초과 시 경고 로그
  • ✅ 민감한 정보 마스킹 처리
  • ✅ 요청/응답 추적을 위한 상관관계 ID
  • ✅ 외부 API 호출 성공/실패 로깅
  • ✅ 데이터베이스 트랜잭션 로깅
  • ✅ 캐시 히트/미스 로깅
  • ✅ 비즈니스 메트릭 로깅

7. 정리

7.1 로깅 설계 가이드

효과적인 로깅 시스템 설계를 위한 종합적인 가이드입니다.

로깅 설계 원칙

  • 목적 중심: 로그의 목적을 명확히 정의 (디버깅, 모니터링, 감사 등)
  • 구조화: 일관된 형식과 구조로 로그 작성
  • 컨텍스트: 충분한 컨텍스트 정보 포함
  • 성능 고려: 애플리케이션 성능에 미치는 영향 최소화
  • 보안: 민감한 정보 노출 방지
  • 확장성: 시스템 규모 증가에 대응 가능한 설계

로깅 아키텍처 설계

// 로깅 계층 구조
Application Layer
    ├── Business Logic Logging
    │   ├── User Actions
    │   ├── Business Events
    │   └── State Changes
    │
    ├── Technical Logging
    │   ├── Performance Metrics
    │   ├── Error Handling
    │   └── System Events
    │
    └── Security Logging
        ├── Authentication Events
        ├── Authorization Events
        └── Audit Trail

Infrastructure Layer
    ├── Log Collection
    │   ├── File-based (Logback)
    │   ├── Network-based (TCP/UDP)
    │   └── Message Queue (Kafka)
    │
    ├── Log Processing
    │   ├── Parsing & Enrichment
    │   ├── Filtering & Sampling
    │   └── Aggregation
    │
    └── Log Storage & Analysis
        ├── Search Engine (Elasticsearch)
        ├── Time Series DB (InfluxDB)
        └── Data Warehouse (BigQuery)

7.2 베스트 프랙티스

로그 메시지 작성 가이드

// ✅ 좋은 로그 메시지 예시
@Service
@Slf4j
public class OrderService {
    
    public Order createOrder(CreateOrderRequest request) {
        // 1. 비즈니스 이벤트 시작 로깅
        log.info("Order creation started: userId={}, productCount={}, totalAmount={}", 
                 request.getUserId(), request.getItems().size(), request.getTotalAmount());
        
        try {
            // 2. 중요한 비즈니스 로직 단계별 로깅
            User user = userService.getUser(request.getUserId());
            log.debug("User retrieved: userId={}, userType={}", user.getId(), user.getType());
            
            // 3. 외부 시스템 호출 로깅
            PaymentResult paymentResult = paymentService.processPayment(request.getPayment());
            log.info("Payment processed: orderId={}, paymentId={}, status={}, amount={}", 
                     request.getOrderId(), paymentResult.getPaymentId(), 
                     paymentResult.getStatus(), paymentResult.getAmount());
            
            // 4. 데이터 변경 로깅
            Order order = orderRepository.save(new Order(request));
            log.info("Order created successfully: orderId={}, userId={}, status={}, totalAmount={}", 
                     order.getId(), order.getUserId(), order.getStatus(), order.getTotalAmount());
            
            // 5. 비즈니스 메트릭 로깅
            log.info("Order metrics: orderId={}, processingTime={}ms, itemCount={}", 
                     order.getId(), System.currentTimeMillis() - startTime, order.getItems().size());
            
            return order;
            
        } catch (PaymentException e) {
            // 6. 비즈니스 예외 로깅 (상세 컨텍스트 포함)
            log.warn("Order creation failed due to payment issue: userId={}, orderId={}, " +
                     "paymentMethod={}, amount={}, reason={}", 
                     request.getUserId(), request.getOrderId(), 
                     request.getPayment().getMethod(), request.getTotalAmount(), e.getReason());
            throw e;
            
        } catch (Exception e) {
            // 7. 시스템 예외 로깅 (전체 컨텍스트 포함)
            log.error("Unexpected error during order creation: userId={}, orderId={}, " +
                      "requestData={}", request.getUserId(), request.getOrderId(), 
                      request, e);
            throw new OrderCreationException("Failed to create order", e);
        }
    }
}

// ❌ 나쁜 로그 메시지 예시
public class BadLoggingExample {
    
    public void badLogging() {
        // 너무 간단한 메시지
        log.info("Processing started");
        
        // 의미 없는 메시지
        log.debug("Debug message");
        
        // 컨텍스트 부족
        log.error("Error occurred");
        
        // 민감한 정보 노출
        log.info("User login: password={}", password);
        
        // 불필요한 상세 정보
        log.info("Processing item 1 of 10000");
        log.info("Processing item 2 of 10000");
        // ...
    }
}

로그 레벨 사용 가이드라인

// 로그 레벨별 사용 가이드
public class LogLevelGuide {
    
    // TRACE: 매우 상세한 디버깅 정보
    public void traceExample() {
        log.trace("Entering method: calculateTax(amount={})", amount);
        log.trace("Tax calculation step 1: baseRate={}", baseRate);
        log.trace("Tax calculation step 2: adjustedRate={}", adjustedRate);
        log.trace("Exiting method: calculateTax, result={}", result);
    }
    
    // DEBUG: 개발 시 필요한 디버깅 정보
    public void debugExample() {
        log.debug("Cache lookup: key={}, found={}", cacheKey, found);
        log.debug("Database query: sql={}, parameters={}", sql, params);
        log.debug("Validation result: field={}, valid={}", field, isValid);
    }
    
    // INFO: 일반적인 정보성 메시지
    public void infoExample() {
        log.info("Application started on port {}", port);
        log.info("User logged in: userId={}", userId);
        log.info("Order processed: orderId={}, amount={}", orderId, amount);
        log.info("Batch job completed: processed={}, failed={}", processed, failed);
    }
    
    // WARN: 잠재적 문제 상황
    public void warnExample() {
        log.warn("Deprecated API used: endpoint={}, client={}", endpoint, clientId);
        log.warn("Retry attempt: operation={}, attempt={}/{}", operation, attempt, maxAttempts);
        log.warn("Resource usage high: memory={}%, threshold={}%", memoryUsage, threshold);
        log.warn("Configuration missing: key={}, using default={}", key, defaultValue);
    }
    
    // ERROR: 오류 발생
    public void errorExample() {
        log.error("Database connection failed: url={}, timeout={}ms", dbUrl, timeout, e);
        log.error("External API call failed: url={}, status={}", apiUrl, statusCode, e);
        log.error("Business rule violation: rule={}, data={}", ruleName, data, e);
        log.error("Unexpected system error: operation={}", operation, e);
    }
}

7.3 로깅 성숙도 모델

로깅 성숙도 단계

Level 1: 기본 로깅
  • • System.out.println 또는 기본 로거 사용
  • • 일관성 없는 로그 형식
  • • 에러 발생 시에만 로깅
  • • 로그 레벨 구분 없음
Level 2: 구조화된 로깅
  • • SLF4J + Logback 사용
  • • 로그 레벨 적절히 구분
  • • 일관된 로그 형식
  • • 파일 롤링 정책 적용
Level 3: 컨텍스트 로깅
  • • MDC를 통한 컨텍스트 정보 추가
  • • 요청 추적 ID 사용
  • • JSON 형태의 구조화된 로그
  • • 환경별 로그 설정 분리
Level 4: 중앙화된 로깅
  • • ELK Stack 또는 유사한 중앙 로그 시스템
  • • 실시간 로그 모니터링
  • • 알림 및 대시보드 구성
  • • 로그 기반 메트릭 수집
Level 5: 지능형 로깅
  • • 머신러닝 기반 이상 탐지
  • • 자동화된 로그 분석
  • • 예측적 알림
  • • 비즈니스 인사이트 도출

7.4 로깅 체크리스트

개발 단계 체크리스트

설계 단계
  • ☐ 로깅 목적 및 요구사항 정의
  • ☐ 로그 레벨 전략 수립
  • ☐ 로그 형식 및 구조 설계
  • ☐ 성능 영향 분석
  • ☐ 보안 요구사항 검토
구현 단계
  • ☐ 로깅 프레임워크 설정
  • ☐ 환경별 설정 분리
  • ☐ 비동기 로깅 적용
  • ☐ MDC 컨텍스트 설정
  • ☐ 예외 처리 로깅
테스트 단계
  • ☐ 로그 출력 검증
  • ☐ 성능 영향 측정
  • ☐ 로그 레벨 동작 확인
  • ☐ 롤링 정책 테스트
  • ☐ 에러 시나리오 테스트
운영 단계
  • ☐ 로그 모니터링 설정
  • ☐ 알림 규칙 구성
  • ☐ 대시보드 구성
  • ☐ 로그 보관 정책 적용
  • ☐ 정기적인 로그 분석

7.5 마무리

핵심 요약

1. 로깅 기본 원칙: 목적에 맞는 로그 레벨 사용, 구조화된 메시지 작성, 충분한 컨텍스트 정보 포함

2. 성능 최적화: 비동기 로깅 적용, 적절한 로그 레벨 설정, 불필요한 로깅 제거

3. 구조화 로깅: JSON 형태의 로그, MDC를 통한 컨텍스트 추가, 요청 추적 ID 사용

4. 중앙화 관리: ELK Stack 또는 유사한 시스템으로 중앙 집중식 로그 관리

5. 보안 고려: 민감한 정보 마스킹, 보안 이벤트 별도 로깅, 감사 로그 구현

6. 지속적 개선: 로그 분석을 통한 인사이트 도출, 모니터링 및 알림 체계 구축

다음 단계 학습 권장사항

  • 분산 추적: Spring Cloud Sleuth, Zipkin, Jaeger 학습
  • 메트릭 수집: Micrometer, Prometheus 연동
  • APM 도구: New Relic, Datadog, AppDynamics 활용
  • 로그 분석: 머신러닝 기반 이상 탐지 기법
  • 클라우드 로깅: AWS CloudWatch, GCP Cloud Logging, Azure Monitor
  • 컨테이너 로깅: Docker, Kubernetes 환경에서의 로깅 전략