Spring

Spring 애플리케이션에서 로그를 잘 남기는 방법

애플리케이션 개발 및 운영에 있어 로깅은 종종 간과되지만, 시스템의 건강 상태를 유지하고 문제가 발생했을 때 신속하게 대응하는 데 필수적인 요소이다. 마치 블랙박스처럼, 로깅은 시스템 내부에서 일어나는 모든 활동에 대한 기록을 남겨 문제 해결의 결정적인 단서를 제공한다.

이 포스팅에서는 좋은 Logging에 대해 알아보고, Spring Boot Application에 어떻게 적용할 지 알아본다.

좋은 Logging(로깅)이란 무엇인가?

Logging은 소프트웨어 시스템 내에서 발생하는 이벤트, 오류 및 기타 주목할 만한 활동을 기록하는 과정이다.

Logging의 목표

지금 남기는 로그가, 새벽 3시에 시스템에 문제가 생겼을 때 진짜 고맙게 느껴져야 한다.

Logging의 목표는 단순히 “시스템 로그를 찍는다” 가 아니다. 어떻게 하면 Logging은 시스템 상태를 보여주고, 문제 해결의 실마리가 될 수 있을까?이다.

따라서 Error 로그는 그냥 실패함이 아니라 왜 실패했는지 알 수 있는 컨텍스트가 중요하다. 그래서 처음에는 조금 과하다 싶을 정도로 로깅을 하고, 운영하면서 노이즈로 느껴지는 부분을 줄여가는 방식도 많이 사용한다.

구조화된 Logging

로그를 그냥 텍스트로만 남겨두면 나중에 찾기 매우 힘들기 때문에, 구조화된 로깅이 필요해진다. 보통 json과 같은 key-value 형식으로 저장해두면 기계가 읽고 분석하기 편해진다. 로그마다 정보 하나하나에 의미있는 이름을 부여함으로써, 훨씬 Logging이 편해진다.

예를들어서,

User 123 logged in to system

보다는

{ "timestamp": "2025-05-16T21:15:00+09:00", "id": "123", "level": "info", "event": "logged in" }

이 훨씬 좋다는 이야기이다.

스프링부트와 거의 항상 함께 쓰이는 SLF4J 또는 LogBack 라이브러리는 이런 구조화된 로깅을 잘 지원해준다.

또 로그레벨(Log-Level)은 매우 중요하게 사용된다.

  • INFO : 핵심적인 비즈니스 흐름들을 나타냄
  • WARN : 당장 에러는 아니지만 좀 이상한데? 싶은 정보들을 나타냄
  • ERROR : 확실한 실패를 나타냄 (DB 연결 실패, 결제 실패, 예외 등등)

컨텍스트가 가장 중요하다

사실 로그 메시지 그 자체 보다 이걸 둘러싼 정보, 즉 컨텍스트가 문제 해결의 열쇠일때가 많다.

  • timestamp: 로그시스템에는 꼭 필요한 정보이다.
  • level: 모든 로그를 볼 수 없으니, WARN이나 ERROR 먼저 봐야하므로 기본적으로 포함되어야한다.
  • thread: Spring MVC는 멀티스레드 기반 I/O를 지원하므로 쓰레드 네임을 기록하는 것은 트레이스에 매우 도움이 된다.
  • method: 어떤 메소드에서 로그를 남겼는지도 기록하면 디버깅에 도움이 많이 된다.
  • trace: Trace id를 포함하면 MSA 에서 특히 도움이 많이 된다.
    • 요즘 자주 보이는 MSA의 경우에는 trace id를 가지고 있다면, 서버가 바뀌어도 하나의 타임라인으로 요청을 볼 수 있어서 매우 유용하다.

이러한 정보들을 포함하면 문제가 발생했을 때 컨텍스트를 파악할 수 있고, 이는 트러블 슈팅에 큰 도움이 된다.

로그를 잘 남겼으면 관리를 잘해야한다

로그를 잘 남겼으면 관리도 잘해야한다.

  • MSA 아키텍쳐에서 로그 파일이 흩어져있으면 분석이 불가능하다.
    • 중앙 관리 시스템 (ELK 스택이나 로키와 같은)에 로그를 모두 모아야한다.
  • 얼마나 오래 보관할지 결정해야한다.
    • ERROR 로그는 90일 INFO 로그는 30일과 같이 차등을 두는 것이 보편적이다.
  • 성능을 고려해야한다.
    • CPU도 사용하고 I/O도 사용하는 문제이므로 비효율적인 로깅 방식이나 너무 과한 로그를 남기면 시스템 전체 성능에 영향을 줄 수도 있다.

로그는 만능이 아니다

로그는 무슨 일이 있었나를 상세하게 알려주지만 시스템 전반적인 상태는 어떤지에 대해 알려주는 것에 한계가 있다. 에러율이 점점 오른다와 같은 추세는 Metric이 더 잘 보여준다. 따라서 메트릭으로 이상 신호를 잡고 Log와 Tracing으로 구체적인 원인을 파고드는 것이 가장 효과적이다.

구조화된 로그 작성하기 (Spring Boot)

1. 의존성 설치

dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'net.logstash.logback:logstash-logback-encoder' }

2. Logback 설정 파일 생성

src/main/resources 경로에 logback-spring.xml을 작성

<?xml version="1.0" encoding="UTF-8"?> <configuration> <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> <encoder class="net.logstash.logback.encoder.LogstashEncoder"> <includeMdc>true</includeMdc> <includeContext>false</includeContext> <customFields>{"application":"my-spring-app"}</customFields> </encoder> </appender> <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>logs/application.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>logs/application-%d{yyyy-MM-dd}.log</fileNamePattern> <maxHistory>30</maxHistory> </rollingPolicy> <encoder class="net.logstash.logback.encoder.LogstashEncoder"> <includeMdc>true</includeMdc> <includeContext>false</includeContext> <customFields>{"application":"my-spring-app"}</customFields> </encoder> </appender> <root level="INFO"> <appender-ref ref="CONSOLE" /> <appender-ref ref="FILE" /> </root> </configuration>
  • 콘솔과 파일 두 곳에 로그를 JSON 형식으로 출력한다
  • 날짜별로 로그 파일이 생성되고, 최대 30일 보관한다.
  • 모든 로그에 애플리케이션 이름("my-spring-app")이 포함된다.
  • 기본 로그 레벨은 INFO (INFO, WARN, ERROR 만 기록)

3. MDC(Mapped Diagonic Context) 활용

SLF4J의 MDC를 활용하면 로그에 추가 정보를 포함할 수 있다. 위에서 언급한대로, 웹 애플리케이션에는 요청 ID, 사용자 ID등을 MDC에 추가하는 것이 좋다.

@Component public class LoggingFilter extends OncePerRequestFilter { @Autowired private RequestMappingHandlerMapping handlerMapping; @Override protected void doFilterInternal( \\t\\t\\tHttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { try { // timestamp - logback에서 기본 제공하지만 MDC에도 추가 String timestamp = LocalDateTime.now() \\t\\t\\t\\t\\t\\t.format(DateTimeFormatter.ISO_DATE_TIME); MDC.put("timestamp", timestamp); // thread 정보 추가 MDC.put("thread", Thread.currentThread().getName()); // 요청 ID 생성 (단일 요청 식별용) String requestId = UUID.randomUUID().toString(); MDC.put("requestId", requestId); // 클라이언트 정보 MDC에 추가 MDC.put("clientIp", request.getRemoteAddr()); MDC.put("userAgent", request.getHeader("User-Agent")); MDC.put("requestUrl", request.getRequestURI()); MDC.put("requestMethod", request.getMethod()); filterChain.doFilter(request, response); } finally { // 필터 체인 완료 후 MDC 정리 MDC.clear(); } } }

4. Application 사용 예시

코드에서 Log는 다음과 같이 기록할 수 있다.

@RestController public class DemoController { private static final Logger logger = LoggerFactory.getLogger(DemoController.class); @GetMapping("/hello") public String hello() { logger.info("Hello endpoint called"); try { // 비즈니스 로직 logger.debug("Processing business logic"); // 구조화된 로깅을 위해 중요 데이터 포함 logger.info("Operation completed successfully", "operation", "hello", "processingTime", "10ms"); return "Hello, World!"; } catch (Exception e) { logger.error("Error in hello endpoint", e); throw e; } } }

글을 마치며

스프링부트에서 구조화된 로깅을 구현하면 애플리케이션 모니터링과 문제 해결이 훨씬 용이해진다. 특히 대규모 시스템이나 마이크로서비스 환경에서는 필수적인 요소라고 할 수 있다.

구조화된 로깅을 도입할 때는 다음 사항을 고려해야한다:

  1. 민감한 정보 제외: 개인정보나 보안 데이터가 로그에 포함되지 않도록 주의
  2. 로그 수준 조정: 개발/테스트/운영 환경에 따라 적절히 로그 레벨 조정
  3. 로그 볼륨 관리: 로그가 너무 많이 생성되면 저장 공간 및 성능 이슈 발생 가능
  4. 표준화: 팀 전체가 일관된 로깅 방식을 사용하도록 가이드라인 제시

참고자료

12 Logging BEST Practices in 12 minutes

Observability of Your Application by Marcin Grzejszczak & Tommy Ludwig @ Spring I/O 2023

댓글

로딩 중...