🤖

본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.

⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

이미지 로딩 중...

관찰 가능한 마이크로서비스 완벽 가이드 - 슬라이드 1/7
A

AI Generated

2025. 12. 22. · 4 Views

관찰 가능한 마이크로서비스 완벽 가이드

마이크로서비스 환경에서 시스템의 상태를 실시간으로 관찰하고 모니터링하는 방법을 배웁니다. Resilience4j, Zipkin, Prometheus, Grafana, EFK 스택을 활용하여 안정적이고 관찰 가능한 시스템을 구축하는 실전 가이드입니다.


목차

  1. 아키텍처_설계
  2. Resilience4j_적용
  3. Zipkin_연동
  4. Prometheus_Grafana_구성
  5. EFK_스택_구성
  6. 통합_대시보드

1. 아키텍처 설계

김개발 씨는 이커머스 회사에 입사한 지 6개월 된 주니어 개발자입니다. 어느 날 갑자기 주문 서비스가 응답하지 않자, 어디서 문제가 생겼는지 찾는 데만 2시간이 걸렸습니다.

팀장님이 다가와 말씀하셨습니다. "김 개발자님, 우리 시스템에 관찰 가능성을 적용해야 할 때가 됐네요."

관찰 가능한 마이크로서비스는 시스템의 내부 상태를 외부에서 실시간으로 파악할 수 있도록 설계하는 것입니다. 마치 자동차 계기판처럼, 시스템의 속도, 연료, 엔진 상태를 한눈에 볼 수 있게 만드는 것이죠.

이를 통해 장애가 발생했을 때 빠르게 원인을 찾고, 사전에 문제를 예방할 수 있습니다. 관찰 가능성의 세 가지 기둥은 메트릭, 로그, 트레이스입니다.

다음 코드를 살펴봅시다.

// Spring Boot 마이크로서비스 기본 구조
@SpringBootApplication
public class ObservableServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(ObservableServiceApplication.class, args);
    }

    // 관찰 가능성을 위한 Bean 설정
    @Bean
    public MeterRegistry meterRegistry() {
        return new SimpleMeterRegistry();
    }
}

김개발 씨가 경험한 2시간짜리 장애 추적은 마이크로서비스 환경에서 흔히 겪는 악몽입니다. 주문 서비스가 느려졌는데, 원인이 결제 서비스인지, 재고 서비스인지, 아니면 데이터베이스인지 알 수가 없었습니다.

각 서비스의 로그를 일일이 뒤져봐야 했고, 그마저도 어떤 요청이 어느 서비스를 거쳐갔는지 추적하기 어려웠습니다. 팀장 박시니어 씨는 이런 상황을 여러 번 겪어왔습니다.

"김 개발자님, 마이크로서비스는 분산 시스템이에요. 단일 애플리케이션과 달리, 여러 서비스가 네트워크로 연결되어 있죠.

하나의 사용자 요청이 5개, 10개의 서비스를 거쳐갈 수 있어요." 그렇다면 관찰 가능성이란 정확히 무엇일까요? 쉽게 비유하자면, 관찰 가능성은 마치 병원의 종합 건강검진과 같습니다.

혈압, 혈당, 심박수 같은 수치를 측정하고, X-ray로 내부를 들여다보고, 의사가 증상을 기록합니다. 이처럼 시스템도 메트릭(수치), 트레이스(흐름), 로그(기록)를 통해 건강 상태를 파악할 수 있습니다.

관찰 가능성이 없던 시절에는 어땠을까요? 개발자들은 장애가 발생하면 각 서버에 SSH로 접속해서 로그 파일을 직접 열어봐야 했습니다.

서비스가 10개라면 10개의 서버를 돌아다니며 로그를 확인해야 했죠. 더 큰 문제는 요청이 어떤 경로로 흘러갔는지 알 수 없다는 것이었습니다.

A 서비스에서 B 서비스를 호출했고, B가 C를 호출했는데, 어디서 느려진 건지 추측만 할 수 있었습니다. 바로 이런 문제를 해결하기 위해 관찰 가능한 아키텍처가 등장했습니다.

관찰 가능한 아키텍처를 설계할 때는 세 가지 핵심 요소를 고려해야 합니다. 첫째, 메트릭 수집입니다.

CPU 사용률, 메모리, 요청 처리 시간, 에러 비율 같은 수치를 수집합니다. 둘째, 분산 트레이싱입니다.

하나의 요청이 여러 서비스를 거쳐가는 전체 흐름을 추적합니다. 셋째, 중앙화된 로깅입니다.

모든 서비스의 로그를 한곳에 모아서 검색할 수 있게 만듭니다. 위의 코드를 한 줄씩 살펴보겠습니다.

먼저 SpringBootApplication 어노테이션으로 스프링 부트 애플리케이션을 선언합니다. 이것이 우리 마이크로서비스의 시작점입니다.

다음으로 MeterRegistry Bean을 등록하는데, 이것이 바로 메트릭을 수집하는 핵심 컴포넌트입니다. 나중에 Prometheus와 연동하면 이 레지스트리를 통해 모든 메트릭이 수집됩니다.

실제 현업에서는 어떻게 활용할까요? 예를 들어 쇼핑몰 서비스를 개발한다고 가정해봅시다.

사용자가 "구매하기" 버튼을 누르면, 주문 서비스가 재고 서비스를 호출하고, 재고 서비스가 확인되면 결제 서비스를 호출합니다. 만약 결제 서비스가 느려진다면, 트레이싱을 통해 정확히 결제 서비스에서 3초가 걸렸다는 것을 알 수 있습니다.

또한 메트릭을 보면 결제 서비스의 CPU가 90%라는 것을 발견할 수 있죠. 하지만 주의할 점도 있습니다.

초보 개발자들이 흔히 하는 실수 중 하나는 너무 많은 메트릭을 수집하려는 것입니다. 모든 변수, 모든 메서드 호출을 측정하면 오히려 시스템 성능이 떨어지고 중요한 지표를 놓칠 수 있습니다.

따라서 핵심 지표에 집중해야 합니다. 보통 요청 처리 시간, 에러율, 처리량, 리소스 사용률 정도면 충분합니다.

또 다른 실수는 로그를 구조화하지 않고 그냥 텍스트로 남기는 것입니다. "사용자 로그인 성공"이라고만 쓰면 나중에 검색하기 어렵습니다.

대신 JSON 형태로 "userId: 12345, action: login, status: success, timestamp: 2025-12-22T10:30:00"처럼 구조화하면 검색과 분석이 훨씬 쉬워집니다. 아키텍처를 설계할 때는 서비스 간 통신 방식도 고려해야 합니다.

REST API를 사용한다면 HTTP 헤더에 Trace ID를 전파해야 합니다. 이 ID가 있어야 요청의 전체 흐름을 추적할 수 있기 때문입니다.

메시지 큐를 사용한다면 메시지에 Trace ID를 포함시켜야 합니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

박시니어 팀장의 설명을 들은 김개발 씨는 고개를 끄덕였습니다. "아, 그래서 처음부터 관찰 가능성을 고려해서 설계해야 하는 거군요!" 관찰 가능한 아키텍처를 제대로 설계하면 장애 대응 시간을 몇 시간에서 몇 분으로 단축할 수 있습니다.

여러분도 오늘 배운 내용을 바탕으로 다음 프로젝트를 설계해 보세요.

실전 팁

💡 - 처음부터 Trace ID 전파 체계를 구축하세요. 나중에 추가하려면 모든 서비스를 수정해야 합니다.

  • 핵심 메트릭 4가지(요청 시간, 에러율, 처리량, 리소스)에 집중하세요.
  • 로그는 반드시 JSON 형태로 구조화하세요. 검색과 분석이 100배 쉬워집니다.

2. Resilience4j 적용

김개발 씨는 주문 서비스를 개발하던 중 큰 문제에 직면했습니다. 결제 서비스가 다운되자 주문 서비스까지 멈춰버린 것입니다.

박시니어 팀장이 모니터를 보며 말했습니다. "서비스 하나가 죽으면 전체가 죽는 구조네요.

회복 탄력성이 필요합니다."

Resilience4j는 분산 시스템에서 장애가 전파되는 것을 막는 라이브러리입니다. 마치 전기 회로의 차단기처럼, 문제가 생긴 부분을 격리해서 전체 시스템을 보호합니다.

Circuit Breaker, Rate Limiter, Retry, Bulkhead 같은 패턴을 제공하며, 각각의 상태를 메트릭으로 노출할 수 있습니다. 이를 통해 시스템이 장애 상황에서도 부분적으로 동작할 수 있게 만듭니다.

다음 코드를 살펴봅시다.

// Resilience4j Circuit Breaker 적용
@Service
public class PaymentService {

    private final CircuitBreaker circuitBreaker;
    private final WebClient webClient;

    public PaymentService(CircuitBreakerRegistry registry) {
        // 결제 서비스용 Circuit Breaker 생성
        this.circuitBreaker = registry.circuitBreaker("payment");
        this.webClient = WebClient.create("http://payment-service");
    }

    public PaymentResponse processPayment(PaymentRequest request) {
        // Circuit Breaker로 감싸서 호출
        return circuitBreaker.executeSupplier(() -> {
            return webClient.post()
                .uri("/payments")
                .bodyValue(request)
                .retrieve()
                .bodyToMono(PaymentResponse.class)
                .block();
        });
    }
}

김개발 씨가 겪은 상황은 연쇄 장애의 전형적인 예입니다. 결제 서비스가 응답하지 않자, 주문 서비스는 타임아웃될 때까지 기다렸습니다.

문제는 그동안 주문 서비스의 스레드가 계속 블로킹되었다는 것입니다. 새로운 주문 요청이 들어와도 처리할 수 없는 상태가 되었죠.

결국 주문 서비스도 다운되었고, 전체 시스템이 마비되었습니다. 박시니어 팀장은 화이트보드에 간단한 그림을 그렸습니다.

"김 개발자님, 이게 바로 장애 전파입니다. 한 서비스의 문제가 도미노처럼 다른 서비스로 번져나가는 거죠.

이걸 막으려면 회복 탄력성 패턴이 필요해요." 그렇다면 Resilience4j란 정확히 무엇일까요? 쉽게 비유하자면, Resilience4j는 마치 집의 안전장치와 같습니다.

전기가 과부하되면 차단기가 내려가서 화재를 방지하고, 가스가 새면 자동으로 차단됩니다. 수도관이 터져도 메인 밸브를 잠가서 피해를 최소화합니다.

이처럼 Resilience4j도 서비스에 문제가 생기면 자동으로 차단하고, 재시도하고, 속도를 제한해서 시스템을 보호합니다. 회복 탄력성 패턴이 없던 시절에는 어땠을까요?

개발자들은 모든 외부 호출에 try-catch를 붙이고, 타임아웃을 설정하고, 수동으로 재시도 로직을 작성해야 했습니다. 코드가 복잡해지고, 실수하기 쉬웠으며, 일관성도 없었습니다.

더 큰 문제는 장애 상황을 감지하고 대응하는 것이 느렸다는 것입니다. 서비스가 계속 실패하는데도 계속 호출을 시도하다가 전체 시스템이 다운되곤 했습니다.

바로 이런 문제를 해결하기 위해 Resilience4j가 등장했습니다. Resilience4j는 여러 가지 회복 탄력성 패턴을 제공합니다.

가장 중요한 것이 Circuit Breaker입니다. 전기 차단기처럼, 실패율이 임계값을 넘으면 회로를 열어서(Open) 더 이상 호출하지 않습니다.

일정 시간 후 반쯤 열린(Half-Open) 상태로 전환해서 테스트 요청을 보내고, 성공하면 다시 닫힙니다(Closed). 이 모든 상태 변화가 메트릭으로 기록됩니다.

두 번째는 Retry 패턴입니다. 일시적인 네트워크 오류 같은 경우, 몇 번 재시도하면 성공할 수 있습니다.

Resilience4j는 exponential backoff를 지원해서, 1초 후, 2초 후, 4초 후처럼 간격을 늘려가며 재시도합니다. 세 번째는 Rate Limiter입니다.

특정 서비스에 대한 호출을 초당 100개로 제한하는 식으로, 과부하를 방지합니다. 네 번째는 Bulkhead로, 스레드 풀을 격리해서 한 서비스의 문제가 다른 서비스에 영향을 주지 않게 합니다.

위의 코드를 한 줄씩 살펴보겠습니다. 먼저 CircuitBreakerRegistry에서 "payment"라는 이름의 Circuit Breaker를 가져옵니다.

이 레지스트리는 설정 파일에서 정의한 규칙을 기반으로 Circuit Breaker를 생성합니다. 다음으로 executeSupplier 메서드로 실제 결제 서비스 호출을 감쌉니다.

이제 이 호출은 Circuit Breaker의 보호를 받게 됩니다. 실패율이 50%를 넘으면 자동으로 회로가 열리고, fallback 로직이 실행됩니다.

실제 현업에서는 어떻게 활용할까요? 예를 들어 쇼핑몰 서비스에서 추천 시스템을 호출한다고 가정해봅시다.

추천 서비스가 느려지거나 다운되어도, 메인 쇼핑 기능은 동작해야 합니다. Circuit Breaker를 적용하면, 추천 서비스가 응답하지 않을 때 즉시 fallback으로 "인기 상품" 목록을 보여줄 수 있습니다.

사용자는 추천이 조금 덜 정확하더라도 쇼핑은 계속할 수 있습니다. 설정 파일에서는 Circuit Breaker의 동작을 세밀하게 조정할 수 있습니다.

실패율 임계값을 50%로 설정하고, 최소 호출 수를 10으로 설정하면, 최소 10번 호출 중 5번 이상 실패해야 회로가 열립니다. Open 상태에서 Half-Open으로 전환하는 대기 시간도 설정할 수 있습니다.

하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 모든 호출에 Circuit Breaker를 적용하는 것입니다.

데이터베이스 호출처럼 필수적인 작업에는 Circuit Breaker가 적합하지 않을 수 있습니다. 대신 Retry나 Timeout을 사용하는 것이 나을 수 있습니다.

Circuit Breaker는 주로 부가 기능이나 외부 서비스 호출에 적용하는 것이 좋습니다. 또 다른 실수는 fallback 로직을 제대로 구현하지 않는 것입니다.

Circuit Breaker가 열리면 fallback이 실행되는데, 이 로직이 없거나 잘못되면 오히려 더 큰 문제가 생깁니다. Fallback은 항상 빠르고 안전하게 동작해야 합니다.

Resilience4j의 모든 상태는 메트릭으로 노출됩니다. Circuit Breaker의 상태(Closed, Open, Half-Open), 실패율, 느린 호출 비율 등을 Prometheus로 수집할 수 있습니다.

이를 Grafana 대시보드에 표시하면, 어떤 서비스가 자주 실패하는지, Circuit Breaker가 언제 열리는지 한눈에 볼 수 있습니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

Resilience4j를 적용한 후, 결제 서비스가 다운되어도 주문 서비스는 계속 동작했습니다. 사용자에게는 "결제 서비스 점검 중입니다.

나중에 다시 시도해주세요"라는 메시지가 표시되었고, 다른 기능은 정상적으로 사용할 수 있었습니다. 회복 탄력성을 제대로 구현하면 시스템이 부분 장애 상황에서도 계속 서비스할 수 있습니다.

여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - Circuit Breaker는 부가 기능이나 외부 서비스 호출에 적용하세요. 필수 기능에는 신중하게 판단하세요.

  • Fallback 로직은 항상 빠르고 안전하게 동작해야 합니다. 데이터베이스 조회 같은 무거운 작업은 피하세요.
  • 실패율 임계값과 최소 호출 수를 현실적으로 설정하세요. 너무 민감하면 불필요하게 자주 열립니다.

3. Zipkin 연동

김개발 씨는 사용자 한 명이 "주문이 너무 느려요"라고 신고한 건을 조사하고 있었습니다. 주문 서비스, 재고 서비스, 결제 서비스, 배송 서비스...

어디서 느린 건지 감이 잡히지 않았습니다. 박시니어 팀장이 Zipkin 대시보드를 열며 말했습니다.

"여기 보세요. 배송 서비스에서 8초나 걸렸네요."

Zipkin은 분산 트레이싱 시스템으로, 하나의 요청이 여러 마이크로서비스를 거쳐가는 전체 흐름을 시각화합니다. 마치 택배 추적 시스템처럼, 요청이 각 서비스에서 얼마나 시간을 보냈는지 타임라인으로 보여줍니다.

Spring Cloud Sleuth와 함께 사용하면 자동으로 Trace ID와 Span ID를 생성하고 전파합니다. 이를 통해 병목 지점을 빠르게 찾고, 서비스 간 의존성을 파악할 수 있습니다.

다음 코드를 살펴봅시다.

// Spring Cloud Sleuth + Zipkin 설정
@Configuration
public class TracingConfig {

    @Bean
    public Sampler defaultSampler() {
        // 모든 요청을 샘플링 (운영에서는 0.1 정도 권장)
        return Sampler.ALWAYS_SAMPLE;
    }
}

// application.yml
// spring:
//   zipkin:
//     base-url: http://zipkin-server:9411
//   sleuth:
//     sampler:
//       probability: 1.0
//   application:
//     name: order-service

// 자동으로 트레이싱되는 서비스 호출
@RestController
public class OrderController {

    private final WebClient webClient;

    @PostMapping("/orders")
    public OrderResponse createOrder(@RequestBody OrderRequest request) {
        // Sleuth가 자동으로 Trace ID를 HTTP 헤더에 추가
        InventoryResponse inventory = webClient.get()
            .uri("http://inventory-service/check")
            .retrieve()
            .bodyToMono(InventoryResponse.class)
            .block();

        // 각 외부 호출이 하나의 Span으로 기록됨
        PaymentResponse payment = webClient.post()
            .uri("http://payment-service/pay")
            .retrieve()
            .bodyToMono(PaymentResponse.class)
            .block();

        return new OrderResponse("success");
    }
}

김개발 씨가 겪은 문제는 마이크로서비스 환경의 가장 큰 난제 중 하나입니다. 단일 애플리케이션이었다면 프로파일러로 어느 메서드가 느린지 바로 알 수 있었을 것입니다.

하지만 요청이 4개의 서비스를 거쳐가는 상황에서는, 각 서비스의 로그를 일일이 확인해야 했습니다. 더 곤란한 것은, 로그에서 같은 요청을 어떻게 식별할지 방법이 없었다는 것입니다.

박시니어 팀장이 보여준 Zipkin 대시보드에는 놀라운 그림이 펼쳐져 있었습니다. 하나의 요청이 주문 서비스에서 시작해서, 재고 서비스, 결제 서비스, 배송 서비스를 거쳐가는 전체 흐름이 타임라인으로 표시되어 있었습니다.

각 서비스가 몇 밀리초를 소비했는지, 어디서 대기 시간이 길었는지 한눈에 보였습니다. 그렇다면 분산 트레이싱이란 정확히 무엇일까요?

쉽게 비유하자면, 분산 트레이싱은 마치 택배 추적 시스템과 같습니다. 여러분이 택배를 보내면, "물류센터 집하", "간선 수송", "배송 출발", "배송 완료"처럼 각 단계가 기록됩니다.

시간도 함께 기록되어서, 어느 단계에서 지연되었는지 알 수 있습니다. 이처럼 분산 트레이싱도 요청이 각 서비스를 거쳐가는 과정을 기록하고, 각 단계의 소요 시간을 측정합니다.

분산 트레이싱이 없던 시절에는 어땠을까요? 개발자들은 각 서비스의 로그에 요청 ID를 수동으로 추가했습니다.

그리고 모든 서비스가 이 ID를 다음 서비스로 전달하도록 코드를 작성해야 했습니다. 실수로 전달을 빠뜨리면 추적이 끊겼습니다.

또한 각 서비스가 몇 초 걸렸는지 계산하려면 로그의 타임스탬프를 수동으로 비교해야 했습니다. 비효율적이고 오류가 많은 방식이었습니다.

바로 이런 문제를 해결하기 위해 Zipkin분산 트레이싱 표준이 등장했습니다. Zipkin의 핵심 개념은 TraceSpan입니다.

Trace는 하나의 요청 전체를 나타냅니다. 예를 들어 "주문 생성" 요청 하나가 하나의 Trace입니다.

Span은 그 안에서 각 서비스나 작업 단위를 나타냅니다. "재고 확인" Span, "결제 처리" Span, "배송 등록" Span이 하나의 Trace 안에 포함됩니다.

각 Trace에는 고유한 Trace ID가 부여됩니다. 이 ID는 요청이 첫 번째 서비스에 도착할 때 생성되고, 이후 모든 서비스 호출에 HTTP 헤더로 전달됩니다.

Spring Cloud Sleuth가 이 작업을 자동으로 처리해줍니다. 개발자가 따로 코드를 작성할 필요가 없습니다.

위의 코드를 한 줄씩 살펴보겠습니다. 먼저 Sampler Bean을 등록합니다.

ALWAYS_SAMPLE은 모든 요청을 추적한다는 뜻입니다. 운영 환경에서는 트래픽이 많으므로 보통 10% 정도만 샘플링합니다(0.1).

application.yml에서 Zipkin 서버 주소를 설정하면, Sleuth가 자동으로 트레이싱 데이터를 Zipkin으로 전송합니다. createOrder 메서드를 보면 특별한 트레이싱 코드가 없습니다.

하지만 Sleuth가 백그라운드에서 작동하고 있습니다. webClient로 외부 서비스를 호출할 때마다, Sleuth가 자동으로 새로운 Span을 생성하고, HTTP 헤더에 Trace ID와 Span ID를 추가합니다.

호출이 완료되면 소요 시간을 기록하고 Zipkin으로 전송합니다. 실제 현업에서는 어떻게 활용할까요?

예를 들어 "상품 검색이 느리다"는 신고가 들어왔다고 가정해봅시다. Zipkin에서 검색 요청의 Trace를 찾아보니, 추천 알고리즘 서비스에서 5초가 걸렸습니다.

해당 서비스의 개발팀에 전달하면, 그들은 정확히 어떤 요청이 느렸는지 Trace ID로 확인할 수 있습니다. 더 나아가 그 Span을 클릭하면, 로그와 연결되어 정확한 에러 메시지를 볼 수 있습니다.

Zipkin의 타임라인 뷰는 매우 직관적입니다. 가로축이 시간이고, 각 Span이 막대로 표시됩니다.

병렬로 실행된 호출은 세로로 나란히 표시되고, 순차적으로 실행된 호출은 이어져서 표시됩니다. 어느 부분이 병목인지 시각적으로 즉시 파악할 수 있습니다.

하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 운영 환경에서 샘플링 비율을 100%로 설정하는 것입니다.

트래픽이 초당 1000개라면, Zipkin 서버가 감당하기 어렵습니다. 보통 1~10% 정도면 충분합니다.

문제가 생긴 요청을 충분히 캡처할 수 있으면서도, 시스템 부하를 최소화할 수 있습니다. 또 다른 실수는 비동기 호출이나 메시지 큐를 사용할 때 Trace ID를 전파하지 않는 것입니다.

HTTP 호출은 Sleuth가 자동으로 처리하지만, Kafka나 RabbitMQ 같은 메시지 큐를 사용할 때는 명시적으로 Trace ID를 메시지에 포함시켜야 합니다. 그렇지 않으면 추적이 끊깁니다.

Zipkin 데이터는 Elasticsearch에 저장할 수 있습니다. 기본적으로 인메모리에 저장되어 재시작하면 사라지지만, Elasticsearch를 사용하면 장기간 데이터를 보관하고 검색할 수 있습니다.

특정 API의 평균 응답 시간 추이를 분석하거나, 특정 사용자의 요청을 추적할 수 있습니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

Zipkin 덕분에 김개발 씨는 5분 만에 문제를 찾았습니다. 배송 서비스가 외부 택배사 API를 호출하는데 8초가 걸렸던 것입니다.

해당 팀에 전달하자, 그들은 즉시 타임아웃 설정을 수정했습니다. 분산 트레이싱을 제대로 구축하면 장애 조사 시간을 몇 시간에서 몇 분으로 단축할 수 있습니다.

여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - 운영 환경에서는 샘플링 비율을 1~10%로 설정하세요. 100%는 시스템에 부담을 줍니다.

  • Trace ID를 로그에도 포함시키세요. Zipkin에서 느린 요청을 찾은 후, 로그에서 상세 정보를 볼 수 있습니다.
  • 메시지 큐나 비동기 작업에서도 Trace ID를 전파하세요. 그래야 전체 흐름을 추적할 수 있습니다.

4. Prometheus Grafana 구성

김개발 씨는 시스템이 언제 느려지는지 패턴을 찾고 싶었습니다. 로그를 보면 지금 상태만 알 수 있지, 시간에 따른 변화를 볼 수 없었습니다.

박시니어 팀장이 Grafana 대시보드를 열며 말했습니다. "여기 보세요.

지난 한 달간 응답 시간 그래프입니다. 매일 오전 10시에 느려지는 게 보이죠?"

Prometheus는 시계열 데이터베이스로, 메트릭을 수집하고 저장합니다. Grafana는 이 데이터를 시각화하는 대시보드 도구입니다.

마치 주식 차트처럼, 시스템의 CPU, 메모리, 요청 처리 시간 등을 시간에 따라 그래프로 보여줍니다. 이를 통해 시스템의 건강 상태를 실시간으로 모니터링하고, 과거 데이터를 분석해서 패턴을 찾을 수 있습니다.

다음 코드를 살펴봅시다.

// Spring Boot Actuator + Micrometer로 메트릭 노출
@RestController
public class OrderController {

    private final MeterRegistry meterRegistry;
    private final Counter orderCounter;
    private final Timer orderTimer;

    public OrderController(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
        // 주문 건수 카운터
        this.orderCounter = Counter.builder("orders.created")
            .tag("service", "order")
            .description("Total number of orders created")
            .register(meterRegistry);

        // 주문 처리 시간 타이머
        this.orderTimer = Timer.builder("orders.processing.time")
            .tag("service", "order")
            .description("Order processing time")
            .register(meterRegistry);
    }

    @PostMapping("/orders")
    public OrderResponse createOrder(@RequestBody OrderRequest request) {
        return orderTimer.record(() -> {
            // 실제 주문 처리 로직
            OrderResponse response = processOrder(request);
            orderCounter.increment();
            return response;
        });
    }
}

// application.yml
// management:
//   endpoints:
//     web:
//       exposure:
//         include: health,info,prometheus
//   metrics:
//     export:
//       prometheus:
//         enabled: true

김개발 씨가 원했던 것은 추세 분석이었습니다. 지금 시스템이 느린지 빠른지는 로그를 보면 알 수 있습니다.

하지만 "어제보다 느려졌는가?", "지난주 월요일과 비교하면?", "점심시간에 항상 느려지는가?" 같은 질문에는 답할 수 없었습니다. 이런 질문에 답하려면 메트릭을 시간에 따라 저장하고, 그래프로 시각화해야 합니다.

박시니어 팀장이 보여준 Grafana 대시보드에는 놀라운 인사이트가 숨어 있었습니다. 응답 시간 그래프를 보니, 매일 오전 10시와 오후 2시에 피크가 있었습니다.

메모리 사용량은 점진적으로 증가하다가 밤 12시에 뚝 떨어졌습니다(가비지 컬렉션). 에러율은 대부분 0%였지만, 가끔 특정 시간대에 스파이크가 보였습니다.

그렇다면 Prometheus와 Grafana란 정확히 무엇일까요? 쉽게 비유하자면, Prometheus는 마치 기상청의 측정 장비와 같습니다.

온도, 습도, 기압을 계속 측정해서 데이터베이스에 저장합니다. Grafana는 일기예보 화면입니다.

저장된 데이터를 그래프로 보여주고, 패턴을 분석해서 "내일 비가 올 확률 80%"라고 알려줍니다. 이처럼 Prometheus는 시스템 메트릭을 수집하고, Grafana는 이를 시각화하고 분석합니다.

메트릭 수집이 체계적이지 않던 시절에는 어땠을까요? 개발자들은 로그 파일에 "현재 메모리: 500MB"라고 기록했습니다.

그리고 나중에 이 로그를 파싱해서 엑셀로 그래프를 그렸습니다. 매우 비효율적이고, 실시간 모니터링은 불가능했습니다.

시스템이 다운되고 나서야 "아, 메모리가 부족했구나"라고 깨닫곤 했습니다. 사전에 경고를 받을 방법이 없었습니다.

바로 이런 문제를 해결하기 위해 Prometheus와 Grafana가 등장했습니다. Prometheus는 Pull 방식으로 동작합니다.

각 마이크로서비스는 /actuator/prometheus 같은 엔드포인트를 노출하고, Prometheus 서버가 주기적으로(예: 15초마다) 이 엔드포인트를 호출해서 메트릭을 수집합니다. 서비스가 다운되면 Prometheus가 바로 감지합니다(메트릭을 가져올 수 없으므로).

수집된 메트릭은 시계열 데이터베이스에 저장됩니다. 예를 들어 "2025-12-22 10:00:00 - CPU 사용률 45%", "2025-12-22 10:00:15 - CPU 사용률 47%" 이런 식입니다.

이 데이터는 압축되어 효율적으로 저장되며, 보통 15일~30일 정도 보관합니다. 위의 코드를 한 줄씩 살펴보겠습니다.

먼저 MeterRegistry를 주입받습니다. 이것이 Micrometer의 핵심 컴포넌트로, 모든 메트릭을 관리합니다.

Counter.builder로 "orders.created"라는 카운터를 생성합니다. tag("service", "order")를 붙이면, 나중에 서비스별로 필터링할 수 있습니다.

Timer.builder로 처리 시간을 측정하는 타이머를 만듭니다. orderTimer.record()로 주문 처리 로직을 감싸면, 자동으로 시작 시간과 종료 시간을 측정해서 메트릭으로 기록합니다.

orderCounter.increment()로 주문 건수를 1 증가시킵니다. 이 모든 데이터가 /actuator/prometheus 엔드포인트를 통해 Prometheus에 노출됩니다.

실제 현업에서는 어떻게 활용할까요? 예를 들어 쇼핑몰 서비스에서 "장바구니 추가" API의 응답 시간을 모니터링한다고 가정해봅시다.

Grafana 대시보드에 P50(중간값), P95, P99 응답 시간을 표시합니다. 평소에는 P99가 100ms인데, 어느 날 갑자기 500ms로 증가했다면, 무언가 문제가 생긴 것입니다.

즉시 조사할 수 있습니다. Grafana에서는 Alert도 설정할 수 있습니다.

"CPU 사용률이 80%를 5분 이상 유지하면 Slack으로 알림"처럼 규칙을 만들 수 있습니다. 또한 "지난 1시간 대비 에러율이 50% 증가하면 알림" 같은 동적 임계값도 가능합니다.

대시보드에는 여러 패널을 배치할 수 있습니다. 왼쪽 상단에는 전체 요청 처리량(TPS), 오른쪽 상단에는 에러율, 아래쪽에는 각 API별 응답 시간 그래프를 놓을 수 있습니다.

한 화면에서 시스템 전체를 조망할 수 있습니다. 하지만 주의할 점도 있습니다.

초보 개발자들이 흔히 하는 실수 중 하나는 너무 많은 메트릭을 수집하는 것입니다. 모든 변수, 모든 메서드의 실행 시간을 측정하면, Prometheus 데이터베이스가 금방 가득 찹니다.

핵심 지표에 집중하세요. 보통 요청 처리량(TPS), 에러율, 응답 시간(P50, P95, P99), 리소스 사용률(CPU, 메모리) 정도면 충분합니다.

또 다른 실수는 tag(레이블)를 과도하게 사용하는 것입니다. tag를 붙이면 데이터가 그만큼 증가합니다.

예를 들어 userId를 tag로 붙이면, 사용자가 10만 명이면 메트릭이 10만 개로 증가합니다. 대신 service, endpoint, status 같은 카디널리티가 낮은 tag만 사용하세요.

Prometheus의 PromQL이라는 쿼리 언어를 배우면 강력한 분석이 가능합니다. "지난 5분간 에러율이 1% 이상인 API 목록"이나 "CPU 사용률 상위 3개 서비스" 같은 쿼리를 작성할 수 있습니다.

Grafana 변수 기능을 사용하면 드롭다운으로 서비스를 선택해서 대시보드를 동적으로 변경할 수 있습니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

Grafana 대시보드 덕분에 김개발 씨는 시스템의 리듬을 이해하게 되었습니다. 오전 10시에 트래픽이 급증하는 것을 알고, 미리 오토스케일링 임계값을 조정했습니다.

메모리 누수 패턴을 발견하고 수정했습니다. 메트릭 모니터링을 제대로 구축하면 장애를 사전에 예방하고, 발생해도 빠르게 대응할 수 있습니다.

여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - 핵심 메트릭(TPS, 에러율, 응답 시간, 리소스)에 집중하세요. 모든 것을 측정하려 하지 마세요.

  • Tag는 카디널리티가 낮은 것만 사용하세요(service, endpoint, status). userId 같은 건 피하세요.
  • Alert는 실제로 대응할 수 있는 것만 설정하세요. 무시되는 알림은 오히려 해롭습니다.

5. EFK 스택 구성

김개발 씨는 에러 로그를 찾기 위해 5개 서버에 일일이 SSH로 접속해서 grep을 돌리고 있었습니다. 각 서버마다 로그 형식이 달라서, 통합해서 검색하는 것이 불가능했습니다.

박시니어 팀장이 Kibana를 열며 말했습니다. "여기서 검색하면 모든 서비스 로그를 한 번에 볼 수 있어요."

EFK 스택은 Elasticsearch, Fluentd, Kibana의 조합으로, 중앙화된 로그 수집 시스템을 구축합니다. Fluentd가 각 서비스의 로그를 수집하고, Elasticsearch에 저장하며, Kibana로 검색하고 시각화합니다.

마치 도서관처럼, 모든 책(로그)을 한곳에 모아서 검색할 수 있게 만드는 것입니다. 로그를 JSON 형태로 구조화하면, 특정 필드로 필터링하고, 패턴을 분석하고, 알림을 설정할 수 있습니다.

다음 코드를 살펴봅시다.

// Logback 설정으로 JSON 형태 로그 출력
@Configuration
public class LoggingConfig {
    // logback-spring.xml에서 JSON 인코더 사용
}

// logback-spring.xml
// <appender name="JSON" class="ch.qos.logback.core.ConsoleAppender">
//     <encoder class="net.logstash.logback.encoder.LogstashEncoder">
//         <includeMdcKeyName>traceId</includeMdcKeyName>
//         <includeMdcKeyName>spanId</includeMdcKeyName>
//     </encoder>
// </appender>

// 구조화된 로그 출력
@Service
public class OrderService {

    private static final Logger log = LoggerFactory.getLogger(OrderService.class);

    public void createOrder(OrderRequest request) {
        // MDC로 컨텍스트 정보 추가
        MDC.put("userId", request.getUserId());
        MDC.put("orderId", request.getOrderId());

        try {
            log.info("Order creation started");
            // 주문 처리 로직
            processOrder(request);
            log.info("Order creation completed");
        } catch (Exception e) {
            log.error("Order creation failed", e);
            throw e;
        } finally {
            MDC.clear();
        }
    }
}

// Fluentd 설정 (fluent.conf)
// <source>
//   @type tail
//   path /var/log/app/*.log
//   tag app.logs
//   format json
// </source>
//
// <match app.logs>
//   @type elasticsearch
//   host elasticsearch
//   port 9200
//   index_name app-logs
// </match>

김개발 씨가 겪은 고통은 분산 시스템의 전형적인 로그 관리 문제입니다. 주문 서비스, 결제 서비스, 배송 서비스가 각각 다른 서버에서 돌아가고, 각자 로그 파일을 생성합니다.

특정 에러를 추적하려면, 각 서버에 접속해서 로그를 검색해야 합니다. 더 곤란한 것은, 로그 형식이 제각각이라서 자동화하기 어렵다는 것입니다.

박시니어 팀장이 보여준 Kibana 화면은 마치 마법 같았습니다. 검색창에 "orderId: 12345"를 입력하자, 모든 서비스에서 해당 주문과 관련된 로그가 시간순으로 나타났습니다.

주문 생성, 재고 확인, 결제 시도, 에러 발생, 재시도... 전체 스토리가 한눈에 보였습니다.

그렇다면 중앙화된 로깅이란 정확히 무엇일까요? 쉽게 비유하자면, 중앙화된 로깅은 마치 도서관의 통합 검색 시스템과 같습니다.

예전에는 각 서점에 가서 책을 찾아야 했습니다. 하지만 도서관은 모든 책을 한곳에 모아놓고, 저자, 제목, 주제로 검색할 수 있게 만들었습니다.

이처럼 중앙화된 로깅도 모든 서비스의 로그를 한곳에 모아서, 키워드, 시간, 서비스명으로 검색할 수 있게 합니다. 중앙화된 로깅이 없던 시절에는 어땠을까요?

개발자들은 각 서버의 로그를 정기적으로 다운로드해서 로컬 PC에 저장했습니다. 그리고 텍스트 에디터나 grep으로 검색했습니다.

문제는 로그가 로테이션되어 삭제되면, 과거 데이터를 볼 수 없다는 것이었습니다. 또한 여러 서비스의 로그를 조합해서 분석하는 것이 거의 불가능했습니다.

디버깅이 매우 비효율적이었습니다. 바로 이런 문제를 해결하기 위해 EFK 스택이 등장했습니다.

EFK는 세 가지 컴포넌트로 구성됩니다. Fluentd는 로그 수집기입니다.

각 서버에서 돌아가며, 로그 파일을 읽어서 Elasticsearch로 전송합니다. Elasticsearch는 검색 엔진이자 데이터베이스입니다.

로그를 인덱싱해서 빠르게 검색할 수 있게 저장합니다. Kibana는 웹 UI입니다.

사용자가 로그를 검색하고, 필터링하고, 시각화하는 인터페이스를 제공합니다. 로그를 구조화하는 것이 핵심입니다.

예전 방식은 "2025-12-22 10:30:00 [INFO] 주문 생성 성공" 같은 평문이었습니다. 이것을 JSON으로 바꾸면 {"timestamp": "2025-12-22T10:30:00", "level": "INFO", "message": "주문 생성 성공", "service": "order", "userId": "12345"} 처럼 각 필드가 분리됩니다.

이렇게 하면 Elasticsearch에서 userId로 필터링하거나, service별로 그룹핑할 수 있습니다. 위의 코드를 한 줄씩 살펴보겠습니다.

먼저 logback-spring.xml에서 LogstashEncoder를 사용합니다. 이것이 로그를 JSON 형태로 출력합니다.

includeMdcKeyName으로 traceId와 spanId를 포함시키면, 나중에 Zipkin과 연동해서 특정 Trace의 로그를 바로 찾을 수 있습니다. OrderService에서는 MDC(Mapped Diagnostic Context)를 사용합니다.

MDC.put으로 userId와 orderId를 컨텍스트에 추가하면, 이 메서드 안의 모든 로그에 자동으로 포함됩니다. try-finally로 MDC.clear()를 호출해서, 다음 요청에 영향을 주지 않도록 합니다.

Fluentd 설정 파일에서는 tail 플러그인으로 로그 파일을 실시간으로 읽습니다. format json으로 설정하면, JSON 로그를 파싱해서 각 필드를 추출합니다.

match 블록에서 Elasticsearch로 전송하도록 설정합니다. 실제 현업에서는 어떻게 활용할까요?

예를 들어 "결제 실패가 갑자기 증가했다"는 알림을 받았다고 가정해봅시다. Kibana에서 level: ERROR AND service: payment로 검색하면, 모든 결제 에러 로그가 나타납니다.

에러 메시지를 보니 "Connection timeout to bank API"가 대부분입니다. 즉시 원인을 파악할 수 있습니다.

Kibana에서는 Discover 탭으로 로그를 검색하고, Visualize 탭으로 그래프를 만들 수 있습니다. 예를 들어 "시간별 에러 발생 건수" 그래프를 만들어서, 특정 시간대에 에러가 집중되는지 확인할 수 있습니다.

Dashboard 탭에서는 여러 그래프를 조합해서 한 화면에 표시합니다. 또한 Watcher로 알림을 설정할 수 있습니다.

"최근 5분간 ERROR 로그가 100개 이상이면 Slack으로 알림" 같은 규칙을 만들 수 있습니다. 이렇게 하면 장애를 빠르게 감지할 수 있습니다.

하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 민감한 정보를 로그에 남기는 것입니다.

비밀번호, 신용카드 번호, 개인정보 같은 것을 로그에 기록하면 안 됩니다. Elasticsearch에 저장되어 누구나 검색할 수 있게 되기 때문입니다.

로깅 라이브러리에 마스킹 기능을 추가하거나, 민감한 필드는 아예 로깅하지 않아야 합니다. 또 다른 실수는 로그를 너무 많이 남기는 것입니다.

DEBUG 레벨로 모든 변수를 로깅하면, Elasticsearch가 금방 가득 차고 비용이 급증합니다. 운영 환경에서는 INFO 레벨 이상만 로깅하고, 필요할 때만 일시적으로 DEBUG를 켜는 것이 좋습니다.

Elasticsearch의 인덱스 관리도 중요합니다. 보통 하루 단위로 인덱스를 생성합니다(app-logs-2025-12-22).

오래된 인덱스는 자동으로 삭제하거나 아카이브해서, 디스크 공간을 절약합니다. 보통 7~30일 정도 보관합니다.

다시 김개발 씨의 이야기로 돌아가 봅시다. EFK 스택을 도입한 후, 김개발 씨는 더 이상 서버에 SSH로 접속할 필요가 없어졌습니다.

Kibana 하나로 모든 로그를 검색하고, 에러 패턴을 분석하고, 알림을 받을 수 있게 되었습니다. 중앙화된 로깅을 제대로 구축하면 디버깅 시간을 대폭 단축하고, 문제 패턴을 빠르게 발견할 수 있습니다.

여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - 로그는 반드시 JSON 형태로 구조화하세요. 검색과 필터링이 100배 쉬워집니다.

  • 민감한 정보는 절대 로그에 남기지 마세요. 마스킹하거나 제외하세요.
  • MDC에 traceId를 포함시켜서 Zipkin과 연동하세요. 트레이스에서 바로 로그로 점프할 수 있습니다.

6. 통합 대시보드

김개발 씨는 이제 Zipkin, Grafana, Kibana를 각각 사용할 수 있게 되었습니다. 하지만 장애가 발생하면 세 개의 툴을 왔다 갔다 해야 해서 불편했습니다.

박시니어 팀장이 말했습니다. "이 세 가지를 하나로 통합한 대시보드를 만들어봅시다.

한 화면에서 모든 것을 볼 수 있게요."

통합 대시보드는 메트릭, 트레이스, 로그를 한 화면에서 볼 수 있게 만드는 것입니다. 마치 자동차 계기판처럼, 속도계(메트릭), 내비게이션(트레이스), 경고등(로그)이 모두 운전석 앞에 있는 것입니다.

Grafana에서 Zipkin과 Elasticsearch를 데이터 소스로 추가하면, 하나의 대시보드에 모든 정보를 표시할 수 있습니다. 특정 API가 느려지면, 같은 화면에서 메트릭을 보고, 트레이스를 확인하고, 에러 로그를 검색할 수 있습니다.

다음 코드를 살펴봅시다.

// Grafana 데이터 소스 설정 (datasources.yml)
// apiVersion: 1
// datasources:
//   - name: Prometheus
//     type: prometheus
//     url: http://prometheus:9090
//
//   - name: Zipkin
//     type: zipkin
//     url: http://zipkin:9411
//
//   - name: Elasticsearch
//     type: elasticsearch
//     url: http://elasticsearch:9200
//     database: app-logs-*

// 통합 대시보드를 위한 어노테이션
@Service
public class IntegratedObservabilityService {

    private final MeterRegistry meterRegistry;
    private final Tracer tracer;
    private final Logger log;

    @Timed(value = "user.service.get", percentiles = {0.5, 0.95, 0.99})
    public User getUser(String userId) {
        // Span 수동 생성
        Span span = tracer.nextSpan().name("get-user").start();
        try (Tracer.SpanInScope ws = tracer.withSpan(span)) {

            // MDC에 추적 정보 추가
            MDC.put("traceId", span.context().traceId());
            MDC.put("spanId", span.context().spanId());
            MDC.put("userId", userId);

            log.info("Fetching user data");

            // 비즈니스 로직
            User user = userRepository.findById(userId);

            // 커스텀 메트릭
            meterRegistry.counter("user.fetched",
                "status", user != null ? "success" : "not_found"
            ).increment();

            return user;

        } catch (Exception e) {
            log.error("Failed to fetch user", e);
            span.tag("error", "true");
            throw e;
        } finally {
            span.end();
            MDC.clear();
        }
    }
}

김개발 씨가 겪은 불편함은 컨텍스트 스위칭의 문제입니다. "주문 API가 느려졌다"는 알림을 받으면, 먼저 Grafana에서 응답 시간 그래프를 봅니다.

그다음 Zipkin에서 느린 Trace를 찾습니다. 마지막으로 Kibana에서 에러 로그를 검색합니다.

세 개의 탭을 왔다 갔다 하며 정보를 조합해야 했습니다. 비효율적이고, 중요한 상관관계를 놓치기 쉬웠습니다.

박시니어 팀장이 제안한 통합 대시보드는 이 문제를 해결하는 열쇠였습니다. Grafana에서 Prometheus뿐만 아니라 Zipkin과 Elasticsearch도 데이터 소스로 추가할 수 있습니다.

이렇게 하면 하나의 대시보드에 메트릭 그래프, 트레이스 링크, 로그 테이블을 모두 표시할 수 있습니다. 그렇다면 통합 관찰 가능성이란 정확히 무엇일까요?

쉽게 비유하자면, 통합 관찰 가능성은 마치 병원의 종합 모니터링 시스템과 같습니다. 중환자실에서는 심박수, 혈압, 산소포화도가 하나의 모니터에 표시됩니다.

의사가 여러 기계를 돌아다니며 확인할 필요가 없습니다. 이상이 생기면 즉시 알람이 울리고, 모든 정보가 한눈에 보입니다.

이처럼 통합 대시보드도 시스템의 모든 생체신호를 한 화면에 표시합니다. 통합되지 않은 도구들을 사용하던 시절에는 어땠을까요?

개발자들은 여러 개의 모니터링 툴을 사용했습니다. 각 툴은 훌륭했지만, 연결되어 있지 않았습니다.

메트릭에서 이상을 발견하면, 수동으로 시간대를 확인하고, 트레이싱 툴에서 같은 시간대의 Trace를 찾아야 했습니다. 또 수동으로 Trace ID를 복사해서 로그 검색창에 붙여넣어야 했습니다.

실수하기 쉽고, 시간이 많이 걸렸습니다. 바로 이런 문제를 해결하기 위해 통합 대시보드상관관계 분석이 등장했습니다.

통합 대시보드의 핵심은 연결입니다. Grafana 패널에서 응답 시간 그래프를 클릭하면, 해당 시간대의 Zipkin Trace 목록이 나타납니다.

Trace를 클릭하면, 그 Trace ID로 Elasticsearch 로그를 자동으로 검색합니다. 이 모든 과정이 클릭 몇 번으로 이루어집니다.

이를 위해서는 일관된 식별자가 필수입니다. 바로 Trace ID입니다.

메트릭에 Trace ID를 태그로 추가하고, 로그에도 Trace ID를 포함시키면, 세 가지 데이터를 연결할 수 있습니다. 이것이 바로 Observability Correlation입니다.

위의 코드를 한 줄씩 살펴보겠습니다. 먼저 @Timed 어노테이션으로 메서드 실행 시간을 자동으로 측정합니다.

percentiles 옵션으로 P50, P95, P99 값을 수집합니다. 다음으로 tracer.nextSpan()으로 수동 Span을 생성합니다.

이렇게 하면 더 세밀한 트레이싱이 가능합니다. MDC에 traceId와 spanId를 추가하는 것이 핵심입니다.

이렇게 하면 이 메서드 안의 모든 로그에 자동으로 Trace 정보가 포함됩니다. 나중에 Kibana에서 traceId로 검색하면, 관련된 모든 로그를 찾을 수 있습니다.

meterRegistry.counter()로 커스텀 메트릭을 추가합니다. "user.fetched"라는 카운터에 status 태그를 붙여서, 성공/실패를 구분합니다.

에러가 발생하면 span.tag("error", "true")로 표시해서, Zipkin에서 에러 Trace를 필터링할 수 있게 합니다. 실제 현업에서는 어떻게 활용할까요?

예를 들어 "상품 검색 API"의 P95 응답 시간이 갑자기 증가했다고 가정해봅시다. Grafana 대시보드에서 그래프의 스파이크 부분을 클릭하면, Variables로 시간 범위가 설정됩니다.

바로 아래 패널에는 그 시간대의 느린 Trace 목록이 나타납니다. 가장 느린 Trace를 클릭하면 Zipkin이 열리고, Span 중 "elasticsearch-query"가 5초 걸렸다는 것을 발견합니다.

이제 그 Span의 Trace ID를 클릭하면, Kibana 로그가 필터링되어 나타납니다. 로그를 보니 "Query timeout: SELECT * FROM products WHERE ..."라는 에러 메시지가 있습니다.

즉시 원인을 파악했습니다. 인덱스가 없는 필드로 검색한 것입니다.

Grafana의 Variables 기능을 사용하면 동적 대시보드를 만들 수 있습니다. 상단에 드롭다운으로 서비스명을 선택하면, 모든 패널이 해당 서비스의 데이터로 업데이트됩니다.

또한 Annotations로 배포 시점을 표시하면, "배포 후 에러가 증가했는지" 바로 알 수 있습니다. 하지만 주의할 점도 있습니다.

초보 개발자들이 흔히 하는 실수 중 하나는 너무 많은 정보를 한 대시보드에 넣는 것입니다. 30개의 그래프가 있으면 오히려 중요한 것을 놓칩니다.

핵심 지표만 상단에 크게 표시하고, 상세 정보는 드릴다운으로 접근하게 만드세요. 또 다른 실수는 대시보드를 정적으로 만드는 것입니다.

"주문 서비스 대시보드", "결제 서비스 대시보드"를 각각 만들면, 서비스가 100개가 되면 대시보드도 100개가 됩니다. 대신 템플릿 변수를 사용해서 하나의 대시보드로 모든 서비스를 커버하세요.

알림 피로도 관리합니다. 알림이 너무 많으면 무시하게 됩니다.

실행 가능한 알림만 설정하세요. "CPU 80% 이상"이라는 알림보다는 "P95 응답 시간이 SLA를 10% 초과하고 5분 이상 지속"처럼 구체적이고 의미 있는 조건을 설정하세요.

대시보드에는 컨텍스트를 추가하세요. 단순히 숫자만 보여주는 게 아니라, "목표: 100ms, 현재: 150ms" 처럼 목표치를 함께 표시하면, 상황을 빠르게 판단할 수 있습니다.

또한 색상 코딩을 사용해서, 녹색(정상), 노란색(주의), 빨간색(위험)으로 시각적 피드백을 줍니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

통합 대시보드를 구축한 후, 김개발 씨는 장애 대응 시간을 평균 30분에서 5분으로 단축했습니다. 한 화면에서 문제를 발견하고, 원인을 파악하고, 해결책을 찾을 수 있게 되었습니다.

관찰 가능한 마이크로서비스를 제대로 구축하면, 시스템을 투명하게 볼 수 있습니다. 여러분도 오늘 배운 내용을 바탕으로, 메트릭, 트레이스, 로그를 통합한 관찰 가능성 플랫폼을 구축해 보세요.

그러면 여러분의 시스템은 더 이상 블랙박스가 아니라, 완전히 투명한 유리상자가 될 것입니다.

실전 팁

💡 - Trace ID를 메트릭, 로그, 트레이스에 모두 포함시켜서 상관관계를 만드세요.

  • 대시보드는 핵심 지표 위주로 심플하게 만드세요. 너무 많은 정보는 오히려 해롭습니다.
  • 템플릿 변수로 동적 대시보드를 만들어서, 하나의 대시보드로 모든 서비스를 커버하세요.

이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!

#Java#Resilience4j#Zipkin#Prometheus#Microservices#마이크로서비스

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.