MSA 구축 (10) - 서비스 장애 대응 Circuit Breaker 구현(feat. Resilience4J)

오형상·2024년 12월 13일
0

Ficket

목록 보기
10/27

개요
MSA(Microservices Architecture) 환경에서는 여러 서비스 간의 통신이 빈번히 이루어집니다. 하지만 특정 서비스에서 장애가 발생하거나 응답 시간이 지연되면 호출하는 서비스에도 영향을 미쳐 전체 시스템의 장애로 이어질 수 있습니다. 이 글에서는 Resilience4J의 Circuit Breaker 패턴을 적용하여 User 서버와 Ticket 서버 간 통신의 안정성을 높이는 방법을 소개합니다.


User 서버와 Ticket 서버 간의 통신

구조

  • User 서버: 유저 관리 및 인증/인가를 담당하는 서비스.
  • Ticketing 서버: 유저가 구매한 티켓 정보를 처리하고 반환하는 서비스.

요청 플로우

  1. 사용자 요청: User 서버에서 사용자가 자신의 티켓 정보를 요청.
  2. 서비스 호출: User 서버가 OpenFeign을 통해 Ticket 서버에 요청.
  3. 응답 처리:
    • 정상적으로 티켓 정보를 반환하면 사용자에게 데이터를 제공.
    • 장애 발생 시 Circuit Breaker가 요청을 차단하고 대체 데이터를 반환.

1. Resilience4J 구성 요소

CircuitBreaker

  • 서비스 호출 결과를 저장하고 상태를 관리합니다.

  • 상태:
    • CLOSED: 모든 요청을 정상 처리.
    • OPEN: 요청을 즉시 차단.
    • HALF_OPEN: 일부 요청을 테스트하여 상태를 다시 결정.

Fallback

  • 장애 발생 시 기본값이나 미리 준비된 응답을 반환합니다.

Bulkhead

  • 스레드 풀을 격리하여 서비스 간 장애 전파를 방지합니다.

TimeLimiter

  • 요청 처리 시간을 제한하고 초과 시 요청을 중단.

2. 구현: User 서버와 Ticket 서버 간의 통신에 Circuit Breaker 적용

2-1. 의존성 추가

User 서버에 Resilience4J 의존성을 추가합니다.

implementation 'org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j'

2-2. 설정 파일 작성

application.yml 파일에 Circuit Breaker와 Time Limiter 설정을 추가합니다.

따로 설정하지 않은 이유는 추후 설명합니다.

spring:  
  cloud:
    openfeign:
      circuitbreaker:
        enabled: true
	  client:
        config:
          default:
            connectTimeout: 5000
            readTimeout: 5000

resilience4j:
  circuitbreaker:
    configs:
      default:
        failure-rate-threshold: 50
        slow-call-rate-threshold: 80
        slow-call-duration-threshold: 5s
        permitted-number-of-calls-in-half-open-state: 3
        max-wait-duration-in-half-open-state: 0
        sliding-window-type: COUNT_BASED
        sliding-window-size: 10
        minimum-number-of-calls: 10
        wait-duration-in-open-state: 10s
  timelimiter:
    configs:
      default:
        timeoutDuration: 7s
        cancelRunningFuture: true

2-3. FeignClient 구성

Ticket 서버와 통신하는 FeignClient에 Circuit Breaker와 Fallback을 추가합니다.

@FeignClient(name = "ticketing-service", fallbackFactory = TicketingServiceClientFallbackFactory.class)
public interface TicketingServiceClient {

    @GetMapping("/api/v1/ticketing/order/my")
    List<TicketInfoDto> getMyTickets(@RequestParam Long userId);

}

2-4. Fallback 클래스 작성

Ticket 서버 장애 발생 시 기본 응답을 반환하는 Fallback 클래스를 작성합니다.

@Component
public class TicketingServiceClientFallbackFactory implements FallbackFactory<TicketingServiceClient> {

    @Override
    public TicketingServiceClient create(Throwable cause) {
        return new TicketingServiceClient() {
            @Override
            public List<TicketInfoDto> getMyTickets(Long userId) {
                return Collections.emptyList();
            }
        };
    }
}

2-5. 예외 처리

서킷이 OPEN 상태로 바뀌면 더 이상 요청이 전달되지 않는다. 대신 요청을 차단하고 바로 CallNotPermittedException 예외를 발생시킨다. 그러므로 각각의 예외 처리 방법에 맞게 CallNotPermittedException 예외를 처리해주어야 한다.

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(CallNotPermittedException.class)
    public ResponseEntity<String> handleCircuitBreakerException(CallNotPermittedException ex) {
        return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
                .body("티켓 조회 서비스가 현재 사용 불가 상태입니다. 나중에 다시 시도해주세요.");
    }
}

2-6. 사용자 정의 Circuit Breaker 이름 생성

구현 목적

  • URL의 호스트명메서드명을 기반으로 Circuit Breaker 이름을 생성.
  • 호스트별로 고유한 Circuit Breaker를 관리하여 장애를 격리.
  • 잘못된 URL의 경우 기본 Circuit Breaker 이름 (cb_default) 반환.

코드 구현

package com.example.ficketuser.global.config.fegin;

import feign.Target;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.openfeign.CircuitBreakerNameResolver;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;

@Slf4j
@Component
public class CustomCircuitBreakerNameResolver implements CircuitBreakerNameResolver {

    @Override
    public String resolveCircuitBreakerName(String feignClientName, Target<?> target, Method method) {
        String url = target.url();
        try {
            String host = new URL(url).getHost();
            String methodName = method.getName();
            return String.format("cb_%s_%s", host, methodName);
        } catch (MalformedURLException e) {
            log.error("MalformedURLException 발생: {}", url, e);
            return "cb_default";
        }
    }
}

4. 테스트 시나리오

  1. 정상 요청

    • Ticket 서버가 정상적으로 작동하며 사용자가 티켓 데이터를 정상 조회.

  2. 서버 장애

    • Ticketing 서버 장애 시 Fallback이 작동하여 기본 데이터를 반환.
    public List<TicketInfoDto> getMyTickets(Long userId) {
        throw new RuntimeException("실패 테스트");
    }

  3. 지연 요청

    • 요청이 설정된 임계값(7초)을 초과하면 Circuit Breaker가 요청을 차단.
    public List<TicketInfoDto> getMyTickets(Long userId) {
        try {
            // 10초 대기 (시간 지연 시뮬레이션)
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new IllegalStateException("Thread sleep interrupted", e);
        }
    
        // 마이 티켓 조회 로직
    }


5. 결론

Resilience4J를 활용하여 User 서버와 Ticket 서버 간의 통신에 Circuit Breaker를 적용함으로써:

  • 장애가 발생하더라도 서비스 전체의 중단을 방지.
  • 사용자 경험을 유지하며 안정성을 보장.

Reference

0개의 댓글