백엔드 서비스를 개발하고 운영하는 과정에서 시스템의 상태를 정확히 파악하고, 문제가 발생했을 때 신속하게 원인을 찾아 해결하는 능력은 매우 중요합니다. 이때 가장 기본적이면서도 강력한 도구가 바로 로그(Log)와 메트릭(Metric)입니다. 이 두 가지를 효과적으로 수집하고 활용하는 것은 안정적인 서비스 운영의 핵심이라고 할 수 있습니다.
이번 글에서는 로그와 메트릭의 기본 개념부터 시작하여, 왜
System.out.println
대신 로깅 프레임워크를 사용해야 하는지, 그리고 Spring Boot 환경에서 메트릭 수집을 위한 강력한 추상화 라이브러리인 마이크로미터(Micrometer), 마지막으로 실질적인 로깅 구현을 위한 Logback 프레임워크의 아키텍처와 설정 방법까지 깊이 있게 다뤄보겠습니다.
◼️ 로그 (Log): 시간의 흐름에 따른 사건의 기록
◼️ 메트릭 (Metric): 시스템 상태를 나타내는 정량적 지표
🤔 경험 공유: 로그와 메트릭 수집 경험
저의 경우, Spring Boot Actuator를 활용하여 애플리케이션 메트릭을 수집하고, 이를 Prometheus에 저장한 뒤 Grafana를 통해 시각화하여 모니터링 대시보드를 구축한 경험이 있습니다. 주요 수집 지표는 다음과 같습니다.
- 시스템 리소스: CPU 사용량, 메모리 사용량, JVM 힙(Heap) 사용량
- 애플리케이션 내부 상태: Tomcat 스레드 풀 활성 스레드 수, 데이터베이스 커넥션 풀 상태(활성/유휴 커넥션 수)
- 오류 발생 현황:
ERROR
레벨 로그 발생 빈도로깅은 Logback 프레임워크를 사용했으며, 로그는 Loki에 7일간 보관하도록 설정했습니다. 특히, 분산 환경에서 요청 추적을 용이하게 하기 위해 MDC(Mapped Diagnostic Context)를 활용하여 각 로그에 트랜잭션 ID나 사용자 ID 등을 포함시켰습니다.
🤓 수집 이유:
- CPU, 메모리, JVM 사용량: 서비스의 전반적인 안정성과 리소스 포화 상태를 파악하여 잠재적인 문제를 사전에 감지하기 위함입니다.
- 톰캣 스레드 풀 및 DB 커넥션 풀 상태: 웹 서버의 동시 요청 처리 능력과 데이터베이스 접근 관련 병목 현상을 모니터링하여, 사용자 요청 처리에 지연이 발생하거나 시스템이 불안정해지는 상황을 신속히 파악하고 대응하기 위함입니다.
- ERROR 레벨 로그 증가량: 예상치 못한 오류 발생 빈도를 추적하여 심각한 버그나 시스템 이상을 즉시 인지하고 조치하기 위함입니다.
System.out.println
대신 로깅 프레임워크를 써야 할까?"간단하게 System.out.println()
으로 로그를 찍으면 되지 않나요?" 라고 생각할 수 있습니다. 하지만 실제 운영 환경에서는 다음과 같은 이유로 로깅 프레임워크(예: Logback, Log4j2, SLF4J) 사용이 강력히 권장됩니다.
System.out.println()
은 동기적으로 동작하며, 특히 많은 양의 로그를 출력할 경우 I/O 대기로 인해 애플리케이션 성능에 부정적인 영향을 미칠 수 있습니다. 로깅 프레임워크는 비동기 로깅, 버퍼링 등 성능 최적화 기능을 제공합니다.TRACE
, DEBUG
, INFO
, WARN
, ERROR
등 다양한 로그 레벨을 지원합니다. 이를 통해 개발 환경에서는 상세한 디버그 로그를, 운영 환경에서는 심각한 오류 위주의 로그만 선택적으로 출력하도록 유연하게 제어할 수 있습니다. System.out.println()
은 이러한 레벨 구분이 어렵습니다.결론적으로, System.out.println()
은 간단한 테스트나 디버깅에는 유용할 수 있지만, 실제 서비스 운영 환경에서는 로깅 프레임워크가 제공하는 강력하고 유연한 기능을 활용하는 것이 훨씬 효율적이고 안정적입니다.
서비스를 안정적으로 운영하기 위해서는 애플리케이션의 CPU, 메모리, 커넥션 사용량, 고객 요청 수 등 다양한 지표(메트릭)를 지속적으로 모니터링해야 합니다. 이를 통해 문제 발생을 사전에 감지하고, 실제 문제가 발생했을 때 원인을 신속하게 파악하여 대처할 수 있습니다.
예를 들어, 메모리 사용량이 급증하는 것을 모니터링 시스템을 통해 발견했다면, 메모리 누수 관련 코드를 빠르게 찾아 수정할 수 있을 것입니다.
현재 시장에는 Prometheus, Datadog, New Relic, Dynatrace 등 수많은 모니터링 툴이 존재합니다. 이러한 툴을 사용하려면 시스템의 다양한 지표를 각 툴이 요구하는 형식에 맞춰 전달해야 합니다.
만약 모니터링 툴을 JMX에서 Prometheus로 변경한다면 어떻게 될까요? 기존에 JMX 형식에 맞춰 작성했던 메트릭 수집 코드를 모두 Prometheus 형식으로 변경해야 하는 번거로움이 발생합니다.
이러한 문제를 해결하기 위해 등장한 것이 바로 마이크로미터(Micrometer) 라이브러리입니다.
◼️ 마이크로미터란? 애플리케이션 메트릭 파사드(Facade)
마이크로미터는 다양한 모니터링 시스템에 대한 측정 지표(메트릭) 수집 인터페이스를 추상화한 라이브러리입니다. 마치 SLF4J가 다양한 로깅 구현체에 대한 추상화를 제공하는 것처럼, 마이크로미터는 다양한 모니터링 툴에 대한 "계측(Instrumentation) 파사드" 역할을 합니다.
◼️ 액추에이터와 마이크로미터를 통한 메트릭 확인
Spring Boot Actuator를 사용하면 /actuator/metrics
엔드포인트를 통해 마이크로미터가 수집하고 있는 다양한 메트릭 목록을 확인할 수 있습니다.
http://localhost:8080/actuator/metrics
http://localhost:8080/actuator/metrics/{metric.name}
(예: jvm.memory.used
)http://localhost:8080/actuator/metrics/{metric.name}?tag=KEY:VALUE
(예: jvm.memory.used?tag=area:heap
)http.server.requests
메트릭의 경우 uri
, method
, status
, exception
, outcome
등의 태그를 사용하여 세분화된 분석이 가능합니다.◼️ 마이크로미터가 기본 제공하는 다양한 메트릭 종류
마이크로미터와 액추에이터는 다음과 같이 다양한 종류의 메트릭을 자동으로 수집하여 제공합니다.
jvm.*
): 메모리 및 버퍼 풀 사용량, 가비지 컬렉션 통계, 스레드 활용도, 클래스 로딩 수, JIT 컴파일 시간 등system.*
, process.*
, disk.*
): CPU 사용량, 파일 디스크립터 수, 시스템 가동 시간, 사용 가능한 디스크 공간 등application.started.time
, application.ready.time
): 애플리케이션 시작 및 요청 처리 준비 완료까지 걸린 시간http.server.requests
): URI별, HTTP 메서드별, 상태 코드별 요청 수, 응답 시간, 예외 발생 수 등jdbc.connections.*
, HikariCP의 경우 hikaricp.*
): 최대/최소/활성/대기 커넥션 수 등 커넥션 풀 상태logback.events
): Logback 사용 시 TRACE, DEBUG, INFO, WARN, ERROR 레벨별 로그 발생 수tomcat.*
): (설정 활성화 필요: server.tomcat.mbeanregistry.enabled: true
) 최대 스레드 수, 현재 바쁜 스레드 수, 세션 관련 정보 등이러한 메트릭들은 Prometheus와 같은 시계열 데이터베이스(TSDB)에 지속적으로 저장하고, Grafana와 같은 대시보드 툴을 사용하여 시각화함으로써 시스템 상태를 한눈에 파악하고 이상 징후를 신속하게 감지하는 데 활용됩니다.
앞서 로깅 프레임워크의 필요성을 언급했습니다. Spring Boot는 기본적으로 SLF4J API와 Logback 구현체를 사용합니다. Logback의 아키텍처와 주요 설정 요소를 이해하면 더욱 효과적인 로깅 전략을 수립할 수 있습니다.
◼️ 로깅이란? 기록의 기술
◼️ 로그 레벨: 정보의 중요도에 따른 분류
로그 레벨은 기록할 로그의 중요도를 나타내며, 이를 통해 특정 레벨 이상의 로그만 선택적으로 출력할 수 있습니다. 일반적인 로그 레벨 순서(낮은 중요도 → 높은 중요도)는 다음과 같습니다.
활용 기준 예시:
◼️ 로깅 vs. 디버깅
◼️ SLF4J (Simple Logging Facade for Java): 로깅 추상화의 힘
logback-classic
) → 로깅 프레임워크 구현체 (예: logback-core
가 실제 로깅 처리)java.util.logging
, log4j 1.x
) 호출을 SLF4J API로 전달해주는 어댑터. 레거시 코드 통합 시 유용.◼️ Logback 아키텍처 및 주요 설정 요소
Logback은 SLF4J의 대표적인 구현체 중 하나이며, 다음과 같은 세 가지 주요 모듈로 구성됩니다.
logback-core
: Appender와 Layout 인터페이스 등 Logback의 핵심 기능을 제공하며, 다른 두 모듈의 기반이 됩니다.logback-classic
: logback-core
를 확장하고 SLF4J API를 구현합니다. 우리가 일반적으로 사용하는 ch.qos.logback.classic.Logger
클래스를 포함하며, SLF4J의 LoggerFactory
가 이 클래스의 인스턴스를 반환합니다.logback-access
: 서블릿 컨테이너(Tomcat, Jetty 등)와 통합되어 HTTP 접근 로그(Access Log) 기능을 제공합니다. (일반적인 애플리케이션 로깅과는 별개)Logback 설정 파일 (logback-spring.xml
또는 logback.xml
)의 핵심 구성 요소:
<configuration>
: 최상위 루트 엘리먼트.<appender>
("어디에 기록할까?"): 로그 이벤트를 실제 목적지(콘솔, 파일 등)에 기록하는 역할을 합니다.ConsoleAppender
: 콘솔에 로그를 출력합니다.FileAppender
: 단일 파일에 로그를 기록합니다.RollingFileAppender
: 날짜, 파일 크기 등 특정 조건에 따라 로그 파일을 롤링(분할 및 아카이브)합니다. 운영 환경에서 가장 많이 사용됩니다.<encoder>
/ <layout>
("어떻게 출력할까?"): 로그 이벤트의 출력 형식을 지정합니다.<pattern>
엘리먼트를 사용하여 타임스탬프, 스레드 이름, 로그 레벨, 로거 이름, 메시지 등을 포함하는 상세한 출력 패턴을 정의할 수 있습니다. (예: %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
)<encoder>
를, ConsoleAppender는 <layout>
이나 <encoder>
를 사용합니다.<logger>
("어떤 로그를, 어떤 레벨로 기록할까?"): 특정 패키지나 클래스에 대한 로그 레벨과 사용할 Appender를 지정합니다. 로거는 계층 구조를 가집니다.<root>
: 모든 로거의 최상위 로거. 기본 로그 레벨과 사용할 Appender를 설정합니다. 특정 <logger>
에서 설정하지 않은 경우 <root>
설정을 상속받습니다.<springProfile name="...">
(Spring Boot 환경): 특정 Spring 프로파일(예: dev
, prod
, test
)이 활성화될 때만 적용되는 설정을 정의할 수 있습니다. 이를 통해 환경별로 다른 로깅 전략을 쉽게 구현할 수 있습니다. (예: 개발 환경에서는 콘솔에 DEBUG 레벨 로그 출력, 운영 환경에서는 파일에 INFO 레벨 로그 기록 및 ERROR 레벨은 별도 파일 기록)<filter>
): Appender 레벨에서 특정 조건(예: 로그 레벨)에 따라 로그 이벤트를 수락(ACCEPT)하거나 거부(DENY)할 수 있습니다. (예: LevelFilter
를 사용하여 특정 Appender에는 특정 레벨의 로그만 기록)<rollingPolicy>
): RollingFileAppender
사용 시 로그 파일을 어떻게 롤링할지 정의합니다. (예: SizeAndTimeBasedRollingPolicy
- 시간 및 크기 기반 롤링)<fileNamePattern>
: 롤링된 파일의 이름 패턴 지정.<maxFileSize>
: 개별 로그 파일의 최대 크기.<maxHistory>
: 보관할 아카이브 파일의 최대 기간(일 수).<totalSizeCap>
: 전체 아카이브 파일의 최대 총 크기.<include resource="...">
: 공통 설정 부분을 별도 파일로 분리하고 메인 설정 파일에서 포함시켜 가독성과 재사용성을 높일 수 있습니다.예시: logback-spring.xml
설정 (운영 환경 - 파일 로깅, 레벨별 분리)
<configuration>
<timestamp key="BY_DATE" datePattern="yyyy-MM-dd"/>
<property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %highlight(%-5level) %cyan(%logger{36}) - %msg%n"/>
<springProfile name="prod">
<!-- INFO 레벨 로그 파일 어펜더 -->
<appender name="FILE-INFO" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>./logs/info/${BY_DATE}.log</file>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>INFO</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<encoder>
<pattern>${LOG_PATTERN}</pattern>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>./logs/backup/info/info-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>30</maxHistory>
<totalSizeCap>3GB</totalSizeCap>
</rollingPolicy>
</appender>
<!-- ERROR 레벨 로그 파일 어펜더 (WARN 레벨도 포함 가능) -->
<appender name="FILE-ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>./logs/error/${BY_DATE}.log</file>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter"> <!-- WARN 레벨 이상만 기록 -->
<level>WARN</level>
</filter>
<encoder>
<pattern>${LOG_PATTERN}</pattern>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>./logs/backup/error/error-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>30</maxHistory>
<totalSizeCap>1GB</totalSizeCap>
</rollingPolicy>
</appender>
<root level="INFO">
<appender-ref ref="FILE-INFO"/>
<appender-ref ref="FILE-ERROR"/>
</root>
</springProfile>
<springProfile name="dev,local">
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${LOG_PATTERN}</pattern>
</encoder>
</appender>
<root level="DEBUG"> <!-- 개발 환경에서는 DEBUG 레벨까지 출력 -->
<appender-ref ref="CONSOLE"/>
</root>
</springProfile>
</configuration>
주의: 로그 메시지 포매팅 시, logger.info("User " + userId + " logged in");
과 같이 문자열 결합(+)을 사용하는 것보다 logger.info("User {} logged in", userId);
와 같이 파라미터화된 메시징(Parameterized Messaging)을 사용하는 것이 성능상 유리합니다. 로그 레벨이 비활성화되어 실제 로그가 출력되지 않더라도, 문자열 결합 방식은 불필요한 문자열 객체 생성 및 연산 오버헤드가 발생할 수 있기 때문입니다.
로그와 메트릭은 백엔드 시스템의 건강 상태를 진단하고 문제를 해결하는 데 있어 의사의 청진기와 CT 촬영 장비와도 같습니다. 마이크로미터를 통해 표준화된 방식으로 메트릭을 수집하고, SLF4J와 Logback을 통해 유연하고 강력한 로깅 시스템을 구축함으로써, 우리는 더욱 안정적이고 예측 가능한 서비스를 운영할 수 있습니다.
단순히 정보를 기록하는 것을 넘어, 수집된 로그와 메트릭을 어떻게 분석하고 활용하여 서비스 개선으로 이끌어낼 것인지 고민하는 것이 진정한 프로 개발자의 자세일 것입니다. 이 글이 여러분의 로깅 및 모니터링 전략 수립에 조금이나마 도움이 되었기를 바랍니다.