Developer.

[멋사 백엔드 19기] TIL 36일차 Spring Boot Logging

📂 목차


📚 본문

우선 로깅은 프로그램이 실행되는 동안 발생하는 이벤트, 상태, 오류, 경고, 정보 등을 기록하는 행위이다.

목적

  • 문제 해결(Debugging): 오류가 발생했을 때 원인을 추적할 수 있음
  • 운영/모니터링: 서비스가 정상적으로 돌아가는지 확인
  • 분석/통계: 사용자 행동, 성능, 트래픽 등을 분석
  • 감사(Audit): 누가 언제 어떤 작업을 했는지 기록

로깅 정보 종류

  • DEBUG: 개발 중 세부적인 상태 정보
  • INFO: 일반적인 실행 정보
  • WARN: 경고, 문제가 될 수 있는 상황
  • ERROR: 오류 발생, 예외 상황
  • FATAL: 치명적인 오류, 프로그램 종료 필요

로깅 소스

  • 파일: 로그 파일에 기록
  • 콘솔: 터미널 또는 IDE 콘솔에 출력
  • 원격 서버 / DB: 중앙 서버로 수집 후 분석
  • 로그 관리 시스템: ELK(Elasticsearch, Logstash, Kibana), Grafana

Spring Boot Logging

spring boot 는 log 구현체에 직접 접근하기 보다 SLF4J 의 Facade 패턴을 사용하여 추상화된 계층을 이용하게 된다. SLF4J 는 Simple Logging Facade for Java 의 약자로 자바용 로깅 추상화 라이브러리이다. 핵심 아이디어는 로깅 라이브러리 자체(Log4j, Logback, java.util.logging 등) 에 의존하지 않고 통일된 인터페이스를 통하여 로그를 남길 수 있도록 해준다.

Application Code
       
SLF4J API (Facade 패턴)
       
Logback (Default) / Log4j2 / JUL
       
Output (Console, File, etc.)

위 의존 관계를 보면 알다시피 실제 로그 처리 구현체와 코드를 분리시켜 어떤 로그 라이브러리를 써도 SLF4J 가 알아서 처리해준다.

  • Logback: Spring Boot 기본 로깅 구현체, 성능이 빠르고, 설정이 비교적 직관적이다, XML, Groovy 로 설정 가능
  • Log4j2: Apache 사에서 개발하였고, 고성능, 비동기 기반의 로깅 프레임워크이다. Logback 의 장점을 흡수하고 Log4j 의 단점을 보완하여 만들어졌다.

보통 위 두 패키지를 사용하게 되고, 개발팀마다 사용하는게 다르지만 둘 다 알아놓으면 좋을거 같다.

Facade 패턴 - 패키지 끼리 서로 다른 인터페이스를 제공하여 모듈을 갈아끼울 때마다 코드를 변경해야 하는 불편함이 생긴다. 이러한 불편함을 중간에 클래스를 두어 인터페이스를 통합하는 책임을 주어 해당 통합된 인터페이스를 사용하여 유지보수를 높인다.

Logger 사용해보기

@RestController
public class LogExampleController {

    // Logger 생성 (클래스 기준)
    private static final Logger logger = LoggerFactory.getLogger(LogExampleController.class);

    @GetMapping("/log-test")
    public String logTest() {
        logger.trace("TRACE 레벨 로그 - 가장 상세한 로그");
        logger.debug("DEBUG 레벨 로그 - 개발 중 디버깅용");
        logger.info("INFO 레벨 로그 - 일반 정보 출력");
        logger.warn("WARN 레벨 로그 - 경고 상황");
        logger.error("ERROR 레벨 로그 - 오류 발생");

        return "로그 출력 완료! (콘솔에서 확인)";
    }
}

이제 application.yml 로 로그가 출력 될 수준을 정해준다. 쉽게 말해서 중요한 정도를 정해준다고 보면된다. 5 정도면 1, 2, 3, 4, 5 의 수준의 적은 중요도 순서로 로그를 출력하게 된다.

  • 1: TRACE,
  • 2: DEBUG
  • 3: INFO
  • 4: WARN
  • 5: ERROR
  • 6: FATAL(Log4j 에만 있음)

순으로 중요하며 FATAL 이 가장 중요한 것으로 본다. 이는 심각한 오류를 뜻한다.

application.yml

logging:
  level:
    root: info        # 전체 기본 로그 레벨
    com.example.demo: debug  # 특정 패키지만 DEBUG 레벨
  file:
    name: logs/app.log # 로그 파일 저장 경로

Spring Boot 로깅 설정

SLF4J API 를 통해 로그를 남기고, 실제 로깅은 구현체(Logback) 이 이를 처리한다, SLF4J 에 설정만 가하면 다양한 로그 커스터마이징 기능들을 사용할 수 있게 된다.

로그 설정 방식은 크게 두 가지이다:

  • application.properties / application.yml: 간단 설정
  • logback-spring.xml / log4j2-spring.xml: 정교한 제어
properties, yml 을 통한 설정

레벨 설정

  • logging.level.root=INFO: info 이하는 모두 출력
  • logging.level.com.example=DEBUG: 패키지/클래스 단위로 사용 가능

파일/콘솔

  • logging.file.name=logs/app.log: 파일 하나에 기록
  • logging.file.path=/var/log/myapp: 디렉토리 지정
  • logging.file.max-size=10MB
  • logging.file.max-history=30

패턴(포맷)

  • logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %logger{36} - %msg%n
  • logging.pattern.file

출력할 포맷을 지정할 수 있는데, 콘솔과 파일 별로 가능하다.

xml 을 통한 고급설정

xml 을 굳이 사용하는 이유는 롤링, 포맷, 서로 다른 appender 분리, 비동기 설정 등의 세세한 제어를 다 가져갈 수 있게 하기 위해 사용한다.

<configuration scan="true" scanPeriod="30 seconds">
  <property name="LOG_HOME" value="./logs" />

  <!-- 패턴 정의 -->
  <property name="PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"/>

  <!-- 콘솔 appender -->
  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern>${PATTERN}</pattern>
    </encoder>
  </appender>

  <!-- 롤링 파일 appender -->
  <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <file>${LOG_HOME}/app.log</file>
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
      <!-- 7일치 보관, 하루 단위로 롤링 -->
      <fileNamePattern>${LOG_HOME}/app.%d{yyyy-MM-dd}.log.gz</fileNamePattern>
      <maxHistory>7</maxHistory>
    </rollingPolicy>
    <encoder>
      <pattern>${PATTERN}</pattern>
    </encoder>
  </appender>

  <!-- 비동기 wrapper (성능) -->
  <appender name="ASYNC_FILE" class="ch.qos.logback.classic.AsyncAppender">
    <appender-ref ref="FILE"/>
    <queueSize>5000</queueSize>
    <discardingThreshold>0</discardingThreshold>
  </appender>

  <!-- 로거 레벨 설정 -->
  <root level="INFO">
    <appender-ref ref="STDOUT"/>
    <appender-ref ref="ASYNC_FILE"/>
  </root>

  <!-- 특정 패키지 DEBUG -->
  <logger name="com.example.demo" level="DEBUG"/>
</configuration>

Logback

Spring Boot 에서는 Logback 이 SLF4J 의 기본값이며, Spring Boot 는 로거의 설정을 아래 파일들을 통해 자동으로 탐색한다.

  1. logback-spring.xml (Spring Boot 전용)
  2. logback.xml
  3. logback-test.xml (테스트 환경용)

logback-spring.xml 을 사용하면 Spring Profile, Property Placeholder 등을 지원하기 때문에 해당 파일을 사용하는게 더 좋을것이다.

logback-spring.xml 로딩 과정

스프링 부트 실행 시에 SpringApplication.run() 내부에서 LoggingApplicationListener 가 등록된다.

이때 클래스가 어플리케이션 초기 단계에서 로깅 시스템을 설정하게 된다(이벤트가 들어오면 로그 설정 동작이 실행됨).

동작

  1. 로깅 초기화 트리거
  2. LoggingSystem 결정: Logback, Log4j2, Java Util Logging 순으로 구현체 탐색
  3. Logback 이 선택됐다면 LogbackLoggingSystem 초기화
@Override
public void initialize(LoggingInitializationContext initializationContext,
                       String configLocation, LogFile logFile)
  1. Spring Boot 전용 설정 처리
    • <springProfile> 태그 지원
    • ${} property placeholder 지원
    • Spring Environment 연결
  2. 실제 Logback 구성 로딩(xml 내부의 LoggerContext, Appender, Logger, Layout 로딩)
⭐️ Logback 클래스 구조
LoggerContext (ch.qos.logback.classic.LoggerContext)
│
├── Logger (ch.qos.logback.classic.Logger)
│    ├── Appenders (ConsoleAppender, FileAppender, RollingFileAppender)
│    └── Level (TRACE, DEBUG, INFO, WARN, ERROR)
│
├── AppenderBase (ch.qos.logback.core.AppenderBase)
│    ├── ConsoleAppender
│    ├── FileAppender
│    └── RollingFileAppender
│
└── Layout (ch.qos.logback.classic.PatternLayout)
  • LoggerContext: Logback 전체 설정 컨테이너
  • Logger: 패키지별 로거
  • Appender: 로그를 출력 할 대상(File, Console 등등)
    • 파일이면 파일 어펜더, 콘솔이면 콘솔 어펜더라고도 부름
  • Encoder/Layout: 로그 메시지 포맷 지정

⭐️ 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{HH:mm:ss.SSS} [%thread] %highlight(%-5level) %cyan(%logger{36}) - %msg%n</pattern>
            <charset>UTF-8</charset>
        </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.SizeAndTimeBasedRollingPolicy">
            <!-- 일별 롤링 및 크기 제한 -->
            <fileNamePattern>${LOG_DIR}/archived/${LOG_FILE}-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <maxFileSize>10MB</maxFileSize>
            <maxHistory>30</maxHistory>
            <totalSizeCap>1GB</totalSizeCap>
        </rollingPolicy>

        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
            <charset>UTF-8</charset>
        </encoder>
    </appender>

    <!-- 에러 전용 Appender -->
    <appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOG_DIR}/error.log</file>

        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>ERROR</level>
        </filter>

        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${LOG_DIR}/archived/error-%d{yyyy-MM-dd}.log</fileNamePattern>
            <maxHistory>30</maxHistory>
        </rollingPolicy>

        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>
        </encoder>
    </appender>

    <!-- 비동기 Appender (성능 향상) -->
    <appender name="ASYNC_FILE" class="ch.qos.logback.classic.AsyncAppender">
        <appender-ref ref="FILE"/>
        <queueSize>512</queueSize>
        <discardingThreshold>0</discardingThreshold>
        <includeCallerData>false</includeCallerData>
    </appender>

    <!-- Spring Profile별 설정 -->
    <springProfile name="dev">
        <logger name="com.example" level="DEBUG"/>
        <logger name="org.springframework.web" level="DEBUG"/>
    </springProfile>

    <springProfile name="prod">
        <logger name="com.example" level="INFO"/>
        <logger name="org.springframework" level="WARN"/>
    </springProfile>

    <!-- 패키지별 로거 설정 -->
    <logger name="org.hibernate.SQL" level="DEBUG" additivity="false">
        <appender-ref ref="FILE"/>
    </logger>

    <logger name="com.example.service" level="DEBUG" additivity="false">
        <appender-ref ref="FILE"/>
        <appender-ref ref="CONSOLE"/>
    </logger>

    <!-- Root 로거 -->
    <root level="INFO">
        <appender-ref ref="CONSOLE"/>
        <appender-ref ref="ASYNC_FILE"/>
        <appender-ref ref="ERROR_FILE"/>
    </root>

</configuration>

Logger 가져오기

저 정도 설정을 하면 이제 로거를 들고올 준비가 끝났으며, 클래스 내부에서 로그를 찍어볼 수 있다. 이전에 봤던 필드 변수로 선언하여 LoggerFactory.getLogger() 를 사용하여 들고올 수 있지만, 더 쉽게 @Slf4j 어노테이션을 클래스에 붙이는 것만으로 클래스 내부에서 log 를 통해 사용할 수 있다.

@Controller
@Slf4j
public class WelcomeController {
    ...
    public String hello() {
        log.info("Welcome to the WelcomeController.");

		log.info(message);
        ...

MDC 를 통한 멀티 스레드 환경, 사용자/요청 단위의 로그 추적하기

MDC 는 Mapped Diagnostic Context 의 약자로 스레드 로컬 기반의 로그 컨텍스트 저장소이다.

  • 현재 스레드에서만 유효한 key-value 데이터를 저장
  • 로그 패턴에서 그 값을 자동으로 출력할 수 있도록 하는 기능

따라서 MDC 는 내부적으로 ThreadLocal<Map<String, String>> 구조를 사용하게 된다.

MDC.put("userId", "A123");
MDC.put("requestId", "req-20251016");

log.info("사용자 요청 처리 중...");

MDC.clear();

// 2025-10-16 13:45:12 [INFO] [userId=A123] [requestId=req-20251016] 사용자 요청 처리 중...

위와 같이 출력이 되게 된다. 여기서 출력 포맷은 logback-spring.xml 에서 %X{key} 형태로 사용한다면, 다음과 같이 출력된다.

// <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg [%X{userId}] [%X{requestId}]%n</pattern> 일때
13:45:12.234 [http-nio-8080-exec-1] INFO  c.e.UserController - 요청 처리  [A123] [req-20251016]

X 를 스레드라고 보면 되고, 거기에 해당하는 userId, requestId 는 코드 내에서 put 했던 값을 호출하여서 출력하게 할 수 있다.

Thead 간 MDC 주의점

MDC 는 ThreadLocal 기반임을 주의하자. 스레드가 바뀌면 값이 전파되지 않기 때문에 다음 코드를 쳐도 traceIdnull 로 되게 된다.

MDC.put("traceId", "abc-123");

CompletableFuture.runAsync(() -> {
    log.info("비동기 로그"); // traceId 출력 ❌ (전파 안됨)
});

위를 해결하려면 수동 복사를 통한 방법이 있다. 비동기나 Reactor, WebFlux 등 환경에서 MDC context 전파를 수동 또는 자동으로 하도록 한다.

개념

  • @Async (Spring MVC 내에 있는): 단일 스레드 풀 기반 비동기 처리 (기존 MVC 구조 유지)
  • Reactor (Project Reactor): Reactive Streams 기반의 비동기 데이터 처리 라이브러리
  • Spring WebFlux: Reactor 위에 만들어지는 비동기/논블로킹 웹 프레임워크, Spring MVC 대체 가능
Map<String, String> context = MDC.getCopyOfContextMap();
CompletableFuture.runAsync(() -> {
    MDC.setContextMap(context);
    log.info("비동기 로그");
    MDC.clear();
});

Spring Cloud Sleuth / Micrometer Tracing 을 사용할 수도 있다. 이는 나중에 여유 시간이 되면 봐도 좋다. 당장 안사용한다.

MDC 를 사용할 일이 잘 없다.

MDC 구조
org.slf4j.MDC
 └── static MDCAdapter mdcAdapter = new LogbackMDCAdapter();

ch.qos.logback.classic.util.LogbackMDCAdapter
 └── ThreadLocal<Map<String, String>> copyOnThreadLocal
  • SLF4J 의 MDC API 는 인터페이스 수준의 표준
  • Logback 이 실제 구현
  • 스레드 별로 Map 을 보관

SLF4J Parameterized Logging

SLF4J 에서는 플레이스 홀더 기능을 제공한다. String.format() 처럼 + 연산이나 문자열 합치는 연산 없이도 {} 를 통해 lazy evaluation 을 하도록 한다.

이를 통해 성능을 최적화할 수 있고, 또 is(LOG_LEVEL)Enable() 함수를 통해 로그 레벨이 활성화 된 경우에만 평가하도록 할 수 있다.

// BAD - 항상 문자열 연결 수행
logger.debug("Processing " + expensiveMethod() + " items");

// GOOD - 로그 레벨 체크 후 실행
if (logger.isDebugEnabled()) {
    logger.debug("Processing {} items", expensiveMethod());
}

// BETTER - 파라미터화된 메시지 사용
logger.debug("Processing {} items for user {}",
             itemCount, userId);

// BEST (Java 8+) - Supplier 사용
logger.debug("Processing items: {}",
             () -> expensiveMethod());

✒️ 용어

Property Placeholder

Spring 및 Logback 설정에서 자주 사용되는 기능으로, 외부에 정의된 값을 설정 파일이나 환경 변수에서 참조할 수 있도록 해주는 플레이스홀더(변수 대체) 기능이다.

즉, 반복적으로 사용되는 값(예: 로그 디렉토리, 포트 번호, 파일 이름 등)을 한 곳에서 정의하고 다른 곳에서 ${} 문법으로 참조할 수 있게 해줌