🥋 팀에서 논의해 봐야 될 사항❓어떤 로그를 남길 것인가💾 롤링 정책📁 로그백 설정 방식🐶 현재 선택한 방법📁 하나의 파일에서 Profile 관리 (로그백 설정 방식)🪬 logback vs logback-spring🔍 logback 구성 요소🧶 appender📺 ConsoleAppender💾 RollingFileAppender🪬 AsyncAppender🔧 logger🖼 layout🌐 API 로그 남기기🫠 첫 번째 방법하지만…?🧑🏻💻 두 번째 방법 (피드백 후)🔖 참고 사이트📌 읽어봐야 될 글
🥋 팀에서 논의해 봐야 될 사항
❓어떤 로그를 남길 것인가
- 예를 들면..?
- API 통신에 대한 로그
- DB 쿼리 로그
- ERROR 단계의 로그만 파일로 저장하겠다.
💾 롤링 정책
- 로그 파일의 보관 주기
- 각 파일의 최고 용량
📁 로그백 설정 방식
- 하나의 파일에서 Profile 별로 관리
<?xml version="1.0" encoding="UTF-8" ?> <configuration> <include resource="logback/properties.xml" /> <springProfile name="local"> ... </springProfile> <springProfile name="dev"> ... </springProfile> </configuration>
- Profile 파일 별로 관리

🐶 현재 선택한 방법
📁 하나의 파일에서 Profile 관리 (로그백 설정 방식)
- 선택 이유
- 하나의 파일에서 Profile 관리 이유
- 장점 : 각각의 환경마다 설정값들을 달리 할 수 있다. (파일의 크기 제한 등)
- 단점 : 경우에 따라서 설정 구문이 중복 될 수 있다.
- appender 분리 이유
Profile 별로 관리를 한다. 라는 것은 장단점이 있다고 생각했습니다.
그런데 이처럼 설정값들을 달리 하기에는 경험치가 부족하지 않을까 싶었습니다.
결국 단점에서 나오는 구문 중복만 야기되지 않을까 싶어서 한 파일에서 관리 되도록 했습니다.
아래의 폴더 트리를 보면 알 수 있듯이 각각의 appender로 분리해서 관리 되도록 했습니다.
appender가 많지 않다면 하나의 파일에서 다 작성하는 것이 좋겠죠.
하지만 로그 레벨마다 파일을 분리해서 관리를 한다면,
레벨에 따라서 로그를 추적하기 쉽지 않을까 싶어 분리를 하게 되었고,
이에 따라 appender 수가 늘어나게 되서 파일을 분리하게 되었습니다.
- 폴더 트리
resources ⌙ logback-spring.xml ⌙ logback[folder] ⌙ console-appender.xml ⌙ db-file-appender.xml ⌙ error-file-appender.xml ⌙ properties.xml
- logback-spring.xml
- include 태그를 이용해서 분리된 파일 활용
- springProfile : Profile 별로 각각 로그 설정
<?xml version="1.0" encoding="UTF-8" ?> <configuration> <include resource="logback/properties.xml" /> <!-- Profile 별로 관리 --> <springProfile name="local"> <include resource="logback/console-appender.xml" /> <include resource="logback/db-file-appender.xml" /> <include resource="logback/error-file-appender.xml" /> <root level="INFO"> <appender-ref ref="CONSOLE" /> <appender-ref ref="DB_FILE" /> <appender-ref ref="ERROR_FILE" /> </root> </springProfile> <springProfile name="dev"> ... </configuration>
- logback 폴더에 있는 appender들… 중에 하나 예시
- 각각 비동기 설정도 추가 완료
- included 태그를 이용해서 메인 logback 파일에서 사용 되도록 설정
<included> <appender name="WARN_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>${ROOT_PATH}/${SUB_PATH_WARN}/warn-${LOG_DATE}.log</file> <filter class="ch.qos.logback.classic.filter.LevelFilter"> <level>WARN</level> <onMatch>ACCEPT</onMatch> <onMismatch>DENY</onMismatch> </filter> <encoder> <pattern>${FILE_LOG_PATTERN}</pattern> </encoder> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>${LOG_BACKUP_PATH}/${SUB_PATH_WARN}/warn-%d{yyyy-MM-dd}.zip</fileNamePattern> <maxHistory>${MAX_HISTORY}</maxHistory> <totalSizeCap>${TOTAL_SIZE_CAP}</totalSizeCap> </rollingPolicy> </appender> <appender name="ASYNC_WARN_FILE" class="ch.qos.logback.classic.AsyncAppender"> <appender-ref ref="WARN_FILE"/> <queueSize>1024</queueSize> <discardingThreshold>0</discardingThreshold> <includeCallerData>false</includeCallerData> <neverBlock>false</neverBlock> </appender> </included>
🪬 logback vs logback-spring
- 스프링 부트의 경우 logback-spring.xml을 이용한다.
logback.xml을 사용하게 되면 스프링 부트 설정 전에 로그백 설정이 먼저 되서 로그 제어가 어려워짐
또는 property의 logging.config = classpath:logback-${spring.profile.active}.xml을 통해 각 프로파일별로 logback 설정 파일을 관리한다. 덧붙여 application.yml의 설정만으로도 logger, 로그 레벨 설정이 가능하다.
🔍 logback 구성 요소
🧶 appender
📺 ConsoleAppender
- 콘솔 로깅
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>${LOG_PATTERN}</pattern> </encoder> </appender>
💾 RollingFileAppender
- 파일로 저장하는 파일 로깅
<appender name="FILE-ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>./log/error/error-${BY_DATE}.log</file> <filter class = "ch.qos.logback.classic.filter.LevelFilter"> <level>ERROR</level> <onMatch>ACCEPT</onMatch> <onMismatch>DENY</onMismatch> </filter> <encoder> <pattern>${LOG_PATTERN}</pattern> </encoder> <!-- SizeAndTimeBasedRollingPolicy : 각각의 로그 파일에 대한 크기를 제한 하는 부분 추가 - fileNamePattern 에서 %i, %d 필수 포함 되어야 함! --> <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"> <!-- 파일명 패턴 지정 → %i, %d가 필수 포함 되어야 됨! --> <fileNamePattern> ./backup/error/error-%d{yyyy-MM-dd}.%i.log</fileNamePattern> <!-- 각 파일명의 최대 용량 [SizeAndTimeBasedRollingPolicy 에서만 있는 필드] --> <maxFileSize>100MB</maxFileSize> <!-- 보관 기간(일단위 / 월단위) --> <maxHistory>30</maxHistory> <!-- 저장소의 최대 크기 (maxHistory → totalSizeCap 순서로 적용) --> <totalSizeCap>3GB</totalSizeCap> </rollingPolicy> </appender>
🪬 AsyncAppender
- 비동기 로깅
- 비동기로 로그를 기록합니다.
- 큐에 있는 쓰기 대기상태의 로그 메시지를 꺼내오는 Event dispatcher 역할
worker thread
생성- 발생한 로그를
BlockingQueue
에 보관 - 서버가 멈추거나 재배포될 때는
maxFlushTime
만큼 큐에 남아있던걸 처리하고 끝남
→ 실제 로그를 남기는 appender 참조 필요(사용 예제를 보면 기존의 appender를 참조)
→ 얘가 큐에서 메시지를 꺼내서 dispatcher appender에게 넘겨줌
→ dispatcher 가 로그 쓰기 비동기 처리
→ 0으로 설정하게 되면 모두 처리
- 장점
- Application 입장에서는 로그 발생 시, File IO 작업이 사라지므로 빨라짐
- 단점
- 비동기식이기 때문에 성공 ≠ 로그 기록 성공 → 다양한 이유로 로그의 손실 발생
- 큐 메모리 관리 등의 관리 대상이 늘어남
데이터 로깅이 많이 필요하고 속도가 중요하다면 비동기 로깅!!
- 사용 예제
<appendername="CONSOLE"class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>${LOG_PATTERN}</pattern> </encoder> </appender> <appendername="ASYNC_CONSOLE_APPENDER"class="ch.qos.logback.classic.AsyncAppender"> <!- 실제로 로그 처리할 Appender 참조 -> <appender-ref ref="CONSOLE_APPENDER"/> <!- BlockingQueue 사이즈 (default: 256) -> <queueSize>1024</queueSize> <!- 큐 크기가 지정한 %가 남았을 때, Log Drop! → 0은 No Drop!! -> <discardingThreshold>0</discardingThreshold> <!- 로그 호출한 곳 정보 표시 여부 -> <includeCallerData>false</includeCallerData> <!- 큐가 다 차면, False : 기다림, True : 메시지 넣는 것을 기다리지 않고 다 버림 -> <neverBlock>false</neverBlock> </appender>
🔧 logger
필수 name 속성, 선택적으로 level 속성과 additivity 속성을 가진다.
- additivity
- True(기본값) : 모든 상위 로거들의 설정값을 상속받아서 현재 로거에 설정된 값을 덮어쓰기
- False : 상속 X
: 상위 로거로부터의 상속 여부
- 사용 예제
<configuration> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>${LOG_PATTERN}</pattern> </encoder> </appender> <logger name="org.hibernate.SQL" level="DEBUG" additivity="false"> <appender-ref ref="CONSOLE"/> </logger> <!-- prepared statement 문 형태의 질의문에 들어가는 파라미터를 보기 위한 설정 - devug 레벨이면 파라미터 로그 나오지 않음 - 로그 형태 --> <logger name="org.hibernate.type.descriptor.sql.BasicBinder" level="TRACE" additivity="false"> <appender-ref ref="CONSOLE"/> </logger> <!-- SELECT문의 질의 결과를 로그로 출력 - 로그 형태 --> <logger name="org.hibernate.type.descriptor.sql.BasicExtractor" level="TRACE" additivity="false"> <appender-ref ref="CONSOLE"/> </logger> <!-- DB 연결 정보 출력 --> <logger name="org.zaxxer.hikari" level="DEBUG"> <appender-ref ref="CONSOLE"/> </logger> <!-- Root 로거의 속성으로는 오직 단 하나의 level만 허용 --> <root level="OFF"> <appender-ref ref="CONSOLE" /> </root> </configuration>
🖼 layout
- 간단하게 사용되는 키워드 정리
%logger : 패키지 포함 클래스 정보 %logger{0} : 패키지를 제외한 클래스 이름만 출력 %logger{최대-자리수} : Logger name 축약 %d : 로그 기록시간 출력 %i : 롤링 순번을 자동적으로 지정 … 0, 1 , … %m : 로그 메시지 %msg : - 로그 메시지 %n : 줄 바꿈 (new line) %thread : 스레드명 %-5level : 로그 레벨(5글자로 고정: 4글자면 1글자는 공백으로 채움) %-4relative : 초 아래 단위 시간(밀리초) %C : 로깅이 발생한 클래스명 %M : 로깅이 발생한 메소드명 출력 %L : 로깅이 발생한 호출지 라인
- 사용 예제
<property name="LOG_PATTERN" value="[%d{yyyy-MM-dd HH:mm:ss}:%-4relative] %green([%thread]) %highlight(%-5level) %cyan(%logger{15}) %C.%M.%L :%msg%n"/> <property name="FILE_LOG_PATTERN" value="[%d{yyyy-MM-dd HH:mm:ss}:%-4relative] [%thread] %-5level %logger{15} %C.%M.%L :%msg%n"/>
🌐 API 로그 남기기
🫠 첫 번째 방법
- LogServletWrappingFilter
HttpServlet은 단 한번만 읽을 수 있도록 톰캣에서 만들어 두었기 때문에, 다시 읽을 수 있도록!!
ContentCaching 으로 Wrapping 이 필요합니다.
✨wrappingResponse.copyBodyToResponse()
이 메서드를 통해서 body 값을 copy해서 캐시로 저장하기 때문에, 다시 읽을 수 있습니다.
@Component public class LogServletWrappingFilter extends OncePerRequestFilter { @Override protected void doFilterInternal( HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { ContentCachingRequestWrapper wrappingRequest = new ContentCachingRequestWrapper(request); ContentCachingResponseWrapper wrappingResponse = new ContentCachingResponseWrapper(response); filterChain.doFilter(wrappingRequest, wrappingResponse); // 이 부분을 하지 않으면 client가 응답을 받지 못한다. wrappingResponse.copyBodyToResponse(); } }
- LogInterceptor
@RequiredArgsConstructor @Component public class LogInterceptor implements HandlerInterceptor { private static final Logger logger = LoggerFactory.getLogger(LogInterceptor.class); private final ObjectMapper objectMapper; @Override public boolean preHandle( HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String[] content = handler.toString().split("\\."); logger.warn("{} invoked", content[content.length - 1]); return true; } @Override public void afterCompletion( HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { ContentCachingRequestWrapper cachingRequest = (ContentCachingRequestWrapper) request; ContentCachingResponseWrapper cachingResponse = (ContentCachingResponseWrapper) response; String params = request.getParameterMap().entrySet().stream() .map(entry -> String.format("{\"%s\":\"%s\"", entry.getKey(), Arrays.toString(entry.getValue()))) .collect(Collectors.joining(",")); logger.warn("Params: {}", params); logger.warn("RequestBody: {} / ResponseBody: {}", objectMapper.readTree(cachingRequest.getContentAsByteArray()), objectMapper.readTree(cachingResponse.getContentAsByteArray()) ); } }
: 어느 Controller 에서 어떤 메서드를 호출하는지 기록하고 있습니다.
: 받은 파라미터, RequestBody, ResponseBody 를 각각 기록했습니다.
하지만…?
위의 방법은 로그 하나를 위해, 필터를 하나 더 늘리게 되는 문제가 있습니다.
🧑🏻💻 두 번째 방법 (피드백 후)
첫 번째 방법은 로그 하나를 위한 필터를 하나 더 생성해서 의존을 하나 늘리는 문제가 있었습니다.
이를 해결하기 위해 기존에 사용중이던
JWT
필터에 로그 구문을 추가했습니다.public class JwtAuthenticationFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { logRequest(request); ... 기존 로직 } private void logRequest(HttpServletRequest request) { log.info(String.format( "[%s] %s %s", request.getMethod(), request.getRequestURI().toLowerCase(), request.getQueryString() == null ? "" : request.getQueryString()) ); } }
- 어떤 불순한 의도로 공격을 할 수도 있기 때문에, QueryString도 함께 남기고 있습니다.