[ADVANCED #3] MSA 회복성, 관측성, 모니터링 전략

2026. 3. 2. 14:13·MSA/MSA 아키텍처

0. 들어가며

마이크로서비스 아키텍처에서는 수많은 서비스가 네트워크를 통해 통신한다. 이는 독립적인 배포와 확장이라는 장점을 제공하지만, 동시에 새로운 유형의 장애 가능성을 만들어낸다. 네트워크 지연, 서비스 과부하, 일시적 장애 등이 연쇄적으로 전파되어 전체 시스템을 마비시킬 수 있다.

 

이러한 분산 환경에서 시스템의 안정성과 회복 탄력성(Resilience)을 확보하는 것은 필수적이다. 또한 장애 발생 시 빠르게 원인을 파악하고 대응하기 위한 관측성(Observability) 체계도 함께 갖춰야 한다.

 

이번 글에서는 Resilience4j를 활용한 장애 처리 패턴(Retry, Circuit Breaker, Bulkhead, Timeout, Fallback)과 OpenTelemetry, Zipkin, Fluentd를 활용한 분산 추적 및 모니터링 시스템 구축 방법을 상세히 살펴본다.


1. Resilience 패턴 개요

1.1. 회복 탄력성(Resilience)이란?

회복 탄력성은 시스템이 장애나 예상치 못한 상황에서도 지속적으로 서비스를 제공할 수 있는 능력을 의미한다. 완벽한 시스템은 존재하지 않으며, 중요한 것은 장애가 발생하더라도 전체 시스템이 마비되지 않고 부분적으로 기능하거나 빠르게 복구되는 것이다.

분산 시스템의 일반적인 장애 유형:

  • 네트워크 지연 또는 단절
  • 서비스 과부하 (응답 시간 증가)
  • 일시적 장애 (일시적 DB 연결 실패)
  • 영구적 장애 (서비스 완전 중단)

1.2. Resilience 패턴의 종류

패턴  설명  사용 사례
Retry 실패한 요청을 자동으로 재시도 일시적 네트워크 오류
Circuit Breaker 연속적 실패 시 요청 차단 장애 서비스로의 호출 방지
Bulkhead 서비스별 리소스 격리 장애 전파 방지
Timeout 응답 대기 시간 제한 지연된 요청 처리 방지
Fallback 실패 시 대체 로직 실행 부분적 서비스 유지
Rate Limiter 요청 속도 제한 과부하 방지

2. Resilience4J 적용

2.1. Resilience4J 소개

Resilience4J는 Netflix Hystrix가 더 이상 유지보수되지 않게 되면서 등장한 경량 장애 내성 라이브러리다. 함수형 프로그래밍을 기반으로 설계되어 있으며, 다양한 장애 처리 기능을 제공한다.

의존성 추가:

dependencies {
    implementation 'org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j'
    implementation 'org.springframework.boot:spring-boot-starter-aop'
    implementation 'org.springframework.boot:spring-boot-starter-actuator'
}

기본 설정:

resilience4j:
  circuitbreaker:
    instances:
      order-service:
        register-health-indicator: true
        sliding-window-type: COUNT_BASED
        sliding-window-size: 10
        failure-rate-threshold: 50
        wait-duration-in-open-state: 10s
        permitted-number-of-calls-in-half-open-state: 3
        automatic-transition-from-open-to-half-open-enabled: true
        minimum-number-of-calls: 5
        event-consumer-buffer-size: 10
        record-exceptions:
          - java.io.IOException
          - java.util.concurrent.TimeoutException
          - org.springframework.web.client.HttpServerErrorException

  retry:
    instances:
      order-service-retry:
        max-attempts: 3
        wait-duration: 1s
        retry-exceptions:
          - java.io.IOException
          - org.springframework.web.client.HttpServerErrorException

  bulkhead:
    instances:
      order-service-bulkhead:
        max-concurrent-calls: 10
        max-wait-duration: 10ms

  timelimiter:
    instances:
      order-service-timelimiter:
        timeout-duration: 3s

2.2. Retry 패턴

Retry 패턴은 일시적인 오류가 발생했을 때 자동으로 요청을 재시도하는 패턴이다.

설정:

resilience4j:
  retry:
    instances:
      inventory-service:
        max-attempts: 3
        wait-duration: 1000
        retry-exceptions:
          - org.springframework.web.client.HttpServerErrorException
          - java.net.SocketTimeoutException
        ignore-exceptions:
          - com.example.BusinessException

구현 예시:

@Service
@Slf4j
@RequiredArgsConstructor
public class ProductService {

    private final ProductRepository productRepository;
    private final InventoryServiceClient inventoryClient;

    @Retry(name = "inventory-service", fallbackMethod = "getProductWithStockFallback")
    public ProductDto getProductWithStock(String productId) {
        Product product = productRepository.findById(productId)
            .orElseThrow(() -> new ProductNotFoundException(productId));

        // 재고 서비스 호출 (실패 시 재시도)
        int stock = inventoryClient.getStock(productId);

        return ProductDto.builder()
            .id(product.getId())
            .name(product.getName())
            .price(product.getPrice())
            .stock(stock)
            .build();
    }

    // Fallback 메서드 (재시도 모두 실패 시 호출)
    public ProductDto getProductWithStockFallback(String productId, Exception ex) {
        log.warn("Fallback called for product: {}, error: {}", productId, ex.getMessage());

        Product product = productRepository.findById(productId)
            .orElseThrow(() -> new ProductNotFoundException(productId));

        // 재고 정보 없이 기본값 반환
        return ProductDto.builder()
            .id(product.getId())
            .name(product.getName())
            .price(product.getPrice())
            .stock(-1)  // 재고 정보 없음 표시
            .build();
    }
}

2.3. Circuit Breaker 패턴

Circuit Breaker는 연속적인 실패가 발생하면 요청을 차단하여 장애가 전파되는 것을 방지하는 패턴이다.

상태 전이:

[CLOSED] (정상) → 실패 임계치 초과 → [OPEN] (차단)
    ↑                                            ↓
    └── 성공 ── [HALF_OPEN] ← 대기 시간 경과 ──┘

설정:

resilience4j:
  circuitbreaker:
    instances:
      payment-service:
        register-health-indicator: true
        sliding-window-size: 10
        failure-rate-threshold: 50
        wait-duration-in-open-state: 30s
        permitted-number-of-calls-in-half-open-state: 3
        record-exceptions:
          - java.io.IOException
          - java.util.concurrent.TimeoutException

구현 예시:

@Service
@Slf4j
@RequiredArgsConstructor
public class PaymentService {

    private final PaymentGatewayClient paymentGatewayClient;
    private final CircuitBreakerRegistry circuitBreakerRegistry;

    @CircuitBreaker(name = "payment-service", fallbackMethod = "processPaymentFallback")
    public PaymentResult processPayment(PaymentRequest request) {
        // 결제 게이트웨이 호출
        return paymentGatewayClient.charge(request);
    }

    public PaymentResult processPaymentFallback(PaymentRequest request, Exception ex) {
        log.error("Payment failed, using fallback. OrderId: {}, Error: {}",
            request.getOrderId(), ex.getMessage());

        // 대체 결제 수단 시도 또는 실패 응답
        return PaymentResult.builder()
            .success(false)
            .message("Payment service is currently unavailable. Please try again later.")
            .build();
    }
}

// Circuit Breaker 이벤트 리스너
@Component
@Slf4j
@RequiredArgsConstructor
public class CircuitBreakerEventListener {

    private final CircuitBreakerRegistry circuitBreakerRegistry;

    @PostConstruct
    public void registerEventListeners() {
        circuitBreakerRegistry.getAllCircuitBreakers().forEach(this::registerEventListeners);
    }

    private void registerEventListeners(CircuitBreaker circuitBreaker) {
        circuitBreaker.getEventPublisher()
            .onSuccess(event ->
                log.debug("CircuitBreaker '{}' success", circuitBreaker.getName()))
            .onError(event ->
                log.warn("CircuitBreaker '{}' error: {}", circuitBreaker.getName(), event))
            .onStateTransition(event ->
                log.info("CircuitBreaker '{}' state transition: {} -> {}",
                    circuitBreaker.getName(), event.getOldState(), event.getNewState()))
            .onReset(event ->
                log.info("CircuitBreaker '{}' reset", circuitBreaker.getName()))
            .onCallNotPermitted(event ->
                log.warn("CircuitBreaker '{}' call not permitted - circuit is OPEN",
                    circuitBreaker.getName()));
    }
}

2.4. Bulkhead 패턴

Bulkhead 패턴은 선박의 격벽에서 이름을 따온 패턴으로, 서비스별로 스레드 풀을 분리하여 한 서비스의 장애가 다른 서비스로 전파되는 것을 방지한다.

설정:

resilience4j:
  bulkhead:
    instances:
      order-service:
        max-concurrent-calls: 20
        max-wait-duration: 10ms
  thread-pool-bulkhead:
    instances:
      order-service:
        max-thread-pool-size: 10
        core-thread-pool-size: 5
        queue-capacity: 20
        keep-alive-duration: 20ms

구현 예시:

@Service
@Slf4j
public class BulkheadOrderService {

    private final OrderRepository orderRepository;
    private final InventoryServiceClient inventoryClient;
    private final PaymentServiceClient paymentClient;

    @Bulkhead(name = "order-service", type = Bulkhead.Type.THREADPOOL)
    @CircuitBreaker(name = "order-service", fallbackMethod = "createOrderFallback")
    public CompletableFuture<Order> createOrder(OrderRequest request) {
        return CompletableFuture.supplyAsync(() -> {
            try {
                // 재고 확인
                boolean inStock = inventoryClient.checkStock(
                    request.getProductId(), request.getQuantity());

                if (!inStock) {
                    throw new OutOfStockException("Product out of stock");
                }

                // 결제 처리
                PaymentResult payment = paymentClient.processPayment(
                    request.getUserId(), request.getTotalAmount());

                if (!payment.isSuccess()) {
                    throw new PaymentFailedException("Payment failed");
                }

                // 주문 생성
                Order order = Order.builder()
                    .userId(request.getUserId())
                    .productId(request.getProductId())
                    .quantity(request.getQuantity())
                    .totalAmount(request.getTotalAmount())
                    .status("COMPLETED")
                    .build();

                return orderRepository.save(order);

            } catch (Exception e) {
                log.error("Failed to create order", e);
                throw new OrderCreationException("Order creation failed", e);
            }
        });
    }

    public CompletableFuture<Order> createOrderFallback(OrderRequest request, Exception ex) {
        log.warn("Bulkhead fallback called for order creation", ex);

        // 대체 응답 (주문 실패)
        Order failedOrder = Order.builder()
            .userId(request.getUserId())
            .productId(request.getProductId())
            .status("FAILED")
            .failureReason(ex.getMessage())
            .build();

        return CompletableFuture.completedFuture(failedOrder);
    }
}

2.5. Timeout 패턴

Timeout 패턴은 외부 서비스 호출 시 최대 대기 시간을 제한하여 무한정 대기하는 상황을 방지한다.

설정:

resilience4j:
  timelimiter:
    instances:
      external-api:
        timeout-duration: 2s
        cancel-running-future: true

구현 예시:

@Service
@Slf4j
@RequiredArgsConstructor
public class ExternalApiService {

    private final RestTemplate restTemplate;

    @TimeLimiter(name = "external-api", fallbackMethod = "getDataFallback")
    public CompletableFuture getData(String id) {
        return CompletableFuture.supplyAsync(() -> {
            String url = "<http://external-api.com/data/>" + id;
            return restTemplate.getForObject(url, ApiResponse.class);
        });
    }

    public CompletableFuture getDataFallback(String id, Exception ex) {
        log.warn("Timeout fallback called for id: {}, error: {}", id, ex.getMessage());

        // 캐시된 데이터 또는 기본 응답 반환
        ApiResponse fallback = ApiResponse.builder()
            .id(id)
            .data("Cached data")
            .cached(true)
            .build();

        return CompletableFuture.completedFuture(fallback);
    }
}

2.6. Rate Limiter 패턴

Rate Limiter는 일정 시간 내에 허용되는 요청 수를 제한하여 시스템 과부하를 방지한다.

설정:

resilience4j:
  ratelimiter:
    instances:
      api-gateway:
        limit-for-period: 100
        limit-refresh-period: 1s
        timeout-duration: 500ms

구현 예시:

@RestController
@RequestMapping("/api/public")
@Slf4j
@RequiredArgsConstructor
public class PublicApiController {

    private final ProductService productService;

    @GetMapping("/products")
    @RateLimiter(name = "api-gateway", fallbackMethod = "rateLimitFallback")
    public List<ProductDto> getProducts() {
        return productService.getAllProducts();
    }

    public List<ProductDto> rateLimitFallback(Exception ex) {
        log.warn("Rate limit exceeded", ex);

        // 요청 제한 초과 시 적절한 응답
        throw new RateLimitExceededException("Too many requests. Please try again later.");
    }
}

// 글로벌 Rate Limit 예외 처리
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(RateLimitExceededException.class)
    public ResponseEntity<ErrorResponse> handleRateLimitExceeded(RateLimitExceededException ex) {
        return ResponseEntity
            .status(HttpStatus.TOO_MANY_REQUESTS)
            .header("Retry-After", "60")
            .body(ErrorResponse.builder()
                .code("RATE_LIMIT_EXCEEDED")
                .message(ex.getMessage())
                .timestamp(LocalDateTime.now())
                .build());
    }
}

3. Observability 개념

3.1. 관측성(Observability)이란?

관측성은 시스템의 내부 상태를 외부에서 측정된 데이터만으로 이해할 수 있는 능력을 의미한다. 전통적인 모니터링이 미리 정의된 메트릭을 수집하는 것이라면, 관측성은 예상치 못한 문제를 탐지하고 디버깅할 수 있는 능력에 초점을 맞춘다.

관측성의 세 가지 기둥:

  1. 로깅(Logging): 이산적인 이벤트 기록
  2. 메트릭(Metrics): 집계된 수치 데이터
  3. 트레이싱(Tracing): 요청 흐름 추적

3.2. 분산 추적(Distributed Tracing)의 필요성

마이크로서비스 아키텍처에서는 하나의 사용자 요청이 여러 서비스를 거쳐 처리된다. 이때 장애가 발생하거나 지연이 생기면, 어떤 서비스에서 문제가 발생했는지 파악하기 매우 어렵다.

분산 추적의 핵심 개념:

  • Trace ID: 하나의 요청에 할당되는 고유 ID (전체 요청 흐름 추적)
  • Span ID: 개별 서비스 호출 단위에 할당되는 ID
  • Parent Span ID: 상위 Span의 ID (호출 관계 표현)

4. OpenTelemetry 구조

4.1. OpenTelemetry란?

OpenTelemetry는 관측성 데이터(트레이스, 메트릭, 로그)를 수집하고 전송하기 위한 벤더 중립적인 오픈소스 표준이다. CNCF(Cloud Native Computing Foundation)의 인큐베이팅 프로젝트로, 기존의 OpenTracing과 OpenCensus를 통합했다.

OpenTelemetry 구성 요소:

  • API: 데이터 생성 인터페이스
  • SDK: API 구현체, 처리 파이프라인
  • Collector: 데이터 수집, 처리, 내보내기
  • Exporters: 다양한 백엔드로 데이터 전송 (Jaeger, Zipkin, Prometheus 등)

4.2. 의존성 추가

dependencies {
    // OpenTelemetry
    implementation 'io.opentelemetry:opentelemetry-api:1.31.0'
    implementation 'io.opentelemetry:opentelemetry-sdk:1.31.0'
    implementation 'io.opentelemetry:opentelemetry-exporter-otlp:1.31.0'
    implementation 'io.opentelemetry.instrumentation:opentelemetry-spring-boot-starter:1.31.0-alpha'

    // Zipkin exporter
    implementation 'io.opentelemetry:opentelemetry-exporter-zipkin:1.31.0'

    // Fluentd 연동을 위한 로그백
    implementation 'ch.qos.logback:logback-classic'
    implementation 'org.slf4j:slf4j-api'
}

4.3. OpenTelemetry 설정

application.yml:

opentelemetry:
  traces:
    exporter: zipkin
  exporter:
    zipkin:
      endpoint: <http://zipkin:9411/api/v2/spans>
  propagators: tracecontext, baggage
  resource:
    attributes:
      service.name: ${spring.application.name}
      service.version: 1.0.0
      deployment.environment: production

Java Config:

@Configuration
public class OpenTelemetryConfig {

    @Bean
    public OpenTelemetry openTelemetry() {
        // Zipkin Exporter 설정
        ZipkinSpanExporter zipkinExporter = ZipkinSpanExporter.builder()
            .setEndpoint("<http://localhost:9411/api/v2/spans>")
            .build();

        // Span Processor 설정
        BatchSpanProcessor spanProcessor = BatchSpanProcessor.builder(zipkinExporter)
            .setScheduleDelay(100, TimeUnit.MILLISECONDS)
            .setMaxExportBatchSize(512)
            .build();

        // SdkTracerProvider 설정
        SdkTracerProvider tracerProvider = SdkTracerProvider.builder()
            .addSpanProcessor(spanProcessor)
            .setResource(Resource.create(Attributes.builder()
                .put(ResourceAttributes.SERVICE_NAME, "order-service")
                .put(ResourceAttributes.SERVICE_VERSION, "1.0.0")
                .build()))
            .build();

        // OpenTelemetry 인스턴스 생성
        return OpenTelemetrySdk.builder()
            .setTracerProvider(tracerProvider)
            .setPropagators(ContextPropagators.create(W3CTraceContextPropagator.getInstance()))
            .build();
    }

    @Bean
    public Tracer tracer(OpenTelemetry openTelemetry) {
        return openTelemetry.getTracer("com.example.order-service");
    }
}

5. Zipkin + Fluentd 구성

5.1. Zipkin 개요

Zipkin은 분산 추적 시스템으로, 여러 서비스에 걸친 요청의 흐름을 시각화하고 지연 시간을 분석할 수 있게 해준다.

Docker로 Zipkin 실행:

docker run -d -p 9411:9411 --name zipkin openzipkin/zipkin

docker-compose.yml:

version: '3'
services:
  zipkin:
    image: openzipkin/zipkin:latest
    container_name: zipkin
    ports:
      - "9411:9411"
    environment:
      - STORAGE_TYPE=mem
    networks:
      - monitoring-network

  fluentd:
    image: fluent/fluentd:latest
    container_name: fluentd
    ports:
      - "24224:24224"
      - "24224:24224/udp"
    volumes:
      - ./fluentd/conf:/fluentd/etc
      - ./logs:/logs
    networks:
      - monitoring-network

networks:
  monitoring-network:
    driver: bridge

5.2. Zipkin 연동 구현

추적 컨텍스트 유틸리티:

@Component
@Slf4j
public class TracingContext {

    private final Tracer tracer;

    public <T> T traceWithSpan(String spanName, Supplier<T> operation) {
        Span span = tracer.spanBuilder(spanName).startSpan();

        try (Scope scope = span.makeCurrent()) {
            span.setAttribute("timestamp", System.currentTimeMillis());
            T result = operation.get();
            span.setStatus(StatusCode.OK);
            return result;

        } catch (Exception e) {
            span.setStatus(StatusCode.ERROR, e.getMessage());
            span.recordException(e);
            throw e;

        } finally {
            span.end();
        }
    }

    public void addEvent(String eventName, Attributes attributes) {
        Span currentSpan = Span.current();
        if (currentSpan != null) {
            currentSpan.addEvent(eventName, attributes);
        }
    }
}

서비스에 추적 적용:

@Service
@Slf4j
@RequiredArgsConstructor
public class OrderService {

    private final OrderRepository orderRepository;
    private final InventoryServiceClient inventoryClient;
    private final PaymentServiceClient paymentClient;
    private final TracingContext tracingContext;

    public OrderDto getOrderWithDetails(String orderId) {
        return tracingContext.traceWithSpan("OrderService.getOrderWithDetails", () -> {

            // 주문 조회 (하위 스팬)
            Order order = tracingContext.traceWithSpan("findOrder", () ->
                orderRepository.findById(orderId)
                    .orElseThrow(() -> new OrderNotFoundException(orderId)));

            tracingContext.addEvent("order_found", Attributes.builder()
                .put("order.id", orderId)
                .put("order.userId", order.getUserId())
                .build());

            // 재고 정보 조회 (원격 호출)
            Integer stock = tracingContext.traceWithSpan("call.inventory-service", () ->
                inventoryClient.getStock(order.getProductId()));

            // 결제 정보 조회 (원격 호출)
            PaymentInfo payment = tracingContext.traceWithSpan("call.payment-service", () ->
                paymentClient.getPaymentByOrderId(orderId));

            return OrderDto.builder()
                .id(order.getId())
                .userId(order.getUserId())
                .productId(order.getProductId())
                .quantity(order.getQuantity())
                .stock(stock)
                .paymentStatus(payment.getStatus())
                .build();
        });
    }
}

5.3. Fluentd 개요

Fluentd는 로그 수집기로, 다양한 소스에서 로그를 수집하여 여러 목적지로 전송할 수 있다.

Fluentd 설정 (fluentd/conf/fluent.conf):

<source>
  @type forward
  port 24224
  bind 0.0.0.0
</source>

<source>
  @type tail
  path /logs/*.log
  pos_file /fluentd/log/application.log.pos
  tag application.log
  <parse>
    @type json
  </parse>
</source>

<filter application.log>
  @type record_transformer
  <record>
    service_name ${tag_parts[0]}
    hostname ${hostname}
  </record>
</filter>

<match application.log>
  @type copy
  <store>
    @type elasticsearch
    host elasticsearch
    port 9200
    logstash_format true
    logstash_prefix fluentd
    flush_interval 5s
  </store>
  <store>
    @type file
    path /fluentd/log/archive
    compress gzip
  </store>
</match>

<match **>
  @type stdout
</match>

5.4. 로그백 설정 (Fluentd 연동)

logback-spring.xml:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <include resource="org/springframework/boot/logging/logback/defaults.xml"/>

    <springProperty scope="context" name="springAppName" source="spring.application.name"/>

    <!-- JSON 로그 포맷 (Fluentd에서 파싱하기 쉽게) -->
    <appender name="JSON" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="ch.qos.logback.classic.encoder.JsonEncoder">
            <includeCallerData>true</includeCallerData>
            <jsonGeneratorDecorator class="net.logstash.logback.decorate.CharacterEscapesJsonGeneratorDecorator"/>
            <providers>
                <timestamp>
                    <timeZone>UTC</timeZone>
                </timestamp>
                <version/>
                <message/>
                <loggerName/>
                <threadName/>
                <logLevel/>
                <stackTrace>
                    <throwableConverter class="net.logstash.logback.stacktrace.ShortenedThrowableConverter">
                        <maxDepthPerThrowable>30</maxDepthPerThrowable>
                        <maxLength>2048</maxLength>
                        <shortenedClassNameLength>20</shortenedClassNameLength>
                        <exclude>sun\\.reflect\\..*\\.invoke.*</exclude>
                        <exclude>net\\.sf\\.cglib\\.proxy\\.MethodProxy\\.invoke</exclude>
                        <rootCauseFirst>true</rootCauseFirst>
                    </throwableConverter>
                </stackTrace>
                <contextName>
                    <fieldName>service</fieldName>
                </contextName>
                <mdc>
                    <includeMdcKeyName>traceId</includeMdcKeyName>
                    <includeMdcKeyName>spanId</includeMdcKeyName>
                    <includeMdcKeyName>userId</includeMdcKeyName>
                </mdc>
            </providers>
        </encoder>
    </appender>

    <!-- Fluentd로 직접 전송 -->
    <appender name="FLUENT" class="ch.qos.logback.more.appenders.DataFluentAppender">
        <tag>application.log</tag>
        <label>normal</label>
        <remoteHost>localhost</remoteHost>
        <port>24224</port>
        <maxQueueSize>10000</maxQueueSize>
    </appender>

    <root level="INFO">
        <appender-ref ref="JSON"/>
        <appender-ref ref="FLUENT"/>
    </root>
</configuration>

5.5. 분산 로그 상관관계 설정

MDC(Mapped Diagnostic Context) 필터:

@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class MDCFilter implements Filter {

    private static final String TRACE_ID = "traceId";
    private static final String SPAN_ID = "spanId";
    private static final String USER_ID = "userId";

    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
                         FilterChain chain) throws IOException, ServletException {
        try {
            HttpServletRequest httpRequest = (HttpServletRequest) request;

            // 헤더에서 Trace ID 추출 (없으면 생성)
            String traceId = httpRequest.getHeader("X-Trace-Id");
            if (traceId == null || traceId.isEmpty()) {
                traceId = generateTraceId();
            }

            String spanId = generateSpanId();

            // MDC에 값 설정 (로그에 자동 포함)
            MDC.put(TRACE_ID, traceId);
            MDC.put(SPAN_ID, spanId);

            // 사용자 ID (인증된 경우)
            Authentication auth = SecurityContextHolder.getContext().getAuthentication();
            if (auth != null && auth.isAuthenticated()) {
                MDC.put(USER_ID, auth.getName());
            }

            // 응답 헤더에 Trace ID 추가
            HttpServletResponse httpResponse = (HttpServletResponse) response;
            httpResponse.setHeader("X-Trace-Id", traceId);

            chain.doFilter(request, response);

        } finally {
            // 요청 완료 후 MDC 정리
            MDC.clear();
        }
    }

    private String generateTraceId() {
        return UUID.randomUUID().toString().replace("-", "");
    }

    private String generateSpanId() {
        return Long.toHexString(ThreadLocalRandom.current().nextLong());
    }
}

Feign Client 인터셉터 (Trace ID 전파):

@Component
public class TraceIdFeignInterceptor implements RequestInterceptor {

    @Override
    public void apply(RequestTemplate template) {
        // 현재 MDC에서 Trace ID 가져오기
        String traceId = MDC.get("traceId");
        if (traceId != null) {
            template.header("X-Trace-Id", traceId);
        }

        String spanId = generateClientSpanId();
        template.header("X-Span-Id", spanId);
    }

    private String generateClientSpanId() {
        return Long.toHexString(ThreadLocalRandom.current().nextLong());
    }
}

RestTemplate 인터셉터:

@Component
public class TraceIdRestInterceptor implements ClientHttpRequestInterceptor {

    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body,
                                        ClientHttpRequestExecution execution) throws IOException {
        String traceId = MDC.get("traceId");
        if (traceId != null) {
            request.getHeaders().add("X-Trace-Id", traceId);
        }

        String spanId = MDC.get("spanId");
        if (spanId != null) {
            request.getHeaders().add("X-Span-Id", spanId);
        }

        return execution.execute(request, body);
    }
}

6. [실습 요약] 관측성 구현

6.1. 실습 20: Resilience4j 기본 설정

  • Circuit Breaker 설정
  • Retry 설정
  • Bulkhead 설정

6.2. 실습 21: Resilience4j + Spring Cloud 연동

  • Feign Client와 Circuit Breaker 연동
  • RestTemplate과 Resilience4j 연동
  • Actuator를 통한 상태 확인

6.3. 실습 22: OpenTelemetry + Zipkin 설정

  • OpenTelemetry 의존성 추가
  • Zipkin Exporter 설정
  • 커스텀 Span 생성

6.4. 실습 23: Fluentd 로그 수집

  • Fluentd 설치 및 설정
  • JSON 로그 포맷 구성
  • 로그 수집 및 Elasticsearch 전송

7. 정리

7.1. Resilience 패턴 선택 가이드

문제 상황  적합한 패턴
일시적 네트워크 오류 Retry
지속적 서비스 장애 Circuit Breaker
리소스 고갈 방지 Bulkhead
느린 응답 처리 Timeout
과도한 요청 제한 Rate Limiter
부분적 서비스 유지 Fallback

7.2. 관측성 구축 시 고려사항

  1. 표준화된 상관관계 ID
    • 모든 서비스가 동일한 Trace ID 형식 사용
    • 로그, 메트릭, 트레이스 연결
  2. 샘플링 전략
    • 모든 트레이스를 저장하면 비용 과다
    • 중요한 요청만 샘플링 (오류, 느린 요청 등)
  3. 성능 영향 최소화
    • 비동기 exporter 사용
    • 배치 처리로 오버헤드 감소
  4. 보안 고려
    • 민감 정보는 로그/트레이스에서 마스킹
    • 접근 제어 설정

7.3. 운영 체크리스트

  • 각 서비스에 헬스 체크 엔드포인트가 있는가?
  • Circuit Breaker 상태를 모니터링하는가?
  • 장기간 OPEN 상태인 Circuit Breaker에 알림이 가는가?
  • 분산 추적 시스템이 구축되어 있는가?
  • 로그가 중앙 집중식으로 수집되고 있는가?
  • 에러율, 응답 시간 등의 메트릭을 대시보드로 확인하는가?

7.4. 다음 글 예고

다음 글에서는 MSA의 보안과 테스트 전략에 대해 다룰 예정이다. 인증/인가 패턴, OWASP API Top 10, Rate Limiting 전략부터 단위 테스트, 통합 테스트, 계약 테스트, E2E 테스트까지 아키텍처 완성 단계의 내용을 살펴보겠다.

'MSA > MSA 아키텍처' 카테고리의 다른 글

[ADVANCED #5] 확장성과 캐시 전략 (Scalability & Redis)  (0) 2026.03.02
[ADVANCED #4] MSA 보안과 테스트 전략  (0) 2026.03.02
[ADVANCED #2] 분산 트랜잭션과 SAGA 패턴 완전 정리  (0) 2026.03.02
[ADVANCED #1] MSA 데이터 관리 전략 (DB per Service, CQRS, Sharding)  (0) 2026.03.02
[BASIC #4] 비동기 통신과 Event-Driven Architecture  (0) 2026.03.02
'MSA/MSA 아키텍처' 카테고리의 다른 글
  • [ADVANCED #5] 확장성과 캐시 전략 (Scalability & Redis)
  • [ADVANCED #4] MSA 보안과 테스트 전략
  • [ADVANCED #2] 분산 트랜잭션과 SAGA 패턴 완전 정리
  • [ADVANCED #1] MSA 데이터 관리 전략 (DB per Service, CQRS, Sharding)
h6bro
h6bro
백엔드 개발자의 기술 블로그
  • h6bro
    Jun's Tech Blog
    h6bro
  • 전체
    오늘
    어제
    • 분류 전체보기 (250) N
      • Java (18)
        • Core (9)
        • Design Pattern (9)
      • Spring (80)
        • Core (24)
        • MVC (6)
        • DB (10)
        • JPA (26)
        • Monitoring (3)
        • Security (11)
        • WebSocket (0)
      • Database (33)
        • Redis (15)
        • MySQL (18)
      • MSA (25) N
        • MSA 기본 (11)
        • MSA 아키텍처 (14) N
      • Kafka (30)
        • Core (18)
        • Connect (12)
      • ElasticSearch (11)
        • Search (11)
        • Logging (0)
      • Test (4)
        • k6 (4)
      • Docker (9)
      • CI&CD (10)
        • GitHub Actions (6)
        • ArgoCD (4)
      • Kubernetes (18)
        • Core (12)
        • Ops (6)
      • Cloud Engineering (4)
        • AWS Infrastructure (3)
        • AWS EKS (1)
        • Terraform (0)
      • Project (8)
        • LinkFolio (1)
        • Secondhand Market (7)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

    • Cloud Engineering 포스팅 정리
  • 인기 글

  • 태그

    ㅈ
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
h6bro
[ADVANCED #3] MSA 회복성, 관측성, 모니터링 전략
상단으로

티스토리툴바