본문 바로가기

자바 심화/TIL

MSA - 기초 3

MSA 기초

 

MSA - 기초 2

MSA MSA - 기초 1개요추후 MSA를 사용한 프로젝트를 진행하기 앞서 MSA가 무엇이고 어떻게 사용하는지에 대해 배워보려고 한다.다만, MSA에 대한 이론부터 완벽하게 익히고 들어가기에는 너무 오래

eleunadeu.tistory.com

 

개요

Spring Cloud의 서킷 브레이커, API Gateway에 대한 간단한 정리와 함께 사용 방법을 익힌다.

 

 

서킷 브레이커(Circuit Breaker)

분산 시스템에서 특정 서비스 호출에 실패가 반복될 경우, 시스템 전체로 장애가 전파되는 것을 방지하기 위해 제공되는 패턴으로 Spring Cloud에서는 Hystrix와 Resilience4j를 통해 구현할 수 있다.

1. Hystrix

 

  • Netflix OSS에서 개발한 서킷 브레이커 라이브러리
  • 주요 기능:
    • 서킷 열기(Open): 호출 실패율이 일정 임계치를 넘을 경우, 서킷을 열고 호출을 차단.
    • Fallback: 서킷이 열리거나 호출 실패 시, 대체 동작(예: 기본값 반환) 수행.
    • 스레드 격리: 호출을 별도 스레드에서 실행해 장애가 확산되지 않도록 격리.
  • Spring Cloud Netflix 프로젝트를 통해 사용 가능.
  • 현재는 유지보수 중단(Deprecated) 상태로, 새로운 프로젝트에서는 권장되지 않음.

2. Resilience4j

 

 

  • Hystrix의 대안으로 개발된 경량 서킷 브레이커 라이브러리.
  • 주요 특징:
    • 함수형 프로그래밍 지원: 람다 표현식 기반 API 제공.
    • 유연성: 서킷 브레이커 외에도 Rate Limiter, Retry, Bulkhead 등의 기능 제공.
    • 가벼움: Java 8 이상에서 작동하며, 외부 종속성이 적음.
    • 모니터링 통합: Prometheus, Micrometer와 쉽게 연동 가능.
  • Spring Cloud Circuit Breaker 프로젝트에서 기본 구현체로 사용.

 

 

Spring Cloud 사용 시 권장 사항

 

  • 새로운 프로젝트: Resilience4j 사용.
  • 기존 Hystrix 프로젝트 유지: 필요하면 Resilience4j로 마이그레이션 검토.

 

서킷 브레이커 상태

  • Closed(클로즈드): 
    • 기본 상태로 모든 요청을 통과 시킴
    • 호출 실패 시 실패 카운터 증가
    • 실패율이 설정된 임계값 초과 시 오픈 상태로 전환
  • Open(오픈):
    • 오픈 상태로 전환 시 모든 요청을 즉시 실패로 처리
    • 이 상태에서 요청이 실패하지 않고 바로 에러 응답을 반환
    • 설정된 대기 시간 경과 후 하프-오픈 상태로 전환
  • Half-Open(하프-오픈):
    • 오픈 상태에서 대기 시간 경과 후 하프-오픈 상태로 전환
    • 제한된 수의 요청을 허용해 시스템이 정상 상태로 복구되었는지 확인
    • 요청 성공 시 클로즈드 상태로 전환되고, 실패 시 오픈 상태로 전환
  • Fallback: 호출 실패 시 대체 로직을 제공하여 시스템 안정성 확보

 

Resilience4j 사용 방법

build.gradle

implementation 'io.github.resilience4j:resilience4j-spring-boot3:2.2.0'
implementation 'org.springframework.boot:spring-boot-starter-aop'

 

 

  • Spring Boot 3 이상의 버전에서 위 dependency 사용

yml 파일 설정

resilience4j:
  circuitbreaker:
    configs:
      default:  # 기본 구성 이름
        registerHealthIndicator: true  # 애플리케이션 헬스 체크에 서킷 브레이커 상태를 추가해 모니터링 가능
        # 서킷 브레이커가 동작할 때 사용할 슬라이딩 윈도우의 타입을 설정
        # COUNT_BASED: 마지막 N번의 호출 결과를 기반으로 상태를 결정
        # TIME_BASED: 마지막 N초 동안의 호출 결과를 기반으로 상태를 결정
        slidingWindowType: COUNT_BASED  # 슬라이딩 윈도의 타입을 호출 수 기반(COUNT_BASED)으로 설정
        # 슬라이딩 윈도우의 크기를 설정
        # COUNT_BASED일 경우: 최근 N번의 호출을 저장
        # TIME_BASED일 경우: 최근 N초 동안의 호출을 저장
        slidingWindowSize: 5  # 슬라이딩 윈도우의 크기를 5번의 호출로 설정
        minimumNumberOfCalls: 5  # 서킷 브레이커가 동작하기 위해 필요한 최소한의 호출수를 5로 설정
        slowCallRateThreshold: 100  # 느린 호출의 비율이 이 임계값(100%)를 초과하면 서킷 브레이커가 동작
        slowCallDurationThreshold: 60000  # 느린 호출의 기준 시간(밀리초)으로, 60초 이상 걸리면 느린 호출로 간주
        failureRateThreshold: 50  # 실패율이 이 임계값(50%)을 초과하면 서킷 브레이커가 동작
        permittedNumberOfCallsInHalfOpenState: 3  # 서킷 브레이커가 Half-open 상태에서 허용하는 최대 호출 수를 3으로 설정
        # 서킷 브레이커가 Open 상태에서 Half-open 상태로 전환되기 전에 기다리는 시간
        waitDurationInOpenState: 20s  # Open 상태에서 Half-open 상태로 전환되기 전에 대기하는 시간을 20초로 설정

management:
  endpoints:
    web:
      exposure:
        include: prometheus
  prometheus:
    metrics:
      export:
        enabled: true

 

 

Fallback 설정

@Service
public class MyService {

    @CircuitBreaker(name = "myService", fallbackMethod = "fallbackMethod")
    public String myMethod() {
        // 외부 서비스 호출
        return externalService.call();
    }

    public String fallbackMethod(Throwable t) {
        return "Fallback response";
    }
}
  • 외부 서비스 호출 시 대체 로직을 제공하는 메서드
  • 장점
    • 시스템 안정성 즈가, 장애 발생 시 사용자에게 일정한 응답 제공
    • 장애가 다른 서비스에 전파되는 것을 방지

 

Dashboard

dependencies {
    implementation 'io.github.resilience4j:resilience4j-micrometer'
    implementation 'io.micrometer:micrometer-registry-prometheus'
    implementation 'org.springframework.boot:spring-boot-starter-actuator'
}
  • 위 dependency 추가 후 애플리케이션을 실행하고, http://${hostname}:${port}/actuator/prometheus 에 접속해 서킷 브레이커 상태를 실시간 모니터링 할 수 있다.

 

실습 코드

@RestController
@RequiredArgsConstructor
public class ProductController {
    private final ProductService productService;

    @GetMapping("/product/{id}")
    public Product getProduct(@PathVariable("id") String id) {
        return productService.getProductDetails(id);
    }
}
  • product의 id를 서비스 레이어로 전달
@Service
@RequiredArgsConstructor
public class ProductService {

    private final Logger log = LoggerFactory.getLogger(getClass());
    private final CircuitBreakerRegistry circuitBreakerRegistry;

    @PostConstruct
    public void registerEventListener() {
        circuitBreakerRegistry.circuitBreaker("productService").getEventPublisher()
                .onStateTransition(event -> log.info("#######CircuitBreaker State Transition: {}", event)) // 상태 전환 이벤트 리스너
                .onFailureRateExceeded(event -> log.info("#######CircuitBreaker Failure Rate Exceeded: {}", event)) // 실패율 초과 이벤트 리스너
                .onCallNotPermitted(event -> log.info("#######CircuitBreaker Call Not Permitted: {}", event)) // 호출 차단 이벤트 리스너
                .onError(event -> log.info("#######CircuitBreaker Error: {}", event)); // 오류 발생 이벤트 리스너
    }


    @CircuitBreaker(name = "productService", fallbackMethod = "fallbackGetProductDetails")
    public Product getProductDetails(String productId) {
        log.info("###Fetching product details for productId: {}", productId);
        if ("111".equals(productId)) {
            log.warn("###Received empty body for productId: {}", productId);
            throw new RuntimeException("Empty response body");
        }
        return new Product(
                productId,
                "Sample Product"
        );
    }

    public Product fallbackGetProductDetails(String productId, Throwable t) {
        log.error("####Fallback triggered for productId: {} due to: {}", productId, t.getMessage());
        return new Product(
                productId,
                "Fallback Product"
        );
    }


    // 이벤트 설명 표
    // +---------------------------+-------------------------------------------------+--------------------------------------------+
    // | 이벤트                      | 설명                                             | 로그 출력                                    |
    // +---------------------------+-------------------------------------------------+--------------------------------------------+
    // | 상태 전환 (Closed -> Open)   | 연속된 실패로 인해 서킷 브레이커가 오픈 상태로 전환되면 발생  | CircuitBreaker State Transition: ...       |
    // | 실패율 초과                  | 설정된 실패율 임계치를 초과하면 발생                     | CircuitBreaker Failure Rate Exceeded: ...  |
    // | 호출 차단                    | 서킷 브레이커가 오픈 상태일 때 호출이 차단되면 발생         | CircuitBreaker Call Not Permitted: ...     |
    // | 오류 발생                    | 서킷 브레이커 내부에서 호출이 실패하면 발생               | CircuitBreaker Error: ...                  |
    // +---------------------------+-------------------------------------------------+--------------------------------------------+


    // +------------------------------------------+-------------------------------------------+-----------------------------------------------------------------+
    // | 이벤트                                    | 설명                                        | 로그 출력                                                         |
    // +------------------------------------------+-------------------------------------------+-----------------------------------------------------------------+
    // | 메서드 호출                                | 제품 정보를 얻기 위해 메서드를 호출                | ###Fetching product details for productId: ...                  |
    // | (성공 시) 서킷 브레이커 내부에서 호출 성공        | 메서드 호출이 성공하여 정상적인 응답을 반환          |                                                                 |
    // | (실패 시) 서킷 브레이커 내부에서 호출 실패        | 메서드 호출이 실패하여 예외가 발생                 | #######CircuitBreaker Error: ...                                |
    // | (실패 시) 실패 횟수 증가                      | 서킷 브레이커가 실패 횟수를 증가시킴               |                                                                 |
    // | (실패율 초과 시) 실패율 초과                   | 설정된 실패율 임계치를 초과하면 발생               | #######CircuitBreaker Failure Rate Exceeded: ...                |
    // | (실패율 초과 시) 상태 전환 (Closed -> Open)   | 연속된 실패로 인해 서킷 브레이커가 오픈 상태로 전환됨   | #######CircuitBreaker State Transition: Closed -> Open at ...  |
    // | (오픈 상태 시) 호출 차단                      | 서킷 브레이커가 오픈 상태일 때 호출이 차단됨         | #######CircuitBreaker Call Not Permitted: ...                   |
    // | (오픈 상태 시) 폴백 메서드 호출                 | 메서드 호출이 차단될 경우 폴백 메서드 호출          | ####Fallback triggered for productId: ... due to: ...           |
    // +------------------------------------------+-------------------------------------------+-----------------------------------------------------------------+



}
  • 서킷 브레이커를 통해 product 서비스 호출을 보호해 장애 확산을 방지하고, 오류 발생 시 대체 동작(Fallback)을 제공.
  • 로깅을 통해 서킷 브레이커의 상태 전환, 실패율 초과, 호출 차단 등 주요 이벤트를 기록

실습 결과

  • 오류가 발생 했을 때 서킷 브레이커가 클로즈드에서 오픈 상태로 전환되고, yml을 통해 설정한 시간이 지난 후 하프-오픈 상태로 전환되는 것을 확인할 수 있다.

 

API 게이트웨이(API Gateway)

클라이언트의 요청을 받아 백엔드 서비스로 라우팅하고, 다양한 부가 기능을 제공하는 중간 서버.

 

주요 기능

  1. 라우팅(Routing)
    • 요청 URL, HTTP Method, 헤더 등을 기반으로 적절한 서비스로 요청을 전달.
    • YAML이나 Java 코드를 사용해 라우팅 규칙을 간단히 정의 가능.
  2. 필터링(Filter)
    • 요청 또는 응답을 가로채 변환하거나 공통 작업 수행.
    • 예: 인증, 로깅, CORS 설정, 요청 수정, 응답 압축 등.
  3. 부하 분산(Load Balancing)
    • Spring Cloud LoadBalancer와 연동해 여러 서비스 인스턴스 간 트래픽 분산 가능.
  4. Fault Tolerance
    • Resilience4j 등과 통합해 서킷 브레이커, Rate Limiter, Retry 기능 제공.
  5. 보안(Security)
    • JWT 인증, OAuth2 인증 서버 연동 등 다양한 인증 및 권한 부여 기능.
  6. 모니터링 및 추적
    • Spring Boot Actuator, Sleuth, Zipkin 등과 통합해 분산 추적모니터링 가능.

 

Spring Cloud Gateway 장점

 

  • Spring 생태계 통합: Spring Boot와 자연스럽게 연동.
  • 비동기 처리: Reactor 기반으로 고성능 비동기 처리 가능.
  • 확장성: 커스텀 필터 및 라우터 추가 가능.
  • 경량: 기존 Zuul 1.x 대비 경량화되고 성능 향상.

 

 

API Gateway 사용?

  • MSA에서 중앙 진입점으로 인증, 라우팅, 로깅, 부하 분산 등 공통 기능을 관리할 때 사용

 

API Gateway 사용방법

build.gradle

implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.cloud:spring-cloud-starter-gateway'
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
  • 위 dependency 사용

 

yml(라우팅 설정)

server:
  port: 19091  # 게이트웨이 서비스가 실행될 포트 번호

spring:
  main:
    web-application-type: reactive  # Spring 애플리케이션이 리액티브 웹 애플리케이션으로 설정됨
  application:
    name: gateway-service  # 애플리케이션 이름을 'gateway-service'로 설정
  cloud:
    gateway:
      routes:  # Spring Cloud Gateway의 라우팅 설정
        - id: order-service  # 라우트 식별자
          uri: lb://order-service  # 'order-service'라는 이름으로 로드 밸런싱된 서비스로 라우팅
          predicates:
            - Path=/order/**  # /order/** 경로로 들어오는 요청을 이 라우트로 처리
        - id: product-service  # 라우트 식별자
          uri: lb://product-service  # 'product-service'라는 이름으로 로드 밸런싱된 서비스로 라우팅
          predicates:
            - Path=/product/**  # /product/** 경로로 들어오는 요청을 이 라우트로 처리
      discovery:
        locator:
          enabled: true  # 서비스 디스커버리를 통해 동적으로 라우트를 생성하도록 설정

eureka:
  client:
    service-url:
      defaultZone: http://localhost:19090/eureka/  # Eureka 서버의 URL을 지정

 

Gateway 필터링

필터 종류

  • Global Filter: 모든 요청에 대해 작동하는 필터
  • Gateway Filter: 특정 라우트에만 적용되는 필터

필터 구현

  • Global 또는 Gateway 필터 인터페이스 구현 후 filter 메서드 오버라이드

주요 객체

  • Mono: 리액티브 프로그래밍에서 0 또는 1개의 데이터를 비동기 처리(Mono<Void>는 아무 데이터도 반환 x)
  • ServerWebExchange: HTTP 요청과 응답을 캡슐화한 객체로 get 메서드를 통해 요청과 응답을 가져온다.
  • GatewayFilterChain: 여러 필터를 체인처럼 연결, chain.filter(exchange)로 다음 필터로 요청을 전달.

시점 별 종류

  • Pre: 요청 처리 전에 실행되며, 요청을 가로채 필요한 작업 수행 후 체인의 다음 필터로 요청을 전달(then 메서드 x)
  • Post: 요청이 처리된 후, 응답이 반환되기 전에 실행되며, 체인의 다음 필터가 완료된 후 실행되야 하는 추가적인 작업 수행(then 메서드 사용)

 

실습 코드

Pre Filter

@Component
public class CustomPreFilter implements GlobalFilter, Ordered {
    private static final Logger logger = Logger.getLogger(CustomPreFilter.class.getName());

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        logger.info("Pre Filter : Request URI:" + request.getURI());
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE;
    }
}
  • 가장 먼저 실행되도록 우선 순위를 최상위로 지정하고 HTTP 요청이 처리되기 전 추가 로직(요청 URI 확인)을 삽입
  • Mono를 사용해 비동기 방식으로 요청 처리

Post Filter

@Component
public class CustomPostFilter implements GlobalFilter, Ordered {
    private static final Logger logger = Logger.getLogger(CustomPreFilter.class.getName());

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        return chain.filter(exchange).then(Mono.fromRunnable(() -> {
            ServerHttpResponse response = exchange.getResponse();
            logger.info("Post Filter : Response status code is " + response.getStatusCode());
        }));
    }

    @Override
    public int getOrder() {
        return Ordered.LOWEST_PRECEDENCE;
    }
}
  • 필터 체인에서 가장 마지막에 실행 되도록 설정, 요청 처리 후 응답에 대한 추가 작업(응답의 Status 코드 확인)을 수행
  • Mono를 사용해 비동기 방식으로 요청 처리

 

실습 결과

  • 각 서비스를 호출할 때 마다 Pre/Post Filter가 동작하는 것을 확인할 수 있다.

 

정리

  • MSA에서 서킷 브레이커를 통해 오류 발생 시 대체 동작을 수행하도록 하는 방법을 배웠다.
  • API 게이트웨이를 사용해 백엔드 서비스로 보내는 요청을 관리하고 Pre/Post 필터를 사용해 필요한 동작을 수행하는 방법을 배웠다.

'자바 심화 > TIL' 카테고리의 다른 글

MSA - 기초 5  (0) 2024.11.27
MSA - 기초 4  (0) 2024.11.26
MSA - 기초 2  (1) 2024.11.22
MSA - 기초 1  (1) 2024.11.21
클린 코드 -2  (1) 2024.11.20