본문 바로가기
Spring/SpringBoot

Micrometer Tracing으로 Spring 애플리케이션 분산 트레이싱하기

by J4J 2025. 9. 29.
300x250
반응형

안녕하세요. J4J입니다.

 

이번 포스팅은 micrometer tracing으로 spring 애플리케이션 분산 트레이싱하는 방법에 대해 적어보는 시간을 가져보려고 합니다.

 

 

 

관련 글

 

애플리케이션 모니터링을 위한 Spring Boot Actuator: 개념 정리와 구성 가이드

 

애플리케이션 모니터링을 위한 Spring Boot Actuator: 개념 정리와 구성 가이드

안녕하세요. J4J입니다. 이번 포스팅은 애플리케이션 모니터링을 위한 spring boot actuator 개념 정리 및 구성 가이드에 대해 적어보는 시간을 가져보려고 합니다. Spring Boot Actuator 개념 spring boot actuato

jforj.tistory.com

 

 

반응형

 

 

Tracing 이란

 

tracing이라고 하는 것은 사전적인 정보와 동일하게 추적한다라는 의미를 가지고 있습니다.

 

이것은 it적으로 표현한다면, 애플리케이션이 실행되고 있는 단계에서 사용자의 요청이 넘어올 때 어떤 비즈니스 흐름을 가지고 요청이 처리되는지에 대해 알기 위해 요청 흐름을 추적하는 방법이라고 얘기할 수 있습니다.

 

 

 

tracing에 대한 개념을 왜 알아야 할까요 ?

 

tracing은 작은 규모의 단일 서비스 기반으로 사용자의 요청을 처리하는 곳에서는 필요하지 않은 개념일 수도 있습니다.

 

왜냐하면 서버에 올라가 있는 애플리케이션 로그를 보게 된다면 요청에 대한 모든 비즈니스 흐름을 확인할 수 있기 때문입니다.

 

뭐.. 이 또한 tracing 중 하나라고 볼 수는 있을 겁니다.

 

 

 

하지만 이곳에서 말하는 tracing은 하나의 작은 서비스에서 요청을 추적하는 것이 아니고, 한 번의 사용자 요청에 의해 작은 단위의 여러 서비스들이 비즈니스 처리가 이루어지는 것에 더 초점이 맞춰져 있습니다.

 

많이들 이해할 수 있는 표현으로는 msa 기반의 서비스라고도 얘기할 수 있습니다.

 

msa 기반을 목적으로 서비스 제공을 조금이라도 해본 개발자 분들은 항상 고민하고 있는 것이 사용자의 액션에 문제가 발생했을 때 어디서 문제가 발생했는지를 추적하는 것입니다.

 

다양한 서비스의 비즈니스를 통해 요청이 처리되고 있기 때문에 오류가 어디서 발생 했는지 확인하려면 모든 서비스의 로그를 훑어보는 최악의 경우도 경험할 수도 있습니다.

 

 

 

이럴 때 필요한 것이 tracing입니다.

 

tracing을 알기 위해서는 크게 다음과 같이 2가지의 개념에 대해 숙지해야 합니다.

 

  • Trace >> 하나의 요청이 발생했을 때 요청 하나의 전체 흐름을 추적할 수 있는 단위
  • Span >> Trace 안에서 발생할 수 있는 개별적인 작업 단위

 

 

개념을 더 이해하기 위해 사용자의 주문 요청에 대해서 예시를 들어보겠습니다.

 

먼저, 사용자의 주문 요청 1개에 대해서는 1개의 trace 정보가 생성됩니다.

 

그리고 주문 요청에 의해 다음과 같은 흐름으로 여러 서비스들을 거쳐갈 수 있습니다.

 

"주문 서비스 (주문 api) > 재고 서비스 (재고 확인 api) > 결제 서비스 (결제 api) > 재고 서비스 (재고 차감 api)"

 

 

 

이와 같은 흐름으로 비즈니스 처리가 이루어졌다고 가정한다면, 각 서비스 처리가 이루어질 때마다 새로운 span 이 생성됩니다.

 

즉, 위의 흐름들은 다음과 같은 trace와 span 정보들을 가질 수 있게 됩니다.

 

  • 주문 서비스 (주문 api) >> Trace (A) / Span (1)
  • 재고 서비스 (재고 확인 api) >> Trace (A) / Span (2)
  • 결제 서비스 (결제 api) >> Trace (A) / Span (3)
  • 재고 서비스 (재고 차감 api) >> Trace (A) / Span (4)

 

 

 

결론적으로 span 정보를 이용해서는 각 서비스의 처리가 이루어진 시간과 종료된 시간, 앞/뒤 비즈니스 처리가 연계되는 span 들간의 관계들에 대해서 알 수 있습니다.

 

그리고 trace 정보를 이용해서는 사용자 요청에 의해 비즈니스 처리가 이루어진 여러 서비스들의 흐름을 한 번에 확인할 수 있게 됩니다.

 

 

 

 

Tracing 정보 공유 방법 & 구조

 

tracing 정보를 공유하는 가장 대표적인 방법은 w3c 기반의 traceparent 정보를 활용하는 방법입니다.

 

해당 방식은 비교적 최근에 tracing 정보에 대한 표준이 수립되면서 사용되기 시작했습니다.

 

각자 표현하던 방식이 달랐던 상황에서 표준 방식에 대해서 자리 잡게 되었고, 이후부터는 spring을 포함한 모든 서비스에서 해당 값을 이용하여 애플리케이션 간 tracing 처리를 수행했습니다.

 

 

 

traceparent를 전달하는 방식은 header 정보를 이용하는 것입니다.

 

서로 다른 api를 호출하는 과정에서 header 정보에 현재 요청 흐름에 대한 traceparent 정보를 담게 되고, 요청을 전달받은 서비스에서는 traceaprent 정보를 확인하여 추가적인 액션을 수행할 수 있습니다.

 

그래서 모든 요청이 처리될 때까지 traceparent 정보를 서로 지속적으로 공유하게 되고, 요청이 끝난 시점에 trace 값을 활용하여 서비스 간 흐름을 추적할 수 있게 됩니다.

 

 

 

tracing의 구조는 다음과 같이 4가지로 나뉩니다.

 

"{version}-{trace-id}-{span-id}-{trace-flags}"

 

 

 

[ version ]

 

먼저 version은 traceparent의 버전 정보에 대한 표기가 되는 곳이지만 아직까지 00으로만 사용되는 것으로 알고 있습니다.

 

 

 

[ trace-id ]

 

trace id는 위에서 설명한 것과 동일하게 요청 1개에 대해서 1개의 정보만 고유하게 존재하는 것으로 거쳐간 모든 서비스에서는 동일한 trace id가 설정되어 있어야 합니다.

 

32자리 16진수 형태로 구성되어 표현되고 있습니다.

 

 

 

[ span-id ]

 

span id도 위에서 설명한 것과 동일하게 서비스 별 개별적인 비즈니스 처리가 이루어질 때마다 생성되는 단위로 서로 다른 비즈니스에서는 서로 다른 span id를 가지게 됩니다.

 

16자리 16진수 형태로 구성되어 표현되고 있습니다.

 

 

 

[ trace-flags ]

 

trace flags는 sampling에 대한 추적 옵션을 담고 있는 곳입니다.

 

여기서 말하는 sampling이라고 하는 것은 사용자의 요청이 발생되었을 때 분산 추적을 위해 사용되는 jaeger, zipkin과 같은 백엔드 서비스에 수집되는 대상을 의미합니다.

 

sampling 대상이면 백엔드 서비스에 수집되어 tracing 정보를 활용한 요청 추적을 할 수 있는 상황이 된 것을 의미합니다.

 

반대로 대상이 되지 않으면 traceparent 정보는 서비스 간 지속적으로 전달되지만 백엔드 서비스에 별도로 수집되지 않는 것을 의미합니다.

 

보통 모든 요청에 대해 백엔드 서비스에 수집이 되면 과도한 요청 정보 수집으로 인해 스토리지 비용이 방대해지게 됩니다.

 

그래서 모든 요청에 대해 수집하지 않기에 해당 정보를 나타내기 위해 trace flags 정보가 활용됩니다.

 

 

 

trace flags 값이 00이면 sampling의 대상이 되지 않아 백엔드 서비스에 수집되지 않는 것을 의미합니다.

 

하지만 값이 01이라면 sampling 대상이 되어 백엔드 서비스에 수집된 것을 의미합니다.

 

 

 

이 외에도 더 자세한 설명은 W3C Trace Context 공식 문서에서 확인할 수 있습니다.

 

더 많은 내용이 필요하신 분들은 참고 부탁드립니다.

 

 

 

 

Micrometer Tracing

 

micrometer라고 하는 것은 spring boot에서 모니터링 처리를 위해 사용되는 대표적인 라이브러리입니다.

 

그중 micrometer tracing은 spring 애플리케이션 내부에서 tracing 처리를 위해 사용될 수 있습니다.

 

자체적으로 tracing 처리를 위한 구성을 해볼 수 있지만, micrometer tracing을 사용하면 단순히 의존성 설정만을 통해 다음과 같은 기능들에 즉시 제공됩니다.

 

  • trace/span 자동 생성 & 관리
  • restclient/webclinet 등을 이용한 api 호출 시 header로 traceparent 자동 전파
  • kafka producer/consumer 등을 이용한 메시지 처리에도 header로 traceparent 자동 전파
  • mdc 기반 로깅 처리에 trace/span 정보 자동 출력 설정
  • exporter를 이용하여 jaeger/zipkin 등 백엔드 서비스에 자동 전송

 

 

 

이와 같이 많은 기능들을 자동으로 모두 설정 및 적용해 주기 때문에 tracing 처리를 위한 별도 작업을 하지 않아도 tracing 처리의 많은 부분이 수행될 수 있습니다.

 

물론 더 상세한 추적을 위해서는 별도의 백엔드 서비스들이 필요하겠지만, 다음 글에서 다시 다루도록 하고 micrometer tracing을 기반으로 더 부가적인 기능들을 활용한다면 spring 애플리케이션 내부에서 tracing을 수행하는 데는 큰 무리가 없을 것으로 생각합니다.

 

 

 

 

Tracing 설정 & 자동 전파 테스트

 

이번에는 간단하게 두 개의 spring 애플리케이션을 동작시킨 뒤 api 요청을 수행할 때 tracing 정보가 자동 설정이 되며, 전파로 자동으로 이루어지는 것을 확인해 보겠습니다.

 

또한 mdc 기반의 로깅 정보에도 trace와 span 정보가 함께 확인되어 결과적으로 비즈니스 처리에 대한 추적이 잘 수행될 수 있을지 함께 적용해 보겠습니다.

 

설정하기에 앞서, 모든 설정들은 tracing 처리가 이루어져야 하는 모든 서비스에 적용되어 있어야 정상적으로 동작합니다.

 

만약, 요청하는 쪽에만 설정이 되어 있고 응답하는 쪽에는 설정이 되어 있지 않다면 올바른 추적이 힘들 수 있습니다.

 

 

 

[ 1. dependency 설정 ]

 

// build.gradle (request, response 모두 설정)
dependencies {
    // actuator 설정
    implementation 'org.springframework.boot:spring-boot-starter-actuator'
    // open telemetry tracing bridge 설정
    implementation 'io.micrometer:micrometer-tracing-bridge-otel'
}

 

 

 

[ 2. mdc 설정 ]

 

// application.yml (request, response 모두 설정)
logging:
  level:
    root: info # 로깅 레벨 설정
  pattern:
    console: "%d{yyyy-MM-dd HH:mm:ss} [%X{traceId:-}] [%X{spanId:-}] %-5level %logger{36} - %msg%n" # 로깅 콘솔 패턴 설정

 

 

 

[ 3. response 서버 port 설정 ]

 

테스트를 위해 response 서버는 8081 port를 사용하도록 설정합니다.

 

// application.yml
server:
  port: 8081

 

 

 

[ 4. 요청 서버 controller ]

 

요청 서버에서 중요한 것 중 하나는 api 요청을 수행할 때 rest client, web client, rest template 등 다양한 것들을 사용할 수 있는데 객체 구성을 할 때 "builder 기반 구성" 이 이루어져야 하는 것입니다.

 

builder 기반으로 구성되지 않으면 자동으로 tracing 정보가 header에 담기지 않기 때문에 필수적으로 적용되어야 하는 사항입니다.

 

package com.jforj.tracingrequest.controller;

import io.micrometer.tracing.Span;
import io.micrometer.tracing.Tracer;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestClient;

@RestController
@RequiredArgsConstructor
@Slf4j
public class TracingRequestController {
    private final Tracer tracer;
    private final RestClient.Builder restClientBuilder; // 모든 api 요청에 tracing 전파를 위해서는 builder 기반으로 생성

    @GetMapping("/tracing-request")
    public ResponseEntity<Object> tracingRequest() {
        Span span = tracer.currentSpan();
        if (span != null) {
            log.info("tracing request info, trace-id: {}, span-id: {}", span.context().traceId(), span.context().spanId());
        }

        // api 요청
        String apiResponse =
                restClientBuilder
                        .build()
                        .get()
                        .uri("http://localhost:8081/tracing-response")
                        .retrieve()
                        .body(String.class);

        log.info("api response, {}", apiResponse);

        return ResponseEntity.ok("request success");
    }
}

 

 

 

[ 5. 응답 서버 controller ]

 

package com.jforj.tracingresponse.controller;

import io.micrometer.tracing.Span;
import io.micrometer.tracing.Tracer;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestClient;

@RestController
@RequiredArgsConstructor
@Slf4j
public class TracingResponseController {
    private final Tracer tracer;
    private final RestClient.Builder restClientBuilder; // 모든 api 요청에 tracing 전파를 위해서는 builder 기반으로 생성

    @GetMapping("/tracing-response")
    public ResponseEntity<Object> tracingResponse() {
        Span span = tracer.currentSpan();
        if (span != null) {
            log.info("tracing response info, trace-id: {}, span-id: {}", span.context().traceId(), span.context().spanId());
        }

        return ResponseEntity.ok("response success");
    }
}

 

 

 

 

[ 6. 테스트 결과 ]

 

위와 같은 구성을 하게 되는 경우 비즈니스 흐름은 다음과 같습니다.

 

"사용자 요청 > 요청 서비스 api > 응답 서비스 api"

 

 

 

그리고 이곳에서 확인해야 되는 사항은 다음과 같습니다.

 

  • 1개의 요청에 대해 trace id 정보가 동일하게 노출되는지
  • 서로 다른 비즈니스 처리에 서로 다른 span id 정보가 노출되는지
  • mdc에 trace id, span id가 확인되는지

 

 

 

postman을 이용하여 다음과 같이 api 요청을 전송해 보겠습니다.

 

postman api 요청

 

 

 

그러면 요청 서버 쪽에서는 다음과 같은 로그가 남는 것을 확인할 수 있습니다.

 

요청 서버 로그

 

 

 

그리고 응답 서버 쪽에서는 다음과 같은 로그가 남는 것을 확인할 수 있습니다.

 

응답 서버 로그

 

 

 

로그를 확인해 보면 trace id 값이 자동으로 설정 및 전파가 되어 서로 다른 애플리케이션 서버이지만 동일한 값이 출력되는 것을 볼 수 있습니다.

 

하지만 span id는 서로 다른 값이 출력되는 것을 볼 수 있습니다.

 

그리고 해당 정보들이 모두 mdc에 자동 설정되어 로깅 패턴 구성에 의해 함께 출력되는 것도 확인할 수 있습니다.

 

 

 

이와 같이 구성이 되었다면 서로 다른 서비스들 간 비즈니스 처리가 반복적으로 이루어진다고 하더라도 에러 발생 등과 같은 사유로 추적이 필요할 때 손쉽게 확인할 수 있게 됩니다.

 

하지만 단순히 traceparent 정보가 전파가 되었다고 하더라도 지금 바로 명확히 확인할 수는 없습니다.

 

해당 내용은 다음 글에서 open telemetry 기반 exporter 처리를 통해 상세하게 다뤄보도록 하겠습니다.

 

 

 

 

 

 

이상으로 micrometer tracing으로 spring 애플리케이션 분산 트레이싱하는 방법에 대해 간단하게 알아보는 시간이었습니다.

 

읽어주셔서 감사합니다.

 

 

 

728x90
반응형

댓글