Spring logback을 사용하여 로그 저장하기

Logback Appender를 활용한 로그 관리 및 파일 관리하기, 로그 포맷, 로그 남기기, 로깅 전략
dustjq1005 2025년 08월 31일 2025년 09월 01일 수정됨

현재 프로젝트는 로그에 대한 처리가 하나도 없는 상태입니다. 어플리케이션이 실행되면 docker가 로그를 기록하고 있지만 컨테이너가 종료되면 로그들은 남지 않습니다. 또한, 실행 중 기록되는 로그들은 패턴이 중구난방이고 정보도 sql 정보와 에러로그 밖에 없어 디버깅 하기가 힘든 상태입니다. 사실상 로깅 시스템이란 것이 하나도 없어 디버깅을 할 수가 없었고, 로깅에 대한 필요성을 많이 느꼈습니다. 그래서 로깅 시스템을 개선하기로 결정했고, 그 첫번째로 제일 큰 문제인 사라지는 로그들을 logback을 사용해 로그가 영구적으로 기록되도록 개선했습니다.

logback을 선택한 이유

logback은 자바 어플리케이션에서 사용한은 로그 관련 오픈소스 프레임워크로 sl4j 다음으로 나왔습니다. SLF4J를 사용한 구현체이고, log4j보다 성능이 우수한걸로 알려져 있습니다. 스프링 부트 환경이라면 기본 라이브러리로 포함되어 있어 dependency 추가하지 않아도 사용할 수 있고요. 그래서 현재 가장 많이 사용되고 있는 로깅 프레임워크라고 합니다.

logback을 선택한 이유는 Spring boot기반의 복잡한 설정 없이 빠르게 구축할 수 있고 성능이 우수하다는 점인 것 같습니다. log4j2가 유연성과 성능면에서 우수하지만 별도 설정 없이 사용하고 싶었고 현재 사이트에선 성능상 차이가 없습니다. 그리고 정보나 문서화도 잘 되어있고, 실무에서 많이 사용하는 logback이 더 적합하다고 생각했습니다.

마지막으로 향후 ELK 구축까지 해서 시각화까지 생각하고 있습니다.

Logback의 장점

  1. 성능이 우수함 log4j, java.util.logging 대비 성능이 우수합니다. 비동기 로깅을 통해 I/O병복 없이 고속 로깅 처리 가능합니다.
  2. 로그 압축 및 보관 정책 우수 RollingFileAppender + TimeBasedRollingPolicy 조합으로 날짜 기반 로그 파일을 분할할 수 있고, 로그 압축 기능도 제공합니다. 또한, 보관 기간을 설정할 수 있고 로그 파일당 최대 용량 설정도 가능합니다.
  3. 조건부 로깅, 필터링 기능 조건부 로깅이라고 특정 패턴/레벨/스레드명 기준으로 필터링이 가능합니다. 콘솔, 파일 등 다양한 Appender에 별도 조건을 부여할 수 있습니다.
  4. MDC/Marker를 통한 Contextual Logging
  5. 다양한 출력 방식 지원 콘솔, 파일, DB, TCP 소켓, Syslog 등 다양한 출력 방식을 지원하고, logstash와 연동도 쉽습니다.

MDC란?

MDC(Mapped Diagnostic Context)는 로그 프레임워크(Logback, Log4j, SLF4J 등) 에서 특정 요청(Request) 또는 스레드(Thread)와 관련된 정보를 로그에 자동으로 포함할 수 있도록 도와주는 기능입니다.

로그에 포함된 ID를 통해 Request를 구분하여 쉽게 트레이싱을 할 수 있습니다.

작업 순서

  1. 레벨 정의
  2. 환경별 설정 정의
  3. logback 설정 파일 작성
  4. MDC 사용
  5. 로컬 환경 테스트
  6. 운영 환경 테스트

로그 레벨

TRACE > DEBUG > INFO > WARN > ERROR

  1. ERROR : 로직 수행 중에 오류가 발생한 경우
  2. WARN : 시스템 에러의 원인이 될 수 있는 경고 레벨, 처리 가능한 사항
  3. INFO : 상태 변경과 같은 정보성 메시지
  4. DEBUG : 어플리케이션의 디버깅을 위한 메시지 레벨
  5. TRACE : DEBUG 레벨보다 더 디테일한 메시지를 표현하기 위한 레벨

로그 레벨은 중요도·심각도·필요성을 구분하는 기준입니다. 레벨이 있으면 시스템 위험도를 파악할 수 있고, 관리하기가 용이해집니다. Logback은 로그를 레벨별로 출력여부를 설정할 수 있고, 환경에따라 보여줄 레벨을 설정할 수 있습니다.

local 환경에서는 레벨을 Debug까지 설정해 개발시 문제 원인 추적에 용이하게 설정하고, 운영환경은 INFO 레벨로 설정하여 시스템에 부하가 가지 않게 하였습니다. 또한, 여러 사용자가 유입되는 운영에서는 너무 많은 정보가 오히려 문제 추적을 방해할 수 있고, 보안사고로 이어질 수도 있어 운영환경 설정은 주의해야합니다.

환경별 설정 정의

로그 pattern

%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){magenta} %clr(%X{clientIP}){blue} %X{requestId} %clr([%-5level]){} %clr([%thread]){cyan} %clr(%logger{30}){cyan} - %msg%n

패턴은 PatternLayoutEncoder 클래스가 로그메시지를 문자열로 변환해줍니다. Logback의 로깅 구조는 대략 Logger -> Appender -> Encoder 순으로 진행되는데 Encoder에서 PatternLayoutEncoder를 사용하여 패턴에 맞게 로그 메세지를 출력할 수 있습니다.

  • %clir (){}는 컬러를 지정할 수 있습니다. {}안에 색상을 지정할 수 있습니다.
  • %d{패턴} : 로그 발생 시각을 나타냅니다.
  • %X{key} : MDC에서 추출한 값 입니다. MDC에서 clientIP, requestId를 가져와서 사용했습니다.
  • %-5level : 로그 레벨을 출력합니다. (최소 5글자, 왼쪽 정렬)
  • %thread : 스레드 명을 출력합니다.
  • %logger{30} : 로거명을 최대 30자리로 출력합니다. (예 : `com.example.service.MyService)
  • %msg : 로그 메세지
  • %n : 개행

logback.xml 설정

<!-- Appenders -->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
		<encoder>
				<pattern>
						%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){magenta} %clr(%X{clientIP}){blue} %X{requestId} %clr([%-5level]){} %clr([%thread]){cyan} %clr(%logger{30}){cyan} - %msg%n
				</pattern>
				<charset>UTF-8</charset>
		</encoder>
</appender>

local 환경에서 console에 찍힐 로그패턴입니다. ConsoleAppender는 콘솔에 남길 로그를 정의할 수 있습니다. pattern은 위에 작성한 패턴입니다. 이제 콘솔에 작성한 패턴으로 로그가 남게 됩니다.

<SpringProfile name="local, test">
        <logger name="org.springframework.web.filter.CommonsRequestLoggingFilter" level="DEBUG" additivity="false">
            <appender-ref ref="console"/>
        </logger>
        <logger name="me.kimyeonsup.home" level="DEBUG" additivity="false">
            <appender-ref ref="console"/>
        </logger>
        <logger name="org.springframework" level="ERROR">
            <appender-ref ref="console"/>
        </logger>

        <root level="ERROR">
            <appender-ref ref="console"/>
        </root>

        <logger name="org.hibernate.SQL" level="DEBUG"/>
        <logger name="org.hibernate.type.descriptor.sql.BasicBinder" level="TRACE"/>
        <!--        <logger name="org.springframework.boot" level="DEBUG"/>-->
        <!--        <logger name="org.springframework.boot.autoconfigure" level="INFO"/> -->
</SpringProfile>

저는 [local, test], [dev] 으로 환경을 분리하여 로그가 남게 했습니다. 위에 설정은 local, test에만 적용될 로그 설정입니다. 로그는 특정 패키지 혹은 클래스마다 레벨을 다르게 설정할 수 있습니다.

  • CommonsRequestLoggingFilter : 스프링에서 제공하는 LoggingFilter로 reqeust의 헤더, body등의 값을 로그로 남기는 Filter입니다. DEBUG 레벨로 설정했습니다.
  • me.kimyeonsup.home : 제가 개발한 프로젝트 패키지 경로입니다. 해당 패키지 아래 있는 모든 로그에 레벨을 DEBUG로 설정했습니다.
  • org.springframework : ERROR 레벨 로그만 콘솔에 남기도록 설정했습니다.
  • root : root 또한 ERROR 레벨 로그만 남기도록 했습니다.
<SpringProfile name="dev">
        <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
            <file>/app/logs/application.log</file>

            <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
                <fileNamePattern>/app/logs/application-%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
                <maxFileSize>100MB</maxFileSize>
                <maxHistory>30</maxHistory>
                <cleanHistoryOnStart>true</cleanHistoryOnStart>
                <gzippedArchive>true</gzippedArchive>
            </rollingPolicy>

            <encoder>
                <pattern>
                    %clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){magenta} %clr(%X{clientIP}){blue} %X{requestId} %clr([%-5level]){} %clr([%thread]){cyan} %clr(%logger{30}){cyan} - %msg%n
                </pattern>
                <charset>UTF-8</charset>
            </encoder>
        </appender>

        <!-- 3. ERROR 로그 분리 -->
        <appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
            <file>/app/logs/error.log</file>
            <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
                <fileNamePattern>/app/logs/error.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
                <maxFileSize>100MB</maxFileSize>
                <maxHistory>30</maxHistory>
                <cleanHistoryOnStart>true</cleanHistoryOnStart>
                <gzippedArchive>true</gzippedArchive>
            </rollingPolicy>
            <encoder>
                <pattern>
                    %clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){magenta} %clr(%X{clientIP}){blue} %X{requestId} %clr([%-5level]){} %clr([%thread]){cyan} %clr(%logger{30}){cyan} - %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>

        <logger name="org.springframework.web.filter.CommonsRequestLoggingFilter" level="DEBUG">
            <appender-ref ref="FILE"/>
            <appender-ref ref="ERROR_FILE"/>
        </logger>
        <logger name="me.kimyeonsup.home" level="DEBUG" additivity="false">
            <appender-ref ref="FILE"/>
            <appender-ref ref="ERROR_FILE"/>
        </logger>
        <logger name="org.springframework" level="ERROR">
            <appender-ref ref="ERROR_FILE"/>
        </logger>

        <root level="ERROR">
            <appender-ref ref="ERROR_FILE"/>
        </root>

        <logger name="org.hibernate.SQL" level="DEBUG"/>
        <logger name="org.hibernate.type.descriptor.sql.BasicBinder" level="TRACE"/>
    </SpringProfile>

다음은 운영환경에서 남길 로그입니다. 운영환경은 콘솔이 필요가 없으니 RollingFileAppender를 사용해 File에 로그가 남도록 설정했습니다. 그리고 에러 파일은 별도로 보관하기 위해서 ERROR_FILE Appender를 하나 더 추가했습니다. 지금 위에 설정은 기존 로그파일에도 error 로그가 남고, 에러 파일에 중복으로 에러 로그가 남습니다. 에러를 중복으로 남기기 싫다면 LevelFilter를 활용해 남지 않게 설정할 수 있습니다. 이렇게 레벨별로 다른 파일에 저장되도록 설정할 수 있습니다. 하지만 레벨별로 다르게 하면 그 만큼 파일별로 열어봐야하기 때문에 디버깅이 오히려 더 힘들어질 수 있겠다 생각했습니다.

MDC 사용

MDC (Mapped Diagnostic Context) 는 스레드 로컬(ThreadLocal) 기반의 Key-Value 로그 컨텍스트 저장소입니다.

흔히 로그에 유저 ID, 세션 ID, 트랜잭션 ID, trace ID 등을 붙이는 데 사용합니다. 그래서 MDC를 사용하면 Client 추적이 수월해집니다.

@Slf4j
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class HttpLoggingFilter extends OncePerRequestFilter {

    @Override
    public void doFilterInternal(HttpServletRequest request,
                                 HttpServletResponse response,
                                 FilterChain filterChain) throws ServletException, IOException {
        String clientIP = request.getHeader("X-Forwarded-For");
        String requestId = request.getHeader("X-Request-Id");

        clientIP = StringUtils.nvl(clientIP, request.getRemoteAddr());
        requestId = StringUtils.nvl(requestId, UUID.randomUUID().toString()); // local 환경에서 구분을 위하여 추가

        MDC.put("clientIP", clientIP);
        MDC.put("requestId", requestId);
        
        // /api로 시작하는 요청에 대해서만 response body 캡처
        ContentCachingResponseWrapper responseWrapper = null;
        if (request.getRequestURI().startsWith("/api")) {
            responseWrapper = new ContentCachingResponseWrapper(response);
            responseWrapper.setCharacterEncoding("UTF-8");
            filterChain.doFilter(request, responseWrapper);
        } else {
            filterChain.doFilter(request, response);
        }

        MDC.clear();
    }

}

MDC 설정은 어렵지 않습니다. SLF4J 에서 MDC지원하는 기능이며, MDC.put(key, value)를 통해 값을 저장할 수 있습니다. Filter나 Interceptor에서 사용하며 request에서 값을 가져와서 사용하거나 UUID를 사용하여 랜덤 ID값을 지정합니다. Filter는 DispatcherServlet 전에 도작하여 모든 요청 로그에 requestId를 지정할 수 있지만, 세션/유저 정보가 어렵습니다. Interceptor는 세션/유저 정보 기반 MDC 세팅이 유리하지만 예외필터나 ExceptionHandler, 뷰 렌더링 과정 로그에는 포함 안될 수도 있습니다.

저는 OncePerRequestFilter를 사용하여 requestId, clientId를 MDC에 넣어 사용했습니다. OncePerRequestFilter는 한 HTTP 요청당 단 한 번만 실행되는 Filter입니다. MDC, request logging, 인증 체크 등은 반드시 “한 번만” 실행돼야 하므로 OncePerRequestFilter를 사용하는 것이 안정적입니다.

마지막으로 MDC.clear()를 해야 메모리 누수를 방지할 수 있습니다.

MDC는 logback.xml 설정에서 %X{requestId} 형태로 사용할 수 있습니다.

로컬 환경 테스트

설정한 값들이 잘 사용되는지 확인을 해보면 로그가 pattern에서 지정한 대로 잘 찍히는 것을 볼 수 있습니다. 또한, HttpLogginFilter, CommonsRequestLoggingFilter등의 클래스의 로그도 잘 남겨지는 것을 확인할 수 있습니다.

운영 환경 테스트

application.log와 error.log도 잘 나뉘어져 영구적으로 기록되고 있습니다.

application.log와 error.log가 각각 정상적으로 분리되어 기록되고 있으며, 영구 저장 설정이 적용되어 로그 유실 문제는 발생하지 않고 있습니다. error.log 내 주요 에러 유형은 HTTP 404 Not Found 응답으로 확인됩니다. 단순 접근 오류(잘못된 URL 호출 등)로 인한 로그가 다수를 차지하고 있어, 실제 장애성 오류와 구분이 필요합니다. 또한, 불필요한 404 로그 발생을 줄이기 위해, 라우팅 정책 보완 또는 정적 리소스 매핑 확인이 필요해 보입니다.

application.log는 사전 정의한 로깅 정책에 맞게 기록되고 있으며, 정상적인 동작 로그가 누락 없이 수집되고 있습니다. 로그 패턴 또한 일관성 있게 유지되고 있어 분석 및 모니터링 활용에 적합한 상태입니다.


마치며

로깅에 대한 기초적인 전략을 수립해봤습니다. 전략이라 하기엔 아직 우습지만, 많은 것을 배울 수 있었습니다. 무엇보다 로그를 단순히 기록하는 수준을 넘어, 서비스 운영의 중요한 자산으로 바라봐야 한다는 점을 체감했습니다. 이번 과정을 통해 다음과 같은 인사이트를 얻을 수 있었습니다.

  1. 로그 분리와 수준(Level) 관리의 중요성

    • Application 로그와 Error 로그를 분리하여 기록함으로써, 운영 로그와 장애 로그를 명확히 구분할 수 있었습니다.
    • 로그 레벨(DEBUG, INFO, WARN, ERROR)을 체계적으로 설정하는 것이 문제 원인 파악 속도를 크게 좌우한다는 점을 배웠습니다.
  2. 영구적 보관의 필요성

    • 컨테이너 종료 시 사라지는 휘발성 로그의 한계를 경험하면서, 로그 영구 보관 전략이 서비스 안정성 확보에 필수적임을 알게 되었습니다.
  3. 분석 가능한 로그 구조 설계

    • 로그가 단순 문자열 나열로만 존재한다면, 분석과 모니터링에 활용하기 어렵습니다.
    • 추후 JSON 형태의 구조적 로그로 확장하면, 모니터링 시스템(예: ELK, Loki, CloudWatch)과의 연동도 수월해질 것입니다.

[출처] [SpringBoot] Logback 대신 Log4j2를 사용해야 하는 이유|작성자 꼼꼼한 재은씨

Comments