Logging, 좋은 로깅이란 무엇인가에서 만든 기능들을 직접 구현하는 대신, 검증된 라이브러리를 사용하자.
의존성 추가 (Gradle)
dependencies {
implementation 'org.slf4j:slf4j-api:2.0.9'
implementation 'ch.qos.logback:logback-classic:1.4.11'
}
Spring Boot 는 이미 SLF4J 와 Logback 을 포함하고 있다.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter'
// spring-boot-starter에 이미 slf4j와 logback이 포함되어 있음
}
기본 사용법
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class OrderService {
private static final Logger logger = LoggerFactory.getLogger(OrderService.class);
public void order(String userId, Long productId, int quantity) {
logger.info("주문 시작: userId={}, productId={}", userId, productId);
logger.debug("주문 수량: {}", quantity);
try {
processOrder(userId, productId, quantity);
logger.info("주문 완료");
} catch (InsufficientStockException e) {
logger.error("재고 부족으로 주문 실패: userId={}, productId={}",
userId, productId, e);
} catch (Exception e) {
logger.error("예상치 못한 오류 발생", e);
}
}
}
Logback 설정 (logback.xml)
src/main/resources/logback.xml 파일 생성:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 콘솔 출력 -->
<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>
<!-- 파일 출력 (Rolling) -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>/var/log/myapp/application.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 일별 롤링 -->
<fileNamePattern>/var/log/myapp/application.%d{yyyy-MM-dd}.log</fileNamePattern>
<!-- 30일 보관 -->
<maxHistory>30</maxHistory>
<!-- 전체 로그 파일 최대 크기 -->
<totalSizeCap>10GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- 에러 전용 파일 -->
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>/var/log/myapp/error.log</file>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>/var/log/myapp/error.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>90</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- 패키지별 로그 레벨 설정 -->
<logger name="com.example.order" level="DEBUG"/>
<logger name="com.example.payment" level="INFO"/>
<logger name="com.example.external" level="WARN"/>
<!-- Hibernate SQL 로그 (개발 환경) -->
<logger name="org.hibernate.SQL" level="DEBUG"/>
<logger name="org.hibernate.type.descriptor.sql.BasicBinder" level="TRACE"/>
<!-- 루트 로거 -->
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE"/>
<appender-ref ref="ERROR_FILE"/>
</root>
</configuration>
Spring Boot application.yml 설정
logging:
level:
root: INFO
com.example.order: DEBUG
com.example.payment: INFO
org.hibernate.SQL: DEBUG
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss} - %msg%n"
file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
file:
name: /var/log/myapp/application.log
logback:
rollingpolicy:
max-file-size: 10MB
max-history: 30
아키텍처 시각화
graph TB
subgraph "애플리케이션 코드"
A[OrderService]
B[PaymentService]
C[ExternalApiService]
end
subgraph "SLF4J API Layer"
D[Logger Interface]
end
subgraph "Logback Implementation"
E[Logger Implementation]
F[LoggerContext]
end
subgraph "Appenders"
G[ConsoleAppender]
H[RollingFileAppender]
I[SMTPAppender]
J[CustomAppender]
end
subgraph "출력 대상"
K[콘솔]
L[파일 시스템]
M[이메일]
N[Kafka/ES]
end
A --> D
B --> D
C --> D
D --> E
E --> F
F --> G
F --> H
F --> I
F --> J
G --> K
H --> L
I --> M
J --> N
style D fill:#e1f5ff
style E fill:#fff4e1
style F fill:#fff4e1
로그 레벨별 흐름
flowchart LR
A[로그 요청] --> B{로거 레벨 체크}
B -->|DEBUG 이상| C[메시지 포맷팅]
B -->|레벨 미달| Z[무시]
C --> D[Appender 탐색]
D --> E{Appender Filter}
E -->|통과| F[ConsoleAppender]
E -->|통과| G[FileAppender]
E -->|통과| H[ErrorFileAppender]
E -->|차단| Z
F --> I[콘솔 출력]
G --> J[일반 로그 파일]
H --> K[에러 로그 파일]
style B fill:#ffe1e1
style E fill:#ffe1e1
style Z fill:#f0f0f0
패키지별 로그 레벨 관리
graph TD
A[LoggerFactory] --> B{패키지 확인}
B -->|com.example.order| C[DEBUG]
B -->|com.example.payment| D[INFO]
B -->|com.example.external| E[WARN]
B -->|기타| F[INFO 기본값]
C --> G[OrderService Logger]
D --> H[PaymentService Logger]
E --> I[ExternalApiService Logger]
F --> J[기타 Logger]
style A fill:#e1f5ff
style B fill:#fff4e1
Appender 처리 흐름
sequenceDiagram
participant App as Application
participant Logger as SLF4J Logger
participant Logback as Logback
participant Console as ConsoleAppender
participant File as FileAppender
participant Error as ErrorFileAppender
App->>Logger: logger.error("오류 발생", ex)
Logger->>Logback: 로그 이벤트 전달
Logback->>Logback: 레벨 체크 (ERROR >= INFO)
Logback->>Console: append(로그)
Console->>Console: 콘솔 출력
Logback->>File: append(로그)
File->>File: application.log 기록
Logback->>Error: append(로그)
Error->>Error: 레벨 필터 체크 (ERROR)
Error->>Error: error.log 기록
주요 포인트 정리
-
SLF4J는 인터페이스, Logback은 구현체
- SLF4J API만 사용하면 구현체 교체 가능 (Logback ↔ Log4j2)
-
로그 레벨 순서
- TRACE < DEBUG < INFO < WARN < ERROR
- INFO 설정 시: INFO, WARN, ERROR만 출력
-
성능 최적화
- 조건부 로깅:
if (logger.isDebugEnabled()) { logger.debug(...) } - 파라미터 바인딩:
logger.info("userId={}", userId)(문자열 연결 X)
- 조건부 로깅:
-
운영 환경 권장 설정
- 루트 레벨: INFO
- 핵심 비즈니스 패키지: DEBUG
- 외부 라이브러리: WARN
- 에러 로그는 별도 파일 + 알림 연동
참고 자료:
이 글에 대해 이야기해요
질문이나 다른 관점을 남겨 주세요.