Spring 18: 로깅
효과적인 로깅 전략
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)
로그 레벨은 로그 메시지의 중요도와 심각도를 나타냅니다. 적절한 로그 레벨 사용은 효과적인 로깅의 핵심입니다.
로그 레벨 계층 구조 (낮음 → 높음)
가장 상세한 정보. 코드 실행 경로 추적용
사용 시기: 매우 세밀한 디버깅이 필요할 때
개발 중 디버깅 정보. 변수 값, 메서드 호출 등
사용 시기: 개발/테스트 환경에서 문제 진단
일반적인 정보성 메시지. 주요 비즈니스 이벤트
사용 시기: 애플리케이션의 정상 동작 흐름 기록
잠재적 문제 상황. 즉시 처리 불필요하지만 주의 필요
사용 시기: 예상치 못한 상황이지만 복구 가능한 경우
오류 발생. 기능 실패했지만 애플리케이션은 계속 실행
사용 시기: 예외 발생, 비즈니스 로직 실패
로그 레벨 사용 예제
@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);
}
}로그 레벨 선택 가이드
| 상황 | 레벨 | 예시 |
|---|---|---|
| 애플리케이션 시작/종료 | INFO | Application started on port 8080 |
| 사용자 로그인/로그아웃 | INFO | User logged in: userId=123 |
| 주문 생성/완료 | INFO | Order created: orderId=456 |
| 캐시 미스 | DEBUG | Cache miss for key: user:123 |
| 재시도 로직 실행 | WARN | Retrying API call (attempt 2/3) |
| Deprecated API 사용 | WARN | Using deprecated method |
| 데이터베이스 연결 실패 | ERROR | Failed to connect to database |
| 외부 API 호출 실패 | ERROR | Payment 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 + Logback | Log4j2 | JUL |
|---|---|---|---|
| 성능 | 우수 | 매우 우수 | 보통 |
| 설정 방식 | XML, Groovy | XML, JSON, YAML | Properties |
| 비동기 지원 | 지원 | 강력한 지원 | 제한적 |
| 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.log2. 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:9200Logstash 설정
# 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 Stack | Loki | CloudWatch |
|---|---|---|---|
| 설치 복잡도 | 높음 | 중간 | 낮음 (관리형) |
| 비용 | 인프라 비용 | 인프라 비용 | 사용량 기반 |
| 확장성 | 수동 확장 | 수동 확장 | 자동 확장 |
| 검색 성능 | 매우 우수 | 우수 | 우수 |
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 로깅 성숙도 모델
로깅 성숙도 단계
- • System.out.println 또는 기본 로거 사용
- • 일관성 없는 로그 형식
- • 에러 발생 시에만 로깅
- • 로그 레벨 구분 없음
- • SLF4J + Logback 사용
- • 로그 레벨 적절히 구분
- • 일관된 로그 형식
- • 파일 롤링 정책 적용
- • MDC를 통한 컨텍스트 정보 추가
- • 요청 추적 ID 사용
- • JSON 형태의 구조화된 로그
- • 환경별 로그 설정 분리
- • ELK Stack 또는 유사한 중앙 로그 시스템
- • 실시간 로그 모니터링
- • 알림 및 대시보드 구성
- • 로그 기반 메트릭 수집
- • 머신러닝 기반 이상 탐지
- • 자동화된 로그 분석
- • 예측적 알림
- • 비즈니스 인사이트 도출
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 환경에서의 로깅 전략