🤖

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

⚠️

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

이미지 로딩 중...

스프링 관찰 가능성 완벽 가이드 - 슬라이드 1/7
A

AI Generated

2025. 12. 22. · 3 Views

스프링 관찰 가능성 완벽 가이드

Spring Boot 3.x의 Observation API를 활용한 애플리케이션 모니터링과 추적 방법을 초급 개발자 눈높이에서 쉽게 설명합니다. 실무에서 바로 적용할 수 있는 메트릭 수집과 분산 추적 기법을 다룹니다.


목차

  1. Observation API 소개
  2. @Observed 어노테이션
  3. ObservationHandler
  4. 메트릭과 추적 통합
  5. 커스텀 관찰
  6. Convention 설정

1. Observation API 소개

어느 날 김개발 씨는 운영팀으로부터 긴급 전화를 받았습니다. "API 응답이 너무 느린데, 어디가 문제인지 모르겠어요!" 로그를 아무리 뒤져봐도 병목 지점을 찾기가 쉽지 않았습니다.

Observation API는 Spring Boot 3.0부터 도입된 관찰 가능성 표준 인터페이스입니다. 마치 자동차의 계기판처럼, 애플리케이션의 상태를 실시간으로 관찰하고 측정할 수 있게 해줍니다.

메트릭 수집, 분산 추적, 로깅을 하나의 통합된 API로 처리할 수 있습니다.

다음 코드를 살펴봅시다.

// build.gradle에 의존성 추가
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'io.micrometer:micrometer-tracing-bridge-brave'

@Service
public class OrderService {
    private final ObservationRegistry registry;

    public OrderService(ObservationRegistry registry) {
        this.registry = registry;
    }

    public Order createOrder(OrderRequest request) {
        // Observation을 생성하고 시작합니다
        return Observation.createNotStarted("order.create", registry)
            .observe(() -> processOrder(request));
    }
}

김개발 씨는 입사 6개월 차 백엔드 개발자입니다. 최근 회사에서 마이크로서비스 아키텍처를 도입하면서, 시스템이 점점 복잡해지고 있었습니다.

그러던 어느 날, 운영팀으로부터 다급한 전화가 걸려왔습니다. "김개발 씨, 주문 처리가 너무 느려요.

고객 불만이 쏟아지고 있는데 어디가 문제인지 알 수 있나요?" 김개발 씨는 당황했습니다. 로그는 잔뜩 쌓여 있지만, 어느 부분에서 시간이 오래 걸리는지 명확하게 파악하기 어려웠습니다.

데이터베이스 쿼리가 느린 건지, 외부 API 호출이 문제인지, 아니면 다른 곳인지 알 수가 없었습니다. 선배 개발자 박시니어 씨가 다가와 화면을 보더니 말했습니다.

"아, 관찰 가능성이 부족하네요. Observation API를 사용해보면 어떨까요?" 관찰 가능성이란 정확히 무엇일까요?

쉽게 비유하자면, 관찰 가능성은 마치 자동차의 계기판과 같습니다. 운전 중에 속도계를 보면 현재 속도를 알 수 있고, 연료 게이지를 보면 남은 연료를 확인할 수 있습니다.

엔진 경고등이 켜지면 문제가 있다는 것을 즉시 알 수 있죠. 애플리케이션도 마찬가지입니다.

시스템의 내부 상태를 외부에서 관찰할 수 있어야 문제를 빠르게 찾아낼 수 있습니다. 전통적인 모니터링의 문제점 Observation API가 등장하기 전에는 어땠을까요?

개발자들은 메트릭을 수집하기 위해 Micrometer를 사용하고, 분산 추적을 위해 별도로 Sleuth를 설정하고, 로깅은 또 따로 관리해야 했습니다. 각각의 라이브러리가 서로 다른 방식으로 동작했고, 코드 곳곳에 중복된 관찰 로직이 흩어져 있었습니다.

새로운 기능을 추가할 때마다 메트릭도 따로, 추적도 따로 설정해야 했습니다. 더 큰 문제는 일관성이 없다는 점이었습니다.

A팀은 이런 방식으로 모니터링하고, B팀은 저런 방식으로 모니터링하다 보니, 전체 시스템을 통합해서 보기가 어려웠습니다. Observation API의 등장 바로 이런 문제를 해결하기 위해 Spring Boot 3.0에서 Observation API가 등장했습니다.

Observation API를 사용하면 하나의 통합된 방식으로 관찰 가능성을 구현할 수 있습니다. 메트릭이 필요하면 메트릭만 수집하고, 추적이 필요하면 추적만 활성화하면 됩니다.

코드는 변경하지 않아도 됩니다. 무엇보다 관심사의 분리라는 큰 이점이 있습니다.

비즈니스 로직과 관찰 로직이 깔끔하게 분리됩니다. 코드 동작 원리 위의 코드를 한 줄씩 살펴보겠습니다.

먼저 ObservationRegistry를 주입받습니다. 이것은 모든 관찰 활동을 조정하는 중앙 레지스트리입니다.

Spring Boot가 자동으로 설정해주므로 별도의 빈 등록이 필요 없습니다. Observation.createNotStarted("order.create", registry)에서 새로운 관찰을 생성합니다.

"order.create"는 이 관찰의 이름입니다. 나중에 메트릭이나 추적 데이터를 볼 때 이 이름으로 필터링할 수 있습니다.

마지막으로 observe() 메서드에 실제 비즈니스 로직을 람다로 전달합니다. 이 메서드는 자동으로 시작 시간을 기록하고, 실행이 끝나면 종료 시간을 기록하며, 중간에 예외가 발생하면 에러도 함께 기록합니다.

실무에서의 활용 실제 현업에서는 어떻게 활용할까요? 예를 들어 전자상거래 플랫폼을 운영한다고 가정해봅시다.

주문 생성 프로세스는 여러 단계를 거칩니다. 재고 확인, 결제 처리, 배송 정보 저장 등이 순차적으로 일어납니다.

Observation API를 각 단계에 적용하면 어느 단계에서 시간이 오래 걸리는지 한눈에 파악할 수 있습니다. Netflix, Uber 같은 대규모 서비스들도 유사한 관찰 가능성 패턴을 적극적으로 사용하고 있습니다.

주의할 점 하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 모든 메서드에 Observation을 추가하는 것입니다.

이렇게 하면 오버헤드가 발생하고 메트릭 데이터가 너무 많아져서 오히려 분석이 어려워집니다. 따라서 중요한 비즈니스 로직이나 외부 호출이 있는 부분에만 선택적으로 적용해야 합니다.

정리하며 다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨의 조언을 듣고 Observation API를 적용한 김개발 씨는 문제를 빠르게 찾아낼 수 있었습니다.

"아, 외부 결제 API 호출에서 평균 3초가 걸리고 있었네요!" Observation API를 제대로 이해하면 시스템의 건강 상태를 실시간으로 파악하고, 문제가 발생했을 때 빠르게 대응할 수 있습니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - Observation은 성능에 민감한 핫스팟에만 선택적으로 적용하세요

  • 의미 있는 이름을 사용하여 나중에 메트릭을 쉽게 찾을 수 있게 하세요
  • Spring Boot Actuator와 함께 사용하면 즉시 모니터링 엔드포인트를 활용할 수 있습니다

2. @Observed 어노테이션

"매번 Observation.createNotStarted()를 호출하는 게 번거롭네요." 김개발 씨가 코드를 작성하다가 중얼거렸습니다. 박시니어 씨가 웃으며 답했습니다.

"그럴 땐 어노테이션을 쓰면 돼요."

@Observed 어노테이션은 메서드 레벨에서 선언적으로 관찰을 적용하는 방법입니다. 마치 @Transactional이 트랜잭션 처리를 자동화하듯이, @Observed는 관찰 로직을 자동으로 추가해줍니다.

Spring AOP를 활용하여 메서드 실행 전후로 관찰 코드가 자동 삽입됩니다.

다음 코드를 살펴봅시다.

@Configuration
@EnableObservedAspect
public class ObservationConfig {
    // @Observed를 활성화하는 설정
}

@Service
public class PaymentService {

    @Observed(name = "payment.process",
              contextualName = "processing-payment",
              lowCardinalityKeyValues = {"type", "credit-card"})
    public PaymentResult processPayment(PaymentRequest request) {
        // 비즈니스 로직만 집중
        return executePayment(request);
    }

    @Observed(name = "payment.refund")
    public void refundPayment(String orderId) {
        // 환불 처리 로직
    }
}

김개발 씨는 지난주에 배운 Observation API를 열심히 프로젝트에 적용하고 있었습니다. 하지만 점점 코드가 늘어나면서 불편한 점을 발견했습니다.

모든 메서드마다 Observation.createNotStarted()를 호출하고, observe() 안에 로직을 감싸야 했습니다. 메서드가 10개, 20개로 늘어나니 보일러플레이트 코드가 반복되었습니다.

"더 간단한 방법은 없을까?" 바로 그때 박시니어 씨가 코드 리뷰를 하다가 말했습니다. "음, 코드가 좀 장황하네요.

@Observed 어노테이션을 사용해보는 게 어때요?" 선언적 프로그래밍의 힘 @Observed가 뭘까요? 쉽게 비유하자면, @Observed는 마치 식당에서 주문하는 것과 같습니다.

손님은 "스테이크 하나 주세요"라고만 말하면 됩니다. 주방에서 고기를 손질하고, 굽고, 플레이팅하는 복잡한 과정은 알 필요가 없죠.

@Observed도 마찬가지입니다. 개발자는 "이 메서드를 관찰해주세요"라고 선언만 하면, Spring이 알아서 관찰 로직을 처리해줍니다.

기존 방식의 불편함 @Observed가 없던 시절을 돌이켜봅시다. 모든 메서드에서 직접 Observation을 생성하고, try-finally 블록으로 시작과 종료를 관리해야 했습니다.

코드가 깊게 중첩되고, 비즈니스 로직과 관찰 로직이 뒤섞였습니다. 실수로 start()를 호출하지 않거나 stop()을 빠뜨리는 경우도 종종 발생했습니다.

더 큰 문제는 일관성을 유지하기 어렵다는 점이었습니다. 개발자마다 다른 스타일로 Observation을 작성하다 보니, 코드 리뷰 때마다 스타일 논쟁이 벌어졌습니다.

@Observed로 해결 바로 이런 문제를 해결하기 위해 @Observed 어노테이션이 제공됩니다. @Observed를 사용하면 메서드 선언부에 어노테이션만 붙이면 됩니다.

Spring AOP가 런타임에 프록시를 생성하여 자동으로 관찰 로직을 삽입합니다. 비즈니스 로직은 깔끔하게 유지되고, 관찰 설정은 어노테이션으로 분리됩니다.

무엇보다 일관성과 유지보수성이 크게 향상됩니다. 코드 세부 분석 위의 코드를 자세히 살펴보겠습니다.

먼저 @EnableObservedAspect를 설정 클래스에 추가해야 합니다. 이것이 없으면 @Observed 어노테이션이 동작하지 않습니다.

Spring이 AOP 프록시를 생성하는 데 필요한 설정입니다. @Observed(name = "payment.process")에서 관찰의 이름을 지정합니다.

이 이름은 메트릭과 추적 데이터에서 식별자로 사용됩니다. contextualName은 좀 더 사람이 읽기 쉬운 이름을 제공합니다.

"processing-payment"처럼 대시로 구분된 형태를 권장합니다. lowCardinalityKeyValues는 태그를 추가하는 부분입니다.

키-값 쌍으로 메타데이터를 제공하면, 나중에 메트릭을 필터링하거나 그룹핑할 때 유용합니다. "low cardinality"는 고유값의 개수가 적다는 의미입니다.

예를 들어 결제 타입은 "credit-card", "bank-transfer" 정도로 제한되므로 low cardinality입니다. 실무 적용 시나리오 실제 프로젝트에서는 어떻게 활용할까요?

쇼핑몰의 주문 서비스를 생각해봅시다. 주문 생성, 주문 조회, 주문 취소 같은 핵심 메서드들이 있습니다.

각 메서드에 @Observed를 붙이면, 자동으로 실행 시간, 호출 횟수, 성공/실패 여부가 기록됩니다. 나중에 Grafana 같은 대시보드에서 "어떤 API가 가장 느린가?", "어느 시간대에 주문이 몰리는가?" 같은 질문에 즉시 답할 수 있습니다.

많은 스타트업들이 초기에는 로깅만으로 시작했다가, 서비스가 성장하면서 @Observed 패턴을 도입하여 체계적인 모니터링 체계를 갖춥니다. 흔한 실수와 주의사항 하지만 조심할 점도 있습니다.

초보 개발자들이 자주 하는 실수는 @EnableObservedAspect를 빠뜨리는 것입니다. 어노테이션만 붙이고 설정을 안 하면 아무 일도 일어나지 않습니다.

또한 private 메서드에는 @Observed가 동작하지 않습니다. Spring AOP는 프록시 기반이므로 public 메서드에만 적용됩니다.

또 하나 주의할 점은 highCardinalityKeyValues를 남발하면 안 된다는 것입니다. 사용자 ID처럼 고유값이 수백만 개인 데이터를 태그로 넣으면 메트릭 저장소가 폭발합니다.

따라서 카디널리티가 낮은 값만 태그로 사용해야 합니다. 마무리 김개발 씨는 @Observed를 적용한 후 코드가 훨씬 깔끔해진 것을 느꼈습니다.

"이제 비즈니스 로직에만 집중할 수 있겠어요!" @Observed 어노테이션을 제대로 활용하면 코드의 가독성을 유지하면서도 강력한 관찰 가능성을 확보할 수 있습니다. 여러분도 반복적인 Observation 코드에서 벗어나 선언적 스타일을 시도해보세요.

실전 팁

💡 - @EnableObservedAspect를 설정하는 것을 잊지 마세요

  • public 메서드에만 적용되므로 접근 제어자를 확인하세요
  • lowCardinalityKeyValues만 사용하여 메트릭 폭발을 방지하세요

3. ObservationHandler

"Observation이 어떻게 메트릭으로 변환되는 거죠?" 김개발 씨가 궁금해하며 물었습니다. 박시니어 씨는 화이트보드에 그림을 그리며 설명하기 시작했습니다.

"ObservationHandler가 바로 그 역할을 하죠."

ObservationHandler는 관찰 이벤트를 실제 메트릭, 추적, 로그로 변환하는 핸들러입니다. 마치 번역가처럼 Observation의 추상적인 데이터를 구체적인 모니터링 시스템이 이해할 수 있는 형태로 바꿔줍니다.

여러 핸들러를 등록하여 동시에 여러 백엔드로 데이터를 전송할 수 있습니다.

다음 코드를 살펴봅시다.

// 커스텀 ObservationHandler 구현
public class CustomLoggingHandler implements ObservationHandler<Observation.Context> {
    private static final Logger log = LoggerFactory.getLogger(CustomLoggingHandler.class);

    @Override
    public boolean supportsContext(Observation.Context context) {
        // 모든 컨텍스트를 지원
        return true;
    }

    @Override
    public void onStart(Observation.Context context) {
        log.info("Observation started: {}", context.getName());
    }

    @Override
    public void onStop(Observation.Context context) {
        log.info("Observation stopped: {} (duration: {}ms)",
                 context.getName(),
                 Duration.between(context.getStartTime(), context.getStopTime()).toMillis());
    }
}

// Handler 등록
@Configuration
public class ObservationHandlerConfig {

    @Bean
    public ObservationRegistry observationRegistry() {
        ObservationRegistry registry = ObservationRegistry.create();
        registry.observationConfig()
                .observationHandler(new CustomLoggingHandler())
                .observationHandler(new DefaultMeterObservationHandler(meterRegistry))
                .observationHandler(new DefaultTracingObservationHandler(tracer));
        return registry;
    }
}

김개발 씨는 @Observed를 적용하고 나서 한 가지 의문이 들었습니다. "어노테이션만 붙였는데 어떻게 Prometheus에 메트릭이 나타나는 거지?" 코드 어디를 봐도 Prometheus에 데이터를 보내는 로직은 보이지 않았습니다.

마법처럼 자동으로 처리되는 것 같았습니다. 궁금증을 참지 못한 김개발 씨는 박시니어 씨에게 물어봤습니다.

박시니어 씨는 화이트보드를 가져와 그림을 그리기 시작했습니다. "Observation API는 관찰만 할 뿐이에요.

실제로 데이터를 처리하는 건 ObservationHandler의 몫이죠." 옵저버 패턴의 실제 구현 ObservationHandler는 정확히 무엇일까요? 쉽게 비유하자면, ObservationHandler는 마치 신문사의 기자와 같습니다.

어떤 사건이 발생하면 여러 기자들이 각자의 관점에서 기사를 작성합니다. 어떤 기자는 경제면에 쓰고, 어떤 기자는 사회면에 씁니다.

같은 사건이지만 다양한 형태로 기록됩니다. ObservationHandler도 마찬가지입니다.

하나의 관찰 이벤트가 발생하면, 등록된 여러 핸들러가 각자의 방식으로 처리합니다. 전통적인 모니터링의 한계 ObservationHandler가 없다면 어떻게 될까요?

개발자는 메트릭을 보내기 위해 직접 MeterRegistry를 호출하고, 추적을 위해 Tracer를 호출하고, 로그를 위해 Logger를 호출해야 합니다. 하나의 비즈니스 로직에 세 가지 관찰 코드가 뒤섞입니다.

코드가 복잡해지고, 새로운 모니터링 도구를 추가할 때마다 모든 코드를 수정해야 합니다. 더 큰 문제는 일관성이 깨진다는 점입니다.

어떤 곳에서는 메트릭만 보내고, 어떤 곳에서는 추적만 하다 보니, 전체 시스템을 통합해서 분석하기 어려워집니다. 핸들러 체인의 힘 바로 이런 문제를 해결하기 위해 ObservationHandler가 설계되었습니다.

ObservationHandler를 사용하면 관찰 로직과 처리 로직이 완전히 분리됩니다. 비즈니스 코드는 Observation만 생성하면 되고, 나머지는 핸들러가 알아서 처리합니다.

새로운 백엔드를 추가하고 싶으면 핸들러만 하나 더 등록하면 됩니다. 무엇보다 확장성과 유연성이 뛰어납니다.

코드 동작 원리 위의 코드를 단계별로 분석해봅시다. 먼저 ObservationHandler 인터페이스를 구현합니다.

supportsContext() 메서드는 이 핸들러가 어떤 타입의 컨텍스트를 처리할지 결정합니다. true를 반환하면 모든 관찰을 처리하고, 특정 조건을 넣으면 선택적으로 처리할 수 있습니다.

onStart() 메서드는 관찰이 시작될 때 호출됩니다. 여기서 시작 로그를 남기거나, 타이머를 초기화하는 등의 작업을 수행합니다.

onStop() 메서드는 관찰이 끝날 때 호출됩니다. 이때 실제 메트릭을 전송하거나, 추적 스팬을 종료하거나, 로그를 기록합니다.

코드에서는 시작 시간과 종료 시간의 차이를 계산하여 실행 시간을 로그로 남깁니다. 설정 클래스에서 observationConfig()를 통해 여러 핸들러를 체인으로 등록합니다.

관찰 이벤트가 발생하면 등록된 순서대로 모든 핸들러가 실행됩니다. 실무 활용 사례 실제 프로젝트에서는 어떻게 활용할까요?

대규모 전자상거래 플랫폼을 운영한다고 가정해봅시다. 메트릭은 Prometheus로 보내서 Grafana 대시보드에 표시하고, 분산 추적은 Zipkin으로 보내서 요청 흐름을 시각화하고, 중요한 이벤트는 Elasticsearch에 로그로 남겨서 나중에 분석합니다.

이 모든 것을 하나의 Observation으로 처리할 수 있습니다. Netflix는 이런 방식으로 수천 개의 마이크로서비스를 모니터링합니다.

각 서비스는 Observation만 생성하고, 중앙 플랫폼팀이 핸들러를 관리합니다. 주의사항과 팁 하지만 조심할 점이 있습니다.

초보 개발자들이 흔히 하는 실수는 핸들러 내부에서 예외를 던지는 것입니다. 핸들러에서 예외가 발생하면 비즈니스 로직도 실패합니다.

따라서 핸들러 내부에서는 반드시 try-catch로 예외를 잡아야 합니다. 또한 핸들러가 너무 무거운 작업을 하면 성능 문제가 발생합니다.

네트워크 호출이나 데이터베이스 쓰기 같은 작업은 비동기로 처리하거나 버퍼링하는 것이 좋습니다. 정리 김개발 씨는 ObservationHandler의 구조를 이해한 후 감탄했습니다.

"와, 이렇게 깔끔하게 분리되어 있었군요!" ObservationHandler를 잘 이해하면 모니터링 시스템을 유연하게 확장하고, 비즈니스 로직과 관찰 로직을 완벽하게 분리할 수 있습니다. 여러분도 커스텀 핸들러를 만들어서 팀의 특별한 요구사항을 충족시켜보세요.

실전 팁

💡 - 핸들러 내부에서는 항상 예외 처리를 해야 비즈니스 로직에 영향을 주지 않습니다

  • 무거운 작업은 비동기로 처리하여 성능 저하를 방지하세요
  • supportsContext()를 활용하여 특정 타입의 관찰만 선택적으로 처리할 수 있습니다

4. 메트릭과 추적 통합

"메트릭과 추적이 어떻게 다른 건가요?" 신입 개발자 이주니어 씨가 질문했습니다. 김개발 씨는 이제 어느 정도 자신감이 생겨서 답했습니다.

"메트릭은 숫자고, 추적은 흐름이에요. 근데 둘 다 중요하죠."

메트릭은 시스템의 수치적 상태를 측정하고, 추적은 요청의 흐름을 시간순으로 기록합니다. Observation API는 이 둘을 하나의 코드로 통합하여 동시에 수집할 수 있게 해줍니다.

메트릭으로는 "얼마나 느린가"를 알고, 추적으로는 "왜 느린가"를 알 수 있습니다.

다음 코드를 살펴봅시다.

// build.gradle 의존성
implementation 'io.micrometer:micrometer-tracing-bridge-brave'
implementation 'io.zipkin.reporter2:zipkin-reporter-brave'

@Configuration
public class ObservabilityConfig {

    @Bean
    public ObservationRegistry observationRegistry(
            MeterRegistry meterRegistry,
            TracingObservationHandler tracingHandler) {
        ObservationRegistry registry = ObservationRegistry.create();

        // 메트릭 핸들러 등록
        registry.observationConfig()
                .observationHandler(
                    new DefaultMeterObservationHandler(meterRegistry))
                .observationHandler(tracingHandler);

        return registry;
    }
}

@Service
public class OrderService {

    @Observed(name = "order.create",
              lowCardinalityKeyValues = {"service", "order"})
    public Order createOrder(OrderRequest request) {
        // 이 메서드는 자동으로:
        // 1. order.create.duration 메트릭을 생성
        // 2. 분산 추적 스팬을 생성
        // 3. trace-id와 span-id를 로그에 포함
        return processOrder(request);
    }
}

김개발 씨는 이제 제법 관찰 가능성 전문가가 되어가고 있었습니다. 그러던 어느 날, 신입 개발자 이주니어 씨가 헷갈리는 표정으로 다가왔습니다.

"선배님, 메트릭이랑 추적이 뭐가 다른 건가요? 둘 다 모니터링 아닌가요?" 좋은 질문이었습니다.

김개발 씨도 처음에는 이 둘의 차이를 제대로 이해하지 못했었습니다. 하지만 이제는 실무에서 직접 경험하면서 그 차이를 명확히 알게 되었습니다.

메트릭과 추적의 본질적 차이 메트릭과 추적은 무엇이 다를까요? 쉽게 비유하자면, 메트릭은 건강검진의 수치와 같습니다.

혈압이 120/80이고, 혈당이 100이라는 숫자로 건강 상태를 파악합니다. 반면 추적은 진료 기록과 같습니다.

"환자가 접수를 하고, 대기실에서 10분 기다리고, 진찰실에서 5분 진료받고, 약국에서 처방전을 받았다"는 시간순 흐름을 기록합니다. 메트릭은 무엇을 측정하고, 추적은 어떻게 진행되었는지를 보여줍니다.

각각만 사용할 때의 한계 메트릭만 있으면 어떤 문제가 있을까요? "API 응답 시간이 평균 2초입니다"라는 메트릭을 봤을 때, 우리는 느리다는 것은 알지만 왜 느린지는 모릅니다.

데이터베이스가 문제인지, 외부 API가 문제인지, 아니면 알고리즘이 비효율적인지 알 수 없습니다. 메트릭은 증상만 알려줄 뿐 원인을 찾아주지 않습니다.

반대로 추적만 있으면 어떨까요? 개별 요청의 흐름은 자세히 볼 수 있지만, 전체적인 패턴을 파악하기 어렵습니다.

"지난 한 시간 동안 평균 응답 시간이 어떻게 변했는가?"같은 질문에 답하기 어렵습니다. 통합의 힘 바로 이런 이유로 메트릭과 추적을 함께 사용해야 합니다.

Observation API는 이 둘을 하나의 코드로 통합합니다. @Observed 어노테이션 하나로 메트릭도 수집되고 추적 스팬도 생성됩니다.

메트릭으로 전체적인 추세를 보고, 이상 징후를 발견하면 추적 데이터로 드릴다운하여 근본 원인을 찾습니다. 무엇보다 상관관계 분석이 가능해집니다.

코드 동작 분석 위의 코드를 자세히 살펴봅시다. 먼저 micrometer-tracing-bridge-bravezipkin-reporter-brave 의존성을 추가합니다.

Micrometer는 메트릭을, Brave는 분산 추적을 담당합니다. DefaultMeterObservationHandler는 Observation 이벤트를 Micrometer 메트릭으로 변환합니다.

자동으로 .count, .duration, .max 같은 메트릭이 생성됩니다. TracingObservationHandler는 분산 추적 스팬을 생성합니다.

각 관찰이 하나의 스팬이 되고, 부모-자식 관계가 자동으로 연결됩니다. @Observed 어노테이션이 붙은 메서드가 실행되면, 자동으로 order.create 라는 이름의 메트릭이 생성됩니다.

동시에 추적 시스템에는 "order.create"라는 스팬이 기록됩니다. lowCardinalityKeyValues로 지정한 태그는 메트릭과 추적 모두에 적용됩니다.

실무 시나리오 실제 장애 대응 상황을 살펴봅시다. 새벽 2시, 온콜 엔지니어에게 알람이 울립니다.

"API 응답 시간 급증!" Grafana 대시보드를 열어보니 메트릭이 평소 200ms에서 3초로 치솟았습니다. 메트릭으로 문제를 발견했습니다.

이제 추적 데이터를 확인합니다. Zipkin을 열어서 느린 요청 하나를 선택합니다.

스팬 다이어그램을 보니 "외부 결제 API 호출"에서 2.8초가 소요되었습니다. 추적으로 원인을 진단했습니다.

결제 서비스 팀에 연락하니 그쪽에서 배포를 하다가 문제가 생겼다고 합니다. 메트릭과 추적을 함께 사용했기 때문에 5분 만에 근본 원인을 찾을 수 있었습니다.

상관관계와 컨텍스트 가장 강력한 점은 컨텍스트 전파입니다. 분산 추적이 활성화되면 모든 로그에 trace-idspan-id가 자동으로 포함됩니다.

로그를 볼 때 이 ID로 검색하면 같은 요청과 관련된 모든 로그를 한 번에 볼 수 있습니다. 마이크로서비스 A, B, C를 거친 요청이라도 trace-id로 묶어서 추적할 수 있습니다.

또한 메트릭에도 같은 태그가 붙어서, 특정 서비스나 엔드포인트의 메트릭만 필터링하여 볼 수 있습니다. 주의사항 하지만 조심할 점도 있습니다.

추적 데이터는 메트릭보다 훨씬 용량이 큽니다. 모든 요청을 100% 샘플링하면 저장소가 금방 차버립니다.

따라서 프로덕션에서는 보통 1-10% 정도만 샘플링합니다. 메트릭은 모든 요청을 집계하지만, 추적은 일부만 기록합니다.

또한 trace-id를 로그에 포함시키려면 로그 포맷을 수정해야 합니다. Logback이나 Log4j2 설정에서 %X{traceId}를 패턴에 추가해야 합니다.

정리 이주니어 씨는 설명을 듣고 고개를 끄덕였습니다. "아, 메트릭은 무엇이 문제인지 찾고, 추적은 왜 문제인지 찾는 거네요!" 메트릭과 추적을 함께 사용하면 시스템을 입체적으로 관찰할 수 있습니다.

여러분도 이 두 도구를 조화롭게 활용하여 빠른 장애 대응 체계를 구축해보세요.

실전 팁

💡 - 프로덕션에서는 추적 샘플링 비율을 1-10%로 설정하여 비용을 절감하세요

  • 로그에 trace-id를 포함시켜 로그와 추적을 연결하세요
  • 메트릭으로 이상 징후를 발견하고, 추적으로 근본 원인을 찾는 워크플로를 구축하세요

5. 커스텀 관찰

"우리 회사만의 특별한 비즈니스 로직도 관찰하고 싶어요." 김개발 씨가 요구사항을 정리하며 말했습니다. 표준 메트릭만으로는 부족했습니다.

재고 수량, 주문 금액, 사용자 등급 같은 도메인 특화 데이터도 함께 기록해야 했습니다.

커스텀 관찰은 표준 Observation에 비즈니스 도메인의 특화된 데이터를 추가하는 방법입니다. Observation.Context를 확장하여 원하는 정보를 담고, 커스텀 핸들러로 처리할 수 있습니다.

마치 택배 상자에 특별한 라벨을 붙이는 것처럼, 관찰 데이터에 도메인 정보를 추가합니다.

다음 코드를 살펴봅시다.

// 커스텀 Context 정의
public class OrderObservationContext extends Observation.Context {
    private final OrderRequest request;
    private OrderResult result;
    private BigDecimal orderAmount;
    private String customerTier;

    public OrderObservationContext(OrderRequest request) {
        this.request = request;
    }

    // getter, setter 생략
}

// 커스텀 Convention 정의
public class OrderObservationConvention implements ObservationConvention<OrderObservationContext> {

    @Override
    public KeyValues getLowCardinalityKeyValues(OrderObservationContext context) {
        return KeyValues.of(
            "customer.tier", context.getCustomerTier(),
            "payment.method", context.getRequest().getPaymentMethod()
        );
    }

    @Override
    public KeyValues getHighCardinalityKeyValues(OrderObservationContext context) {
        return KeyValues.of(
            "order.amount", String.valueOf(context.getOrderAmount())
        );
    }
}

// 사용 예시
@Service
public class OrderService {
    private final ObservationRegistry registry;

    public Order createOrder(OrderRequest request) {
        OrderObservationContext context = new OrderObservationContext(request);

        return Observation.createNotStarted(new OrderObservationConvention(), context, registry)
            .observe(() -> {
                Order order = processOrder(request);
                context.setResult(order);
                context.setOrderAmount(order.getTotalAmount());
                context.setCustomerTier(order.getCustomer().getTier());
                return order;
            });
    }
}

김개발 씨는 이제 Observation API를 능숙하게 사용하게 되었습니다. 하지만 새로운 요구사항이 들어왔습니다.

"김개발 씨, 메트릭에 주문 금액도 같이 기록할 수 있나요? VIP 고객의 주문만 필터링해서 보고 싶어요." 프로덕트 매니저가 요청했습니다.

표준 Observation은 실행 시간, 성공/실패 정도만 기록합니다. 하지만 실제 비즈니스에서는 도메인 특화 데이터가 훨씬 중요합니다.

주문 금액, 재고 수량, 사용자 등급, 할인율 같은 정보 말이죠. 도메인 지식을 관찰에 녹이기 커스텀 관찰이란 무엇일까요?

쉽게 비유하자면, 커스텀 관찰은 마치 택배 상자에 특별한 메모를 붙이는 것과 같습니다. 일반 택배는 주소와 무게만 표시하지만, 특별한 택배는 "깨지기 쉬움", "냉장 보관", "고가품" 같은 추가 정보를 붙입니다.

커스텀 관찰도 마찬가지로 표준 정보 외에 비즈니스 도메인의 특별한 데이터를 함께 기록합니다. 표준 관찰의 한계 기본 Observation만 사용하면 어떤 문제가 있을까요?

모든 주문이 똑같은 메트릭으로 집계됩니다. 100원짜리 주문이나 100만 원짜리 주문이나 똑같이 "주문 1건"으로만 카운트됩니다.

VIP 고객의 주문이 느린지, 일반 고객의 주문이 느린지 구분할 수 없습니다. 신용카드 결제와 계좌이체의 성공률 차이를 알 수 없습니다.

비즈니스 인사이트를 얻으려면 도메인 데이터가 필수입니다. 커스텀 Context의 도입 바로 이 문제를 해결하기 위해 커스텀 Context를 만듭니다.

Observation.Context를 상속받아 원하는 필드를 추가합니다. 주문 요청 정보, 처리 결과, 금액, 고객 등급 등 비즈니스 로직에서 중요한 데이터를 모두 담을 수 있습니다.

이 Context는 관찰이 진행되는 동안 살아있으면서 계속 업데이트됩니다. 무엇보다 타입 안전성을 보장합니다.

코드 상세 분석 위의 코드를 단계별로 살펴봅시다. 먼저 OrderObservationContext 클래스를 만듭니다.

Observation.Context를 상속받아 주문 관련 필드들을 추가합니다. orderAmount, customerTier 같은 비즈니스 데이터가 여기 담깁니다.

다음으로 OrderObservationConvention을 구현합니다. Convention은 Context의 데이터를 어떻게 태그로 변환할지 정의합니다.

getLowCardinalityKeyValues()에서는 카디널리티가 낮은 값들을 반환합니다. 고객 등급은 "VIP", "일반", "신규" 정도로 제한되므로 low cardinality입니다.

getHighCardinalityKeyValues()에서는 주문 금액처럼 고유값이 많은 데이터를 반환합니다. 이 값들은 메트릭 태그로는 부적합하지만, 추적이나 로그에서는 유용합니다.

실제 사용 시에는 Observation.createNotStarted()에 Convention과 Context를 함께 전달합니다. observe() 블록 안에서 비즈니스 로직을 실행하고, 결과를 Context에 채워넣습니다.

실무 활용 시나리오 실제 전자상거래 플랫폼에서 이렇게 활용합니다. 주문이 완료된 후 메트릭을 보면 "VIP 고객의 평균 주문 금액은 50만 원, 일반 고객은 5만 원"이라는 인사이트를 얻을 수 있습니다.

"신용카드 결제는 평균 2초, 계좌이체는 5초"라는 패턴을 발견할 수 있습니다. 장애가 발생했을 때도 유용합니다.

"100만 원 이상 고액 주문만 실패한다"는 패턴을 발견하면, 금액 검증 로직에 문제가 있다는 것을 빠르게 알 수 있습니다. 고급 패턴: DocumentedObservation 더 나아가면 DocumentedObservation을 구현할 수 있습니다.

이것은 관찰에 대한 메타데이터를 문서로 남기는 기능입니다. 어떤 이름을 사용하고, 어떤 태그가 있는지, 무슨 의미인지를 enum으로 정의합니다.

IDE에서 자동완성도 지원되고, 문서도 자동 생성됩니다. 주의사항 하지만 조심할 부분이 있습니다.

Context에 너무 많은 데이터를 담으면 메모리 오버헤드가 발생합니다. 필요한 최소한의 정보만 담아야 합니다.

또한 민감한 정보는 절대 포함하면 안 됩니다. 신용카드 번호, 비밀번호 같은 데이터는 메트릭이나 추적에 남기면 보안 사고가 됩니다.

Convention의 네이밍도 중요합니다. "order.create.amount" 같은 일관된 네이밍 규칙을 팀 전체가 따라야 나중에 메트릭을 찾기 쉽습니다.

정리 김개발 씨는 커스텀 관찰을 적용한 후 프로덕트 매니저에게 데모를 보여줬습니다. "와, 이제 VIP 고객의 주문 패턴이 한눈에 보이네요!" 커스텀 관찰을 활용하면 기술 메트릭을 넘어 비즈니스 메트릭을 수집할 수 있습니다.

여러분도 도메인 지식을 관찰 가능성에 녹여서 더 깊은 인사이트를 얻어보세요.

실전 팁

💡 - Context에는 필요한 최소한의 정보만 담아 메모리 효율을 유지하세요

  • 민감한 정보는 절대 Context에 포함시키지 마세요
  • Convention 네이밍은 팀 전체가 일관된 규칙을 따라야 합니다

6. Convention 설정

"왜 우리 팀과 저 팀의 메트릭 이름이 다르죠?" 코드 리뷰 시간에 누군가 질문했습니다. 한 팀은 "order-create"를 쓰고, 다른 팀은 "order.create"를 썼습니다.

일관성이 필요했습니다.

Convention은 관찰 데이터의 네이밍 규칙과 태그 체계를 표준화하는 메커니즘입니다. 마치 코딩 컨벤션이 코드 스타일을 통일하듯이, Observation Convention은 메트릭과 추적의 명명 규칙을 통일합니다.

팀 전체가 일관된 관찰 데이터를 생성할 수 있게 해줍니다.

다음 코드를 살펴봅시다.

// 기본 Convention 정의
public enum OrderObservations implements DocumentedObservation {
    ORDER_CREATE {
        @Override
        public String getName() {
            return "order.create";
        }

        @Override
        public String getContextualName() {
            return "order create";
        }

        @Override
        public KeyName[] getLowCardinalityKeyNames() {
            return new KeyName[]{
                KeyName.of("order.type", "주문 타입 (일반/정기)"),
                KeyName.of("payment.method", "결제 수단")
            };
        }
    },
    ORDER_CANCEL {
        @Override
        public String getName() {
            return "order.cancel";
        }
        // ... 생략
    }
}

// GlobalObservationConvention으로 전역 태그 추가
@Configuration
public class GlobalObservationConfig implements GlobalObservationConvention {

    @Override
    public KeyValues getLowCardinalityKeyValues(Observation.Context context) {
        return KeyValues.of(
            "application", "order-service",
            "environment", getEnvironment(),
            "region", "ap-northeast-2"
        );
    }

    @Override
    public boolean supportsContext(Observation.Context context) {
        return true;
    }
}

// 사용 예시
@Service
public class OrderService {

    public Order createOrder(OrderRequest request) {
        return OrderObservations.ORDER_CREATE.start(registry)
            .lowCardinalityKeyValue("order.type", request.getType())
            .lowCardinalityKeyValue("payment.method", request.getPaymentMethod())
            .observe(() -> processOrder(request));
    }
}

김개발 씨의 회사는 이제 여러 팀이 Observation API를 적극적으로 사용하고 있었습니다. 하지만 새로운 문제가 생겼습니다.

A팀은 메트릭 이름을 "order-create"로 짓고, B팀은 "order.create"로 짓고, C팀은 "orderCreate"로 지었습니다. Grafana 대시보드를 만들려고 하니 팀마다 다른 이름 때문에 쿼리가 복잡해졌습니다.

태그 이름도 제각각이었습니다. 박시니어 씨가 전체 회의를 소집했습니다.

"이제 Convention을 정의할 때가 됐습니다. 전사 표준을 만들어야 해요." 표준화의 필요성 Convention이 왜 중요할까요?

쉽게 비유하자면, Convention은 마치 도로의 교통 신호와 같습니다. 모든 나라가 빨간불은 정지, 초록불은 진행이라는 규칙을 따르기 때문에 운전이 가능합니다.

만약 나라마다 색깔의 의미가 다르다면 혼란스러울 것입니다. 관찰 데이터도 마찬가지입니다.

팀마다 다른 네이밍을 쓰면 통합 분석이 불가능합니다. 일관성 없는 관찰의 문제점 Convention이 없으면 어떻게 될까요?

메트릭 이름이 제각각이면 Grafana에서 여러 서비스를 한 번에 조회할 수 없습니다. 쿼리를 작성할 때마다 각 팀의 네이밍을 외워야 합니다.

새로운 팀원이 들어오면 어떤 메트릭이 무엇을 의미하는지 알 수가 없습니다. 알람 설정도 복잡해집니다.

더 큰 문제는 데이터가 중복 생성된다는 점입니다. 같은 개념을 다른 이름으로 기록하면 저장소 용량이 2배, 3배로 불어납니다.

DocumentedObservation으로 표준화 바로 이런 문제를 해결하기 위해 Convention을 정의합니다. DocumentedObservation 인터페이스를 구현한 enum을 만들면, IDE에서 자동완성을 지원받을 수 있습니다.

팀원들이 임의로 이름을 만들지 않고 정의된 enum을 사용하게 됩니다. 문서도 자동 생성되어 어떤 관찰이 있는지 한눈에 파악할 수 있습니다.

무엇보다 타입 안전성이 보장됩니다. 코드 구조 분석 위의 코드를 자세히 살펴봅시다.

OrderObservations enum은 주문 도메인의 모든 관찰을 정의합니다. ORDER_CREATE, ORDER_CANCEL 같은 상수를 만들고, 각각 getName()으로 메트릭 이름을 반환합니다.

getContextualName()은 사람이 읽기 쉬운 이름을 제공합니다. 메트릭 이름은 "order.create"처럼 기계적이지만, contextual name은 "order create"처럼 자연스럽습니다.

getLowCardinalityKeyNames()에서 허용되는 태그들을 문서화합니다. 태그 이름과 설명을 함께 제공하면, 나중에 메트릭을 보는 사람이 "order.type이 뭐지?"라고 헷갈리지 않습니다.

GlobalObservationConvention은 모든 관찰에 공통으로 적용되는 태그를 정의합니다. 애플리케이션 이름, 환경, 리전 같은 인프라 정보를 자동으로 추가합니다.

개별 코드에서 일일이 추가하지 않아도 됩니다. 실무 적용 전략 대규모 조직에서는 어떻게 운영할까요?

플랫폼 팀이 공통 Convention 라이브러리를 만들어서 배포합니다. 각 서비스 팀은 이 라이브러리를 의존성으로 추가하고, 정의된 Observation만 사용합니다.

새로운 관찰이 필요하면 풀 리퀘스트로 Convention을 추가하고, 코드 리뷰를 거쳐 승인됩니다. 이렇게 하면 전사 표준이 자연스럽게 유지됩니다.

Grafana 대시보드도 표준 Convention 기반으로 템플릿화할 수 있습니다. 새로운 서비스를 추가할 때도 일관된 메트릭으로 즉시 모니터링이 가능합니다.

네이밍 규칙 모범 사례 어떤 네이밍 규칙이 좋을까요? Micrometer의 권장사항은 점(.)으로 구분된 소문자 단어입니다.

"order.create", "payment.process" 같은 형태입니다. Prometheus는 밑줄(_)을 선호하지만, Micrometer가 자동 변환해주므로 점을 쓰는 것이 좋습니다.

태그 이름도 일관성이 중요합니다. "service.name"을 쓸지 "service"를 쓸지 정해야 합니다.

OpenTelemetry의 시맨틱 컨벤션을 참고하면 좋습니다. 주의사항 하지만 과도한 표준화는 독이 됩니다.

Convention을 너무 엄격하게 만들면 팀의 유연성이 떨어집니다. 모든 세부사항을 규정하기보다는, 핵심 원칙만 정하고 세부사항은 팀에 맡기는 것이 좋습니다.

"점으로 구분한다", "소문자를 사용한다" 정도만 강제하고, 구체적인 이름은 도메인 팀이 정하게 합니다. 또한 Convention은 살아있는 문서입니다.

비즈니스가 변하면 Convention도 진화해야 합니다. 1년에 한 번 정도는 회고를 통해 불필요한 규칙을 제거하고 새로운 패턴을 추가합니다.

정리 김개발 씨의 회사는 Convention을 도입한 후 혼란이 크게 줄었습니다. "이제 어떤 팀의 메트릭이든 바로 이해할 수 있어요!" Convention을 잘 정의하면 팀 간 협업이 쉬워지고, 관찰 데이터의 품질이 향상됩니다.

여러분도 조직의 규모에 맞는 Convention을 만들어서 체계적인 관찰 가능성 문화를 구축해보세요.

실전 팁

💡 - DocumentedObservation enum을 사용하여 IDE 자동완성과 타입 안전성을 확보하세요

  • GlobalObservationConvention으로 공통 태그를 자동 추가하세요
  • 네이밍 규칙은 Micrometer와 OpenTelemetry의 표준을 따르는 것이 좋습니다

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

#Spring#Observation#Micrometer#Tracing#Metrics#마이크로서비스

댓글 (0)

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