JAVA

Java 효과적인 로깅 전략: SLF4J + Logback, 로그 레벨 가이드, 안티패턴 정리

IT Lab 2026. 2. 28. 21:48

Java 17 기준으로 SLF4J + Logback 조합에서 로그 레벨을 일관되게 운영하는 방법과 실무에서 자주 터지는 로깅 안티패턴을 정리합니다.

도입 (문제 상황)

장애가 났을 때 로그를 열어보면, 정작 필요한 정보는 없고 비슷한 문장만 수천 줄 쌓여 있는 경험이 있으실 거예요. 반대로 로그가 너무 조용해서 “왜 실패했는지”를 재현해야만 알 수 있는 경우도 자주 생깁니다. 로깅은 많이 찍는 기술이 아니라, 필요한 순간에 정확히 찾을 수 있게 남기는 기술입니다.

핵심 개념 (Java 로깅 전략의 기준점: SLF4J + Logback)

SLF4J 파사드와 Logback 구현체, 그리고 콘솔/파일로 출력되는 흐름을 보여주는 로깅 아키텍처 다이어그램

Java에서 로깅 전략을 세울 때 가장 먼저 정해야 하는 건 “어떤 API로 호출하고, 어떤 구현체로 출력할지”입니다. 실무에서는 SLF4J(파사드) + Logback(구현체) 조합이 사실상 표준처럼 쓰입니다. 코드에서는 SLF4J로만 로깅하고, 운영 환경에서 Logback 설정으로 포맷/레벨/출력 대상을 통제할 수 있어요. 즉, 로깅을 “코드 변경”이 아니라 “설정 변경”으로 운영할 수 있는 구조가 됩니다.

SLF4J + Logback이 중요한 이유

  • 벤더 종속 최소화: 코드가 Logback API에 묶이지 않고 SLF4J에만 의존합니다.
  • 성능과 안정성: Logback은 성숙한 설정/롤링 정책을 제공하고, 운영에서 검증된 조합입니다.
  • 관측 가능성(Observability) 기반: “로그는 사건의 증거”입니다. 요청 단위로 추적 가능한 필드(예: requestId)를 남기면, 장애 분석 시간이 급격히 줄어듭니다.

로그 레벨을 “팀 규칙”으로 만드는 가이드

로그 레벨은 개인 취향이 아니라 운영 비용(스토리지/검색/알람 피로도) 과 직결됩니다. 아래처럼 기준을 정해두면 흔들림이 줄어들어요.

레벨 의미(의도) 언제 쓰면 좋은가 주의점
ERROR 요청/기능이 실패했고 즉시 대응이 필요 예외로 인해 처리 실패, 데이터 손상 가능 너무 많이 찍히면 알람이 무력화됩니다
WARN 실패는 아니지만 이상 징후 재시도 성공, 외부 의존성 지연, fallback 동작 “경고지만 정상”을 남발하면 신뢰도가 떨어집니다
INFO 서비스 운영에 필요한 주요 이벤트 서버 시작/종료, 배치 시작/종료, 주요 상태 변화 모든 요청/응답을 INFO로 남기면 비용 폭증
DEBUG 개발/분석용 상세 흐름 분기/파라미터/중간 결과 확인 운영 기본 레벨로 두지 않는 게 일반적
TRACE 매우 상세(루프/내부 단계) 짧은 시간, 특정 문제 재현에 한정 성능/용량에 가장 치명적

추상적으로 보면 “INFO는 사건의 요약, DEBUG는 사건의 과정, ERROR는 사건의 실패”라고 생각하시면 편합니다. 마치 CCTV가 24시간 모든 프레임을 저장하는 게 아니라, 사건이 발생했을 때 필요한 장면을 빠르게 찾도록 인덱싱하는 것과 비슷해요.

로깅 안티패턴 (운영에서 진짜 문제 되는 것들)

  1. 문자열 더하기로 로그 만들기
  • logger.debug("value=" + expensiveCall()) 같은 코드는 레벨이 꺼져 있어도 문자열 결합/메서드 호출이 실행될 수 있습니다.
  • SLF4J의 {} 플레이스홀더를 쓰면, 해당 레벨이 비활성일 때 불필요한 비용을 줄일 수 있어요.
  1. 예외 스택트레이스를 누락
  • logger.error("failed: {}", e.getMessage()) 만 남기면 “어디서 터졌는지”가 사라집니다.
  • 예외는 마지막 인자로 넘겨 스택트레이스를 반드시 남기는 게 원칙입니다.
  1. 로그에 민감정보(PII/토큰/비밀번호) 출력
  • 운영 로그는 생각보다 많은 사람이 접근합니다(권한이 있는 사람, 외부 위탁, 백업 등).
  • 마스킹/필터링을 기본값으로 두고, 필요한 경우에만 제한적으로 출력해야 합니다.
  1. 요청 단위 상관관계(correlation) 없이 로그를 흩뿌리기
  • 멀티스레드/비동기 환경에서는 같은 요청의 로그가 섞입니다.
  • MDCrequestId 같은 키를 심어두면 검색이 “한 번에” 됩니다.
flowchart LR
  A["Request Start"] --> B["MDC put requestId"]
  B --> C["Business Logs (INFO/DEBUG)"]
  C --> D["Error occurs?"]
  D -->|Yes| E["logger.error with exception"]
  D -->|No| F["Normal response"]
  E --> G["MDC clear"]
  F --> G["MDC clear"]

요청 시작 시 MDC를 세팅하고 종료 시 정리하면, 로그를 requestId로 손쉽게 묶을 수 있습니다.

 

코드 예제 (SLF4J + Logback 설정과 MDC까지 한 번에)

아래 예제는 Java 17 + Maven 기준이며, 그대로 복붙해서 실행할 수 있습니다. 핵심은 (1) SLF4J API로만 로깅 (2) Logback 설정으로 출력 제어 (3) MDC로 requestId를 자동 주입하는 흐름입니다.

실행 방법: mvn -q -DskipTests package && java -jar target/logging-demo-1.0.0.jar

1) pom.xml

<!-- pom.xml -->
<project xmlns="http://maven.apache.org/POM/4.0.0"  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>blog</groupId>
  <artifactId>logging-demo</artifactId>
  <version>1.0.0</version>

  <properties>
    <maven.compiler.release>17</maven.compiler.release>
  </properties>

  <dependencies>
    <!-- SLF4J API -->
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-api</artifactId>
      <version>2.0.13</version>
    </dependency>

    <!-- Logback implementation (SLF4J 2.x compatible) -->
    <dependency>
      <groupId>ch.qos.logback</groupId>
      <artifactId>logback-classic</artifactId>
      <version>1.5.6</version>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-jar-plugin</artifactId>
        <version>3.4.2</version>
        <configuration>
          <archive>
            <manifest>
              <mainClass>blog.LoggingDemoApp</mainClass>
            </manifest>
          </archive>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>

2) logback.xml (src/main/resources/logback.xml)

<!-- src/main/resources/logback.xml -->
<configuration>
  <!-- 운영에서는 INFO, 문제 분석 시 DEBUG로 올리는 식으로 환경별로 분리하는 편이 좋습니다 -->
  <property name="LOG_PATTERN"
            value="%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{36} requestId=%X{requestId} - %msg%n"/>

  <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern>${LOG_PATTERN}</pattern>
    </encoder>
  </appender>

  <!-- 파일 롤링(일 단위 + 용량 제한) -->
  <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <file>logs/app.log</file>
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
      <fileNamePattern>logs/app.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
      <timeBasedFileNamingAndTriggeringPolicy
          class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
        <maxFileSize>10MB</maxFileSize>
      </timeBasedFileNamingAndTriggeringPolicy>
      <maxHistory>7</maxHistory>
      <totalSizeCap>200MB</totalSizeCap>
    </rollingPolicy>
    <encoder>
      <pattern>${LOG_PATTERN}</pattern>
    </encoder>
  </appender>

  <root level="INFO">
    <appender-ref ref="CONSOLE"/>
    <appender-ref ref="FILE"/>
  </root>

  <!-- 특정 패키지만 DEBUG로 (필요할 때만) -->
  <logger name="blog" level="DEBUG"/>
</configuration>

3) 실행 코드 (src/main/java/blog/LoggingDemoApp.java)

package blog;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;

import java.util.UUID;

public class LoggingDemoApp {
    private static final Logger log = LoggerFactory.getLogger(LoggingDemoApp.class);

    public static void main(String[] args) {
        // 요청 시작(예: HTTP 요청이 들어왔다고 가정)
        String requestId = UUID.randomUUID().toString().substring(0, 8);
        MDC.put("requestId", requestId);

        try {
            log.info("request started");

            String userId = "user-123";
            log.debug("load user profile. userId={}", userId);

            // 안티패턴 방지: 문자열 결합 대신 {} 사용
            int result = expensiveCalculation(10);
            log.info("calculation done. result={}", result);

            // 예외 로깅: 스택트레이스를 반드시 남김
            simulateFailure();
            log.info("request finished"); // 여기까지 오면 정상 종료

        } catch (Exception e) {
            log.error("request failed", e);
        } finally {
            // 스레드 재사용(스레드풀) 환경에서 MDC 누수 방지
            MDC.clear();
        }
    }

    private static int expensiveCalculation(int input) {
        // 예시용: 실제로는 DB/외부 API/복잡한 계산 등이 들어갈 수 있습니다.
        return input * 42;
    }

    private static void simulateFailure() {
        throw new IllegalStateException("downstream service timeout");
    }
}

실무 팁

💡 실무에서는: “로그 레벨”을 배포가 아니라 운영 설정으로 바꾸게 해두세요

  • 장애 대응 중에 가장 많이 하는 행동이 “일시적으로 DEBUG를 올려서 증거를 더 모으는 것”입니다.
  • 애플리케이션 재배포 없이 레벨을 바꿀 수 있도록(예: 설정 파일/환경변수/관리 엔드포인트) 운영 전략을 잡아두면 MTTR이 줄어듭니다.
  • 단, DEBUG를 장시간 켜두면 비용이 급증하니 시간 제한대상 패키지 제한을 같이 두는 편이 안전합니다.

💡 실무에서는: “무엇을 남길지” 체크리스트를 먼저 만들면 로그가 정돈됩니다

  • 성공 로그: 무엇을 처리했는지(행위) + 대상 식별자(id) + 결과(건수/상태) 정도면 충분한 경우가 많습니다.
  • 실패 로그: 실패한 행위 + 입력의 핵심 식별자 + 예외(스택트레이스)는 거의 고정 세트입니다.
  • 개인정보/토큰은 기본적으로 마스킹하고, 꼭 필요하면 “길이/해시/마지막 4자리” 같은 형태로 남겨보세요.

핵심 요약: SLF4J로 호출하고 Logback으로 제어하면 로깅을 운영 설정으로 다룰 수 있습니다.
핵심 요약: 레벨 기준을 팀 규칙으로 고정하면 비용과 신뢰도를 동시에 잡을 수 있습니다.
핵심 요약: MDC(requestId)와 예외 스택트레이스는 장애 분석 시간을 크게 줄여줍니다.

다음 글: #34 단위 테스트 시작하기