[BlerOn] Logging 설정
BlerOn Logging 설정
BlerOn Admin Backend(Spring Boot 3.3.4 / Java 21) 프로젝트의 로깅 설정을 정리한 글입니다. 운영 환경에서 로그는 "장애 분석의 1차 자료"이기 때문에, 아래 3가지 관점으로 나누어 설계했습니다.
- profile(local/dev/production)별 로깅 설정 : 환경마다 다른 로그 레벨/출력 대상
- 로깅 관련 라이브러리와 설정 : Logback, SLF4J, log4jdbc, Micrometer Tracing 등
- LogAspect(AOP) 설정 : 모든 컨트롤러의 요청/응답을 공통으로 기록
1. 로깅 아키텍처 한눈에 보기
[HTTP 요청]
│
▼
TraceIdMdcFilter ──▶ MDC 에 traceId, clientIp 저장
│
▼
LoggingAspect(AOP) ──▶ MDC 에 userId, memberSeq 저장 + REQ/RES 로그 기록
│
▼
Controller → Service → Repository
│ │
│ ▼
│ log4jdbc ──▶ 실제 실행 SQL 로그 (바인딩 값 포함)
▼
Logback (logback-spring.xml)
├─ CONSOLE Appender
├─ FILE Appender (롤링)
└─ ERROR_FILE Appender (ERROR만)
- SLF4J : 로깅 API(인터페이스) 역할. 코드에서는
log.info()형태로만 사용 - Logback : SLF4J의 실제 구현체. Spring Boot의 기본 로깅 엔진
- MDC(Mapped Diagnostic Context) : 스레드별 보관소. traceId/userId 같은 "요청 단위 정보"를 로그 패턴에 자동으로 끼워 넣음
핵심 아이디어: 코드에서는 SLF4J로만 로그를 찍고, 출력 형식/대상/레벨은 전부
logback-spring.xml과application-{profile}.yml에서 제어한다.
2. 로깅 관련 라이브러리
build.gradle에서 로깅과 직접 관련된 의존성은 다음과 같습니다.
// Spring Boot 기본 starter에 Logback + SLF4J가 포함되어 있음
implementation 'org.springframework.boot:spring-boot-starter-web'
// AOP (LoggingAspect 동작에 필요)
implementation 'org.springframework.boot:spring-boot-starter-aop'
// 분산 추적(traceId 발급) - Micrometer Tracing + OpenTelemetry
implementation 'io.micrometer:micrometer-tracing-bridge-otel'
// JDBC SQL 로깅 (바인딩된 실제 SQL 출력)
implementation 'org.bgee.log4jdbc-log4j2:log4jdbc-log4j2-jdbc4.1:1.16'
// Logback EvaluatorFilter(조건식 필터)용 - 특정 로그 메시지 필터링에 필요
implementation 'org.codehaus.janino:janino:3.1.12'
| 라이브러리 | 역할 |
|---|---|
spring-boot-starter (내부 logback-classic, slf4j-api) |
로깅 엔진 + API |
spring-boot-starter-aop |
LoggingAspect가 컨트롤러를 가로채기 위한 AOP |
micrometer-tracing-bridge-otel |
요청마다 고유 traceId를 발급 → 로그 추적용 |
log4jdbc-log4j2 |
JPA/Hibernate가 실행하는 실제 SQL(바인딩 값 포함)을 로그로 출력 |
janino |
Logback의 EvaluatorFilter에서 자바 표현식(message.contains(...))을 평가하기 위한 필수 라이브러리 |
2-1. log4jdbc 설정
JPA만 사용하면 ? 자리표시자(placeholder)가 들어간 SQL만 보이지만, log4jdbc를 쓰면 실제 바인딩된 값이 채워진 SQL을 볼 수 있어 디버깅이 훨씬 편합니다.
src/main/resources/log4jdbc.log4j2.properties:
# SQL 앞뒤 공백/줄바꿈 정리
log4jdbc.trim.sql=true
# SQL 한 줄 최대 길이 제한 해제 (0 = 무제한)
log4jdbc.dump.sql.maxlinelength=0
# 감시할 JDBC 드라이버 지정
log4jdbc.drivers=com.mysql.cj.jdbc.Driver
# 인기 드라이버 자동 로드 비활성화 (명시한 드라이버만 사용)
log4jdbc.auto.load.popular.drivers=false
# 로그 출력을 커스텀 클래스에 위임 (SQL 포맷팅 + 결과셋 마스킹)
log4jdbc.spylogdelegator.name=kr.co.bler.infrastructure.config.logging.CustomLogDelegator
여기서 CustomLogDelegator는 log4jdbc의 기본 출력 동작을 확장한 클래스입니다.
public class CustomLogDelegator extends Slf4jSpyLogDelegator {
@Override
public void sqlOccurred(Spy spy, String methodCall, String sql) {
// Hibernate 포맷터로 SQL을 보기 좋게 정렬
BasicFormatterImpl formatter = new BasicFormatterImpl();
sql = formatter.format(sql);
super.sqlOccurred(spy, methodCall, sql);
}
@Override
public void resultSetCollected(ResultSetCollector resultSetCollector) {
// 조회 결과 중 비밀번호/이메일 등 민감 컬럼은 마스킹 후 출력
ResultSetCollector maskingCollector =
new MaskingResultSetCollector(resultSetCollector, MASK_COLUMN_NAMES, REPLACE_COLUMN_NAMES);
String resultsToPrint = new ResultSetCollectorPrinter().getResultSetToPrint(maskingCollector);
org.slf4j.LoggerFactory.getLogger("jdbc.resultsettable").info(resultsToPrint);
}
}
이렇게 하면 SQL 로그가 자동으로 포맷팅 + 결과셋 민감정보 마스킹되어 출력됩니다.
log4jdbc는 다음과 같은 로거(logger) 이름으로 출력 종류를 세분화할 수 있습니다.
| 로거 이름 | 설명 |
|---|---|
jdbc.sqlonly |
바인딩 값이 채워진 SQL만 출력 |
jdbc.sqltiming |
SQL + 실행 시간 출력 |
jdbc.audit |
JDBC 호출 전체(매우 상세, 보통 OFF) |
jdbc.resultset |
결과셋의 각 row 호출(상세, 보통 OFF) |
jdbc.resultsettable |
조회 결과를 표 형태로 출력 |
jdbc.connection |
커넥션 open/close 추적 |
운영에서는 SQL 로그가 성능과 보안에 영향을 줄 수 있으므로 필요한 로거만 켜는 것이 중요합니다. (뒤의 profile별 설정 참고)
3. profile별 로깅 설정
이 프로젝트는 로그 설정을 두 파일에서 나눠서 관리합니다.
application-{profile}.yml: 로그 레벨(level) 위주 (어떤 패키지를 어느 레벨까지 볼지)logback-spring.xml: 로그 출력 대상(Appender)과 출력 형식(pattern) 위주
3-1. application.yml (공통 기본값)
logging:
level:
root: info
kr.co.bler: DEBUG
org.springframework.security: DEBUG
# Spring Boot 자동 설정 관련 로그 비활성화 (불필요한 로그 줄이기)
org.springframework.boot.autoconfigure: WARN
# Logback 설정 파일 위치 지정
config: classpath:logback-spring.xml
root: info: 기본 레벨은 INFOkr.co.bler: DEBUG: 우리 애플리케이션 코드만 DEBUG까지logging.config로 Logback 설정 파일을 명시
3-2. logback-spring.xml의 <springProfile>
logback-spring.xml은 <springProfile name="..."> 태그로 profile마다 다른 Appender/패턴/레벨을 적용할 수 있습니다.
(파일명이 logback-spring.xml이어야 <springProfile>을 사용할 수 있습니다. logback.xml은 불가)
local 프로파일 (개발자 PC)
<springProfile name="local">
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<!-- 컬러를 입혀 가독성을 높인 콘솔 전용 패턴 -->
<pattern>%yellow(%d{yyyy-MM-dd HH:mm:ss.SSS}) %highlight(%-5level)
%green([%-20thread]) %cyan([%X{traceId}])
%magenta([%-30logger{0}])
%cyan([%X{userId}][%X{memberSeq}][%X{ip}]) - %yellow(%msg) %n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<logger name="kr.co.bler" level="DEBUG"/>
<!-- log4jdbc: 바인딩된 SQL만 보이도록 -->
<logger name="jdbc.sqlonly" level="DEBUG"/>
<logger name="jdbc.sqltiming" level="OFF"/>
<!-- Hibernate 원시 SQL은 OFF (log4jdbc와 중복 방지) -->
<logger name="org.hibernate.SQL" level="OFF"/>
<root level="DEBUG">
<appender-ref ref="CONSOLE"/>
</root>
</springProfile>
- 콘솔에만 출력, 컬러 패턴으로 가독성 강화
- 파일 출력은 주석 처리(로컬에선 불필요)
kr.co.bler와jdbc.sqlonly를 DEBUG로 → 코드 흐름 + 실제 SQL 모두 확인- Hibernate 기본 SQL 로그는 OFF → log4jdbc 로그와 중복 방지
dev 프로파일 (개발 서버)
<springProfile name="dev">
<appender name="CONSOLE" .../>
<!-- 전체 로그 파일 (날짜+용량 기준 롤링) -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${logging.file.path:-logs}/bler-admin-dev.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${logging.file.path:-logs}/bler-admin-dev.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxFileSize>200MB</maxFileSize>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%-20thread] [%X{traceId}]
[%-30logger{0}] [%X{userId}][%X{memberSeq}][%X{ip}] - %msg%n</pattern>
</encoder>
</appender>
<!-- ERROR 레벨만 따로 모으는 파일 -->
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<file>${logging.file.path:-logs}/bler-admin-dev-error.log</file>
...
</appender>
<logger name="kr.co.bler" level="INFO"/>
<logger name="org.hibernate" level="WARN"/>
<logger name="org.springframework" level="WARN"/>
<root level="WARN">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE"/>
<appender-ref ref="ERROR_FILE"/>
</root>
</springProfile>
- 콘솔 + 파일 + 에러전용 파일 3개 Appender 사용
RollingFileAppender+SizeAndTimeBasedRollingPolicy- 날짜가 바뀌거나(
%d{yyyy-MM-dd}) 파일이 일정 크기(maxFileSize)를 넘으면(%i) 새 파일로 분리 maxHistory로 보관 일수 제한 → 디스크 가득 차는 사고 방지
- 날짜가 바뀌거나(
- ERROR_FILE:
LevelFilter로 ERROR만 골라 별도 파일에 저장 → 장애 발생 시 에러 로그만 빠르게 확인 - 로컬보다 로그 레벨을 한 단계 올림(
kr.co.bler: INFO)
application-dev.yml의 레벨 보강:
logging:
level:
jdbc.sqlonly: OFF
jdbc.sqltiming: WARN # 닫힌 커넥션 관련 오류만 경고로 확인
kr.co.bler: DEBUG
production 프로파일 (운영 서버)
<springProfile name="production">
<appender name="CONSOLE" .../>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${logging.file.path:-logs}/bler-admin-prod.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>...bler-admin-prod.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxFileSize>500MB</maxFileSize>
<maxHistory>90</maxHistory>
<totalSizeCap>10GB</totalSizeCap> <!-- 전체 로그 총량 상한 -->
<cleanHistoryOnStart>true</cleanHistoryOnStart>
</rollingPolicy>
...
</appender>
<appender name="ERROR_FILE" .../> <!-- maxHistory 180일, totalSizeCap 5GB -->
<logger name="kr.co.bler" level="INFO"/>
<logger name="org.hibernate" level="WARN"/>
<logger name="org.springframework" level="WARN"/>
<root level="WARN">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE"/>
<appender-ref ref="ERROR_FILE"/>
</root>
</springProfile>
운영은 dev와 구조는 같지만 보관 정책을 강화한 것이 포인트입니다.
| 항목 | dev | production |
|---|---|---|
일반 로그 maxFileSize |
200MB | 500MB |
일반 로그 maxHistory |
30일 | 90일 |
에러 로그 maxHistory |
90일 | 180일 |
totalSizeCap (총량 제한) |
없음 | 10GB / 에러 5GB |
cleanHistoryOnStart |
없음 | true (시작 시 오래된 로그 정리) |
- 운영은 SQL 상세 로그(
jdbc)를 OFF → 성능/보안 보호 totalSizeCap과cleanHistoryOnStart로 디스크 용량을 안전하게 관리
3-3. profile별 요약 비교
| 구분 | local | dev | production |
|---|---|---|---|
| 출력 대상 | 콘솔 | 콘솔 + 파일 + 에러파일 | 콘솔 + 파일 + 에러파일 |
| 콘솔 컬러 | O | X | X |
kr.co.bler 레벨 |
DEBUG | INFO(xml)/DEBUG(yml) | INFO |
| SQL 로그 | jdbc.sqlonly=DEBUG |
대부분 OFF | jdbc 전체 OFF |
| 로그 보관 | 안 함 | 30~90일 | 90~180일 + 총량 제한 |
4. LogAspect(LoggingAspect) 설정
LoggingAspect는 AOP를 이용해 모든 컨트롤러의 요청/응답을 자동으로 로그에 남기는 공통 컴포넌트입니다.
컨트롤러마다 일일이 로그 코드를 넣지 않아도 되고, 형식도 통일됩니다.
4-1. 전체 구조
@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class LoggingAspect {
private final ObjectMapper objectMapper;
// presentation.controller 패키지 하위 전체를 대상으로 지정
@Pointcut("within(kr.co.bler.presentation.controller..*)")
public void traceMethod() {}
@Around("traceMethod()")
public Object traceMethod(ProceedingJoinPoint joinPoint) throws Throwable {
String className = joinPoint.getTarget().getClass().getSimpleName();
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
// 1) 파라미터를 JSON 문자열로 직렬화
String argsString = ...;
// 2) MDC에 인증 정보 저장 (로그 패턴의 %X{...} 자리에 출력됨)
if (ObjectUtils.isNotEmpty(SecurityUtil.getCurrentMemberSeq())) {
MDC.put(USER_ID, SecurityUtil.getCurrentMemberId());
MDC.put(MEMBER_SEQ, String.valueOf(SecurityUtil.getCurrentMemberSeq()));
} else {
MDC.put(USER_ID, "anonymous");
MDC.put(MEMBER_SEQ, null);
}
MDC.put(IP, SecurityUtil.getCurrentClientIpAddress());
// 3) 요청 로그
log.info("[{}][{}.{}] Param:{}", REQUEST, className, methodName, argsString);
try {
Object result = joinPoint.proceed(); // 실제 컨트롤러 실행
// 4) 응답 로그
log.info("[{}][{}.{}] Result:{}", RESPONSE, className, methodName, result);
return result;
} finally {
// 5) 요청 끝나면 MDC 정리 (스레드 재사용 시 값 오염 방지)
MDC.remove(USER_ID);
MDC.remove(MEMBER_SEQ);
MDC.remove(IP);
}
}
}
4-2. 어노테이션 의미
| 어노테이션 | 의미 |
|---|---|
@Aspect |
이 클래스가 AOP의 부가기능(Aspect)임을 선언 |
@Component |
스프링 빈으로 등록 (AOP가 동작하려면 빈이어야 함) |
@Pointcut("within(...)") |
어디에 적용할지(대상) 지정 — 여기선 컨트롤러 패키지 전체 |
@Around |
대상 메서드 실행 전후 모두 가로채는 advice. 요청/응답 둘 다 로깅 가능 |
4-3. @Around 동작 순서
@Around는 joinPoint.proceed()를 기준으로 앞쪽 = 실행 전, 뒤쪽 = 실행 후로 나뉩니다.
1. 컨트롤러 호출 직전 → 파라미터 직렬화 + MDC 세팅 + REQ 로그
2. joinPoint.proceed() → 실제 컨트롤러 메서드 실행
3. 컨트롤러 정상 반환 → RES 로그
4. finally → MDC 정리 (반드시 실행)
4-4. 안전한 파라미터 직렬화 (OOM 방지)
요청 파라미터를 무조건 JSON으로 바꾸면 파일 업로드(MultipartFile)나 byte[] 같은 대용량 데이터에서 메모리 부족(OOM)이 발생할 수 있습니다.
그래서 직렬화 대상이 위험한 타입이면 요약 문자열로 대체합니다.
// 응답 스트림 객체는 직렬화하지 않음
if (arg instanceof HttpServletResponse) {
return "<HttpServletResponse>";
}
// 파일 업로드는 메타정보만 출력
if (arg instanceof MultipartFile multipartFile) {
return String.format("<MultipartFile: name=%s, size=%d bytes, contentType=%s>",
multipartFile.getOriginalFilename(),
multipartFile.getSize(),
multipartFile.getContentType());
}
// byte[]는 길이만 (Base64 등 대용량 → OOM 유발)
if (arg instanceof byte[] bytes) {
return String.format("<byte[]: length=%d>", bytes.length);
}
실무에서 의외로 자주 만나는 장애가 "로그 때문에 OOM"입니다. 로깅 코드도 방어 로직이 필요합니다.
4-5. MDC와 로그 패턴의 연결
LoggingAspect가 MDC.put(USER_ID, ...)로 넣은 값은 logback-spring.xml의 패턴에서 %X{userId}로 출력됩니다.
MDC.put("userId", "master1") ─┐
MDC.put("memberSeq", "1024") ├─▶ 패턴: [%X{userId}][%X{memberSeq}][%X{ip}]
MDC.put("ip", "10.0.0.5") ─┘ 출력: [master1][1024][10.0.0.5]
traceId,ip:TraceIdMdcFilter(서블릿 필터)에서 요청 진입 시 세팅userId,memberSeq:LoggingAspect에서 인증 정보로 세팅
이 덕분에 로그 한 줄만 봐도 "어떤 요청(traceId)에서, 어떤 사용자(userId)가, 어떤 IP에서" 호출했는지 추적할 수 있습니다.
4-6. 실제 로그 출력 예시
2026-01-05 14:22:01.532 INFO [http-nio-8080-exec-3] [a1b2c3d4e5] [ClassController] [master1][1024][10.0.0.5] - [REQ][ClassController.list] Param:{"page":0,"size":20}
2026-01-05 14:22:01.610 DEBUG [http-nio-8080-exec-3] [a1b2c3d4e5] [jdbc.sqlonly] [master1][1024][10.0.0.5] - select c1_0.class_seq, ... from class c1_0 ...
2026-01-05 14:22:01.655 INFO [http-nio-8080-exec-3] [a1b2c3d4e5] [ClassController] [master1][1024][10.0.0.5] - [RES][ClassController.list] Result:ApiResponse(success=true, ...)
같은 traceId(a1b2c3d4e5)로 요청 → SQL → 응답이 한 흐름으로 묶이는 것을 볼 수 있습니다.
5. 민감정보 마스킹
로그에는 비밀번호, 휴대폰 번호 같은 개인정보가 노출되면 안 됩니다. 이 프로젝트는 두 계층에서 마스킹합니다.
- REQ 파라미터(JSON) 마스킹 :
LoggingParamMaskProperties에 지정한 필드명을 마스킹# config/util/util-{profile}.yml util: logging: param-mask: json-field-names: - password - mobile - SQL 결과셋 마스킹 : 앞서 본
CustomLogDelegator+MaskingResultSetCollector가 조회 결과의 민감 컬럼을 가려서 출력
6. 정리
- 코드는 SLF4J로만, 출력 제어는
logback-spring.xml+application-{profile}.yml로 분리 - profile별 전략
local: 콘솔 컬러 로그 + 상세 SQL → 개발 편의 우선dev: 파일 롤링 + 에러 분리 + 적당한 보관 기간production: 보관 정책 강화(totalSizeCap,cleanHistoryOnStart) + SQL 로그 OFF
- log4jdbc로 바인딩된 실제 SQL을 보기 좋게 출력 (
CustomLogDelegator로 포맷팅/마스킹) - LoggingAspect(AOP)로 모든 컨트롤러의 요청/응답을 공통 포맷으로 기록
- MDC + TraceId로 요청 단위 추적(traceId/userId/ip) 가능
- 마스킹으로 개인정보 노출 방지
로깅은 "잘 만들어두면 평소엔 안 보이지만, 장애가 났을 때 가장 먼저 찾게 되는 자산"입니다. 환경별로 레벨/출력/보관 정책을 명확히 구분해두는 것이 핵심입니다.