Search
Duplicate

스프링 복원력

수업
Spring MSA
주제
5 more properties
새로운 도전에 대해 이야기할 시간이 되었습니다. 이번 도전에서는 마이크로서비스의 복원력에 대해 논의할 것입니다. 복원력이란 어려운 상황을 견디고 반등할 수 있는 능력을 의미합니다. 우리 인류는 코로나바이러스와 같은 어려운 시기를 겪었지만, 이제 정상적인 생활로 돌아왔습니다. 마이크로서비스 역시 네트워크 문제나 성능 문제와 같은 어려운 시기를 견디고 복원할 수 있도록 만들어야 합니다.
주요 질문들:
1.
마이크로서비스 네트워크 내에서 전파되는 실패를 어떻게 피할 수 있을까요?
2.
어떻게 하면 실패를 우아하게 처리하고 fallback 메커니즘을 활용할 수 있을까요?
3.
자체 치유가 가능한 서비스는 어떻게 만들 수 있을까요?
이러한 질문에 답하기 위해 여러 복원력 패턴이 존재합니다. 과거에는 Netflix에서 개발한 Hystrix 라이브러리가 널리 사용되었지만, 2018년에 유지 보수 모드로 들어가 더 이상 활발하게 개발되지 않았습니다. 대신, Resilience4j라는 새로운 라이브러리가 대중적인 인기를 얻게 되었고, 개발자들에게 필요한 복원력 관련 기능을 제공했습니다.
Resilience4j란? Resilience4j는 자바 기반 애플리케이션을 위한 가벼운 내결함성 라이브러리로서, 함수형 프로그래밍을 위해 설계되었습니다. 이 라이브러리는 서킷 브레이커, 폴백, 재시도, 속도 제한, 벌크헤드 등 여러 패턴을 제공합니다.
Resilience4j 공식 웹사이트에서 이 라이브러리에 대한 자세한 정보를 찾을 수 있습니다. 이 사이트는 Resilience4j를 시작하는 방법, 스프링 부트 2나 3 버전, 그리고 마이크로노트 프레임워크와의 통합 방법 등을 포함한 다양한 정보를 제공합니다.
Resilience4j는 함수형 프로그래밍을 위해 설계된 가벼운 내결함성 라이브러리로, 다음과 같은 패턴들을 제공합니다:
1.
Circuit Breaker: 서비스가 실패할 때 요청을 중단시키는 데 사용됩니다.
2.
Fallback: 실패하는 요청에 대한 대체 경로를 제공합니다.
3.
Retry: 서비스가 일시적으로 실패했을 때 재시도를 수행하는 데 사용됩니다.
4.
Rate Limit: 서비스가 일정 시간 내에 받는 호출 수를 제한합니다.
5.
Bulkhead: 서비스로의 동시 요청 수를 제한하여 과부하를 방지합니다.
마이크로서비스 아키텍처에서는 여러 독립적인 서비스들이 각자의 역할을 하며 상호작용합니다. 예를 들어, 고객의 계좌, 대출, 카드 정보를 한 번에 보여주는 서비스가 있다고 해봅시다. 여기서 계좌 서비스는 자체 정보뿐만 아니라 대출 및 카드 서비스로부터 정보를 받아오는 역할을 합니다.
문제 상황: 만약 카드 서비스에 문제가 발생해 응답이 지연되거나 실패하면, 계좌 서비스는 카드 서비스로부터의 응답을 기다리며 대기 상태에 빠질 수 있습니다. 이는 계좌 서비스 자신의 성능을 저하시키고, 연쇄적으로 전체 시스템에 부정적인 영향을 줄 수 있습니다.
서킷 브레이커 패턴: 이러한 문제를 방지하기 위해 '서킷 브레이커' 패턴을 사용합니다. 서킷 브레이커는 전기 회로에서 과부하가 발생할 때 전류를 차단하는 장치의 원리를 소프트웨어에 적용한 것입니다. 즉, 특정 서비스에 문제가 생겼을 때, 이를 자동으로 감지하고 해당 서비스로의 요청을 일시적으로 차단하여 시스템의 나머지 부분이 정상적으로 작동할 수 있도록 돕습니다.
예를 들어, 계좌 서비스가 카드 서비스로부터 응답을 받지 못하면 서킷 브레이커는 일정 시간 후에 다시 요청을 시도할 수 있도록 설정할 수 있습니다. 만약 계속해서 실패한다면, 서킷 브레이커는 요청을 차단하고 대체 메커니즘(폴백 로직)을 실행합니다. 폴백 로직은 기본적인 정보를 반환하거나 캐시에서 정보를 제공하는 등의 방법을 포함할 수 있습니다.
자가 치유 능력: 또한, 마이크로서비스가 잠시 문제를 겪고 있을 때 자동으로 회복하도록 만들 수도 있습니다. 이는 서비스가 다시 정상으로 돌아올 수 있도록 기회를 주는 재시도 로직이나, 너무 오랜 시간 기다리지 않고 빠르게 실패 처리를 하여 시스템에 부하를 줄이는 타임아웃 설정을 포함할 수 있습니다.
이렇게 서킷 브레이커와 같은 패턴을 사용함으로써, 마이크로서비스 아키텍처를 보다 견고하고 신뢰할 수 있게 만들어, 시스템 전체가 개별 서비스의 실패에 의해 영향을 받지 않도록 할 수 있습니다. 이는 사용자에게 안정적인 서비스를 제공하고, 시스템 운영에 있어 예측 가능성과 안정성을 높이는 데 중요한 역할을 합니다.
서킷 브레이커 패턴은 마이크로서비스와 같은 분산 시스템에서 과부하로부터 시스템을 보호하고 안정성을 유지하는 방법입니다. 이 패턴은 네트워크 문제나 서비스의 장애 등으로 인해 발생할 수 있는 연쇄적 실패를 방지하는 데 도움이 됩니다.
서킷 브레이커 패턴은 다음과 같은 원리로 작동합니다:
1.
감시: 서킷 브레이커는 특정 서비스로의 요청을 감시합니다.
2.
중단: 서비스가 과도하게 느리게 응답하거나 실패할 경우, 서킷 브레이커는 자동으로 이 서비스로의 요청을 중단시킵니다.
3.
격리: 이는 과부하가 다른 시스템의 부분으로 확산되는 것을 막아줍니다.
4.
복구 시도: 일정 시간이 지난 후, 서킷 브레이커는 서비스가 복구되었는지 확인하기 위해 소량의 요청을 시도합니다.
5.
재개: 만약 서비스가 정상적으로 응답한다면, 서킷 브레이커는 다시 전체 요청을 허용합니다.
이러한 방식으로 서킷 브레이커는 시스템 전체가 특정 서비스의 문제로 인해 마비되는 것을 방지하고, 서비스가 문제를 해결하고 정상 상태로 돌아올 수 있는 시간을 제공합니다.
여기서 중요한 것은 서킷 브레이커가 문제를 해결하는 것이 아니라, 문제가 발생했을 때 이를 감지하고 신속하게 대응하여 시스템의 나머지 부분을 보호하며, 문제가 해결될 수 있는 시간을 제공하는 것입니다.
1.
CLOSED 상태: 서킷 브레이커는 초기에 CLOSED 상태로 시작하여 모든 클라이언트 요청을 받습니다.
2.
OPEN 상태: 서킷 브레이커가 설정된 실패율 임계값 이상의 요청이 실패하는 것을 감지하면 OPEN 상태가 되어 회로를 열고 요청이 즉시 실패하도록 합니다. 이는 요청이 빠르게 실패하게 하여 서비스에 대한 추가 요청을 중단시키고, 장애가 다른 부분으로 확산되는 것을 방지합니다.
3.
HALF-OPEN 상태: 설정된 대기 시간이 지난 후, 서킷 브레이커는 일부 요청을 허용하여 문제가 해결되었는지 확인합니다. 이 단계에서의 요청 결과에 따라 서킷 브레이커는 다시 CLOSED 상태로 돌아가거나, 문제가 지속되면 OPEN 상태를 유지합니다.
resilience4j: circuitbreaker: configs: default: # sliding-window-size: 회로 차단기가 고장율을 계산하기 위해 사용하는 호출의 수입니다. sliding-window-size: 10 # permitted-number-of-calls-in-half-open-state: 회로 차단기가 'half-open' 상태일 때 # 허용하는 최대 호출 수입니다. 이 수를 초과하여 실패가 계속 발생하면 다시 'open' 상태가 됩니다. permitted-number-of-calls-in-half-open-state: 2 # failure-rate-threshold: 회로 차단기를 'open' 상태로 전환하기 위한 실패율 임계값(퍼센트)입니다. # 이 값이 초과하면 회로 차단기는 실패한 상태로 간주되어 'open' 상태가 됩니다. failure-rate-threshold: 50 # wait-duration-in-open-state: 회로 차단기가 'open' 상태로 유지되는 최소 시간(밀리초)입니다. # 이 시간 동안 모든 호출은 차단됩니다. 이 시간이 지나면 회로 차단기는 'half-open' 상태로 전환됩니다. wait-duration-in-open-state: 10000
YAML
복사
// 20240429172842 // http://localhost:8072/actuator/circuitbreakers { "circuitBreakers": { "accountsCircuitBreaker": { "failureRate": "-1.0%", "slowCallRate": "-1.0%", "failureRateThreshold": "50.0%", "slowCallRateThreshold": "100.0%", "bufferedCalls": 3, "failedCalls": 0, "slowCalls": 0, "slowFailedCalls": 0, "notPermittedCalls": 0, "state": "CLOSED" } } }
JSON
복사
/ 20240429173725 // http://localhost:8072/actuator/circuitbreakerevents?name=accountsCircuitBreaker { "circuitBreakerEvents": [ { "circuitBreakerName": "accountsCircuitBreaker", "type": "ERROR", "creationTime": "2024-04-29T17:34:46.433287+09:00[Asia/Seoul]", "errorMessage": "io.netty.channel.AbstractChannel$AnnotatedConnectException: Connection refused: /192.168.1.236:8080", "durationInMs": 88, "stateTransition": null }, { "circuitBreakerName": "accountsCircuitBreaker", "type": "SUCCESS", "creationTime": "2024-04-29T17:34:47.975663+09:00[Asia/Seoul]", "errorMessage": null, "durationInMs": 81, "stateTransition": null }, { "circuitBreakerName": "accountsCircuitBreaker", "type": "SUCCESS", "creationTime": "2024-04-29T17:34:49.282450+09:00[Asia/Seoul]", "errorMessage": null, "durationInMs": 15, "stateTransition": null }, { "circuitBreakerName": "accountsCircuitBreaker", "type": "SUCCESS", "creationTime": "2024-04-29T17:34:50.061051+09:00[Asia/Seoul]", "errorMessage": null, "durationInMs": 6, "stateTransition": null }, { "circuitBreakerName": "accountsCircuitBreaker", "type": "ERROR", "creationTime": "2024-04-29T17:35:34.023026+09:00[Asia/Seoul]", "errorMessage": "java.util.concurrent.TimeoutException: Did not observe any item or terminal signal within 1000ms in 'circuitBreaker' (and no fallback has been configured)", "durationInMs": 1000, "stateTransition": null }, { "circuitBreakerName": "accountsCircuitBreaker", "type": "ERROR", "creationTime": "2024-04-29T17:36:48.108177+09:00[Asia/Seoul]", "errorMessage": "java.util.concurrent.TimeoutException: Did not observe any item or terminal signal within 1000ms in 'circuitBreaker' (and no fallback has been configured)", "durationInMs": 1000, "stateTransition": null }, { "circuitBreakerName": "accountsCircuitBreaker", "type": "ERROR", "creationTime": "2024-04-29T17:36:49.846588+09:00[Asia/Seoul]", "errorMessage": "java.util.concurrent.TimeoutException: Did not observe any item or terminal signal within 1000ms in 'circuitBreaker' (and no fallback has been configured)", "durationInMs": 1000, "stateTransition": null }, { "circuitBreakerName": "accountsCircuitBreaker", "type": "ERROR", "creationTime": "2024-04-29T17:36:58.490660+09:00[Asia/Seoul]", "errorMessage": "java.util.concurrent.TimeoutException: Did not observe any item or terminal signal within 1000ms in 'circuitBreaker' (and no fallback has been configured)", "durationInMs": 1000, "stateTransition": null }, { "circuitBreakerName": "accountsCircuitBreaker", "type": "ERROR", "creationTime": "2024-04-29T17:37:10.857355+09:00[Asia/Seoul]", "errorMessage": "java.util.concurrent.TimeoutException: Did not observe any item or terminal signal within 1000ms in 'circuitBreaker' (and no fallback has been configured)", "durationInMs": 1000, "stateTransition": null }, { "circuitBreakerName": "accountsCircuitBreaker", "type": "ERROR", "creationTime": "2024-04-29T17:37:13.867986+09:00[Asia/Seoul]", "errorMessage": "java.util.concurrent.TimeoutException: Did not observe any item or terminal signal within 1000ms in 'circuitBreaker' (and no fallback has been configured)", "durationInMs": 1000, "stateTransition": null }, { "circuitBreakerName": "accountsCircuitBreaker", "type": "FAILURE_RATE_EXCEEDED", "creationTime": "2024-04-29T17:37:13.870940+09:00[Asia/Seoul]", "errorMessage": null, "durationInMs": null, "stateTransition": null }, { "circuitBreakerName": "accountsCircuitBreaker", "type": "STATE_TRANSITION", "creationTime": "2024-04-29T17:37:13.877831+09:00[Asia/Seoul]", "errorMessage": null, "durationInMs": null, "stateTransition": "CLOSED_TO_OPEN" }, { "circuitBreakerName": "accountsCircuitBreaker", "type": "NOT_PERMITTED", "creationTime": "2024-04-29T17:37:14.481066+09:00[Asia/Seoul]", "errorMessage": null, "durationInMs": null, "stateTransition": null } ] }
JSON
복사
@Bean public RouteLocator eazyBankRouteConfig(RouteLocatorBuilder routeLocatorBuilder) { return routeLocatorBuilder.routes() .route(p -> p .path("/eazybank/accounts/**") .filters( f -> f.rewritePath("/eazybank/accounts/(?<segment>.*)","/${segment}") .addResponseHeader("X-Response-Time", LocalDateTime.now().toString()) .circuitBreaker(config -> config.setName("accountsCircuitBreaker") .setFallbackUri("foward:/contactSupport"))) .uri("lb://ACCOUNTS")).build();
Java
복사
스프링 클라우드 게이트웨이를 사용하여 서킷 브레이커 패턴을 구축하는 단계를 보여줍니다. 여기서는 세 가지 주요 단계를 따르는데, 자세한 설명은 다음과 같습니다:
1.
Maven 의존성 추가: Resilience4j를 사용하는 스프링 클라우드 스타터 서킷 브레이커 의존성을 pom.xml 파일에 추가합니다.
2.
서킷 브레이커 필터 추가: RouteLocator의 빈을 생성하는 메서드 내부에서, 서킷 브레이커 필터를 추가하고 /contactSupport URI로 팰백을 처리할 REST API 핸들러를 생성합니다. 코드 조각에서는 .circuitBreaker 메서드를 사용하여 구성이 명시되어 있으며, 팰백 URI로는 "forward:/contactSupport"가 설정되어 있습니다.
3.
프로퍼티 추가: application.yml 파일에 다음 프로퍼티를 추가합니다:
resilience4j.circuitbreaker.configs.default.slidingWindowSize: 10으로 설정된 슬라이딩 윈도우 크기는 회로 차단기가 상태를 결정하기 위해 고려하는 최근 호출 수를 정의합니다.
permittedNumberOfCallsInHalfOpenState: 2로 설정된 이 값은 회로 차단기가 HALF-OPEN 상태일 때 허용하는 호출 수를 정의합니다.
failureRateThreshold: 50으로 설정된 실패율 임계값은 OPEN 상태로 전환하기 전에 허용되는 실패율의 퍼센트를 정의합니다.
waitDurationInOpenState: 10000으로 설정된 이 값은 회로 차단기가 OPEN 상태에서 HALF-OPEN 상태로 전환하기 전에 대기하는 시간(밀리초)을 정의합니다.
이번 강의에서는 계정 마이크로서비스 내부에 회로 차단기 패턴을 구현해 보겠습니다.
계정 마이크로서비스에서는 fetchCustomerDetails라는 REST API가 있습니다. 이 API를 통해, 계정 마이크로서비스는 카드 및 대출 마이크로서비스를 호출하게 됩니다.
만약 대출이나 카드 마이크로서비스 중 하나가 매우 느리게 응답하거나 완전히 다운되었거나 네트워크 문제가 있다면, 이전 강의에서 논의한 것처럼 계정 마이크로서비스와 게이트웨이 서버에 연쇄적인 영향을 미칠 것입니다.
현재 계정 마이크로서비스의 코드는 의존하는 마이크로서비스 중 하나가 다운될 경우 심각한 문제를 일으킬 것입니다. 이 문제를 극복하기 위해 계정 마이크로서비스에도 회로 차단기 패턴을 구현해 보겠습니다.
계정 마이크로서비스는 페인 클라이언트(feign client)를 사용하여 카드 및 대출 마이크로서비스를 호출합니다. 이에 따라 페인 클라이언트와 회로 차단기 간에 통합이 가능한지, 계정 마이크로서비스 내에서 적은 노력이나 설정으로 활용할 수 있는지 확인해야 합니다. 이를 위해 spring.io의 공식 웹사이트에서 Spring Cloud 프로젝트를 클릭한 다음, Spring Cloud OpenFeign을 찾습니다.
Spring Cloud OpenFeign 프로젝트를 클릭하고 'LEARN' 버튼을 눌러 공식 문서를 열면, 회로 차단기와 관련된 몇 가지 섹션이 보입니다. 여기에서 Feign과 Spring Cloud 회로 차단기 지원에 대한 설명을 클릭합니다.
여기에는 회로 차단기가 클래스패스에 있고 application.yml 파일 내에 spring.cloud.openfeign.circuitBreaker.enabledtrue로 설정하면, Feign이 모든 메소드를 회로 차단기로 감싸서 처리할 것이라는 설명이 있습니다.
이제 회로 차단기와 관련된 스프링 클라우드 의존성을 추가하고, application.yml 내에 이 속성을 활성화할 것입니다. pom.xml 파일을 열고 spring-cloud-starter-netflix-eureka-client 바로 아래에 springcloud-starter-circuitBreaker-resiliency4j 의존성을 추가합니다.
그 다음 application.yml 파일을 열고 이 속성을 추가한 후 저장하고 빌드를 수행합니다.
공식 문서로 돌아가면, 대출 및 카드 마이크로서비스의 Fallback 메커니즘을 정의해야 합니다. 즉, 대출 마이크로서비스가 다운되었을 때 어떻게 처리할지에 대한 지침이 있습니다. 인터페이스 위에 Fallback 클래스를 지정해야 합니다. 이 Fallback 클래스에서는 Feign 클라이언트 인터페이스를 구현하고, 인터페이스 내 정의된 추상 메소드를 오버라이드하여 fallback 로직을 작성합니다.
이제 계정 마이크로서비스의 패키지로 가서 Feign 클라이언트 인터페이스를 작성합니다. 같은 패키지 내에 LoansFallbackCardsFallback 클래스를 생성하고 각각 LoansFeignClientCardsFeignClient 인터페이스를 구현합니다. 이 클래스에는 @Component 애너테이션을 붙이고, 인터페이스의 메소드를 오버라이드하여 기본적으로 null 값을 반환합니다.
Fallback 클래스가 완성되면, Feign 인터페이스에 fallback 매개변수와 함께 LoansFallback.classCardsFallback.class를 지정합니다. 그 후 서비스 레이어에서 null 값을 체크하고 필요한 로직을 수행합니다.
모든 변경 사항을 저장하고 빌드를 한 후에는 다음 강의에서 지금까지 한 작업에 대한 전체 데모를 볼 수 있습니다.
// 20240429195541 // http://localhost:8080/actuator/circuitbreakers { "circuitBreakers": { "CardsFeignClientfetchCardDetailsStringString": { "failureRate": "-1.0%", "slowCallRate": "-1.0%", "failureRateThreshold": "50.0%", "slowCallRateThreshold": "100.0%", "bufferedCalls": 1, "failedCalls": 0, "slowCalls": 0, "slowFailedCalls": 0, "notPermittedCalls": 0, "state": "CLOSED" }, "LoansFeignClientfetchLoanDetailsStringString": { "failureRate": "-1.0%", "slowCallRate": "-1.0%", "failureRateThreshold": "50.0%", "slowCallRateThreshold": "100.0%", "bufferedCalls": 1, "failedCalls": 0, "slowCalls": 0, "slowFailedCalls": 0, "notPermittedCalls": 0, "state": "CLOSED" } } } // 20240429200022 // http://localhost:8080/actuator/circuitbreakers { "circuitBreakers": { "CardsFeignClientfetchCardDetailsStringString": { "failureRate": "0.0%", "slowCallRate": "0.0%", "failureRateThreshold": "50.0%", "slowCallRateThreshold": "100.0%", "bufferedCalls": 10, "failedCalls": 0, "slowCalls": 0, "slowFailedCalls": 0, "notPermittedCalls": 0, "state": "CLOSED" }, "LoansFeignClientfetchLoanDetailsStringString": { "failureRate": "90.0%", "slowCallRate": "0.0%", "failureRateThreshold": "50.0%", "slowCallRateThreshold": "100.0%", "bufferedCalls": 10, "failedCalls": 9, "slowCalls": 0, "slowFailedCalls": 0, "notPermittedCalls": 1, "state": "OPEN" } } }
JSON
복사
ResponseEntity<LoansDto> loansDtoResponseEntity = loansFeignClient.fetchLoanDetails(correlationId, mobileNumber); if(null != loansDtoResponseEntity) customerDetailsDto.setLoansDto(loansDtoResponseEntity.getBody());
Java
복사
@FeignClient(name = "loans", fallback = LoansFallback.class) public interface LoansFeignClient { @GetMapping(value = "/api/fetch",consumes = "application/json") public ResponseEntity<LoansDto> fetchLoanDetails(@RequestHeader("eazybank-correlation-id") String correlationId, @RequestParam String mobileNumber); }
Java
복사
@Component public class LoansFallback implements LoansFeignClient { @Override public ResponseEntity<LoansDto> fetchLoanDetails(String correlationId, String mobileNumber) { return null; } }
Java
복사
spring: cloud: gateway: httpclient: # connect-timeout: HTTP 클라이언트가 서버에 연결을 시도할 때까지의 최대 대기 시간(밀리초)입니다. # 이 시간이 초과하면 연결 시도가 실패로 간주됩니다. connect-timeout: 1000 # response-timeout: HTTP 클라이언트가 서버로부터 응답을 받기 위해 대기하는 최대 시간입니다. # '5s'는 5초를 의미합니다. 이 시간을 초과하면 요청은 타임아웃으로 처리됩니다. response-timeout: 5s
YAML
복사
@Bean public RouteLocator eazyBankRouteConfig(RouteLocatorBuilder routeLocatorBuilder) { return routeLocatorBuilder.routes() .route(p -> p .path("/eazybank/accounts/**") .filters( f -> f.rewritePath("/eazybank/accounts/(?<segment>.*)","/${segment}") .addResponseHeader("X-Response-Time", LocalDateTime.now().toString()) .circuitBreaker(config -> config.setName("accountsCircuitBreaker") .setFallbackUri("foward:/contactSupport"))) .uri("lb://ACCOUNTS"))
Java
복사
포스트맨(Postman)을 사용하는 클라이언트 애플리케이션의 응답 대기 시간을 조절해 보겠습니다.
이번에는 다시 요청을 시도하고, 이번에는 게이트웨이 서버의 스레드가 대출 마이크로서비스로부터 응답을 받기 위해 대기하고 있는 것을 확인할 수 있습니다. 포스트맨에서는 응답을 기다리고 있는 상태입니다.
만약 브레이크포인트를 해제하지 않는다면, 클라이언트 애플리케이션은 계속해서 응답을 기다릴 것입니다. 이는 서버 리소스와 스레드가 불필요하게 응답을 기다리게 되며, 응답 시간이 얼마나 걸릴지 알 수 없는 상태가 됩니다.
이러한 도전을 극복하기 위해서는 타임아웃 설정을 정의해야 합니다. 이 타임아웃 설정을 통해 설정된 시간을 초과할 경우 대기하지 않고 요청을 다시 시도하거나 비즈니스 로직에 따라 펄백 메커니즘을 실행할 수 있습니다.
포스트맨으로 다시 요청을 보내면, 게이트웨이 서버는 약 2분 26초 후에 응답을 받습니다. 실제 프로젝트에서는 이렇게 긴 시간을 기다리지 않아야 합니다.
이제 계정 마이크로서비스의 요청을 시도해보겠습니다. 이번에는 클라이언트 애플리케이션이 대기하지 않고 바로 에러 응답을 받습니다. 이는 게이트웨이 서버에서 계정 마이크로서비스에 대해 설정된 회로 차단기 필터 덕분입니다.
기본적으로 회로 차단기 타임아웃은 최대 1초를 기다립니다. 1초를 초과하면 즉시 펄백 메커니즘으로 전환합니다. 그러나 전체 마이크로서비스에서 회로 차단기 패턴을 사용하지 않는 경우, 게이트웨이 서버나 다른 마이크로서비스에서 응답을 오랫동안 기다리지 않도록 타임아웃 설정을 구성해야 합니다.
이를 위해 Spring Cloud Gateway의 공식 문서를 참조할 수 있습니다. 여기서 타임아웃에 대해 검색하면 관련 설정 섹션을 찾을 수 있습니다. 이 섹션에서는 타임아웃을 설정하는 방법에 대한 자세한 정보를 제공합니다. 개발자로서 이러한 타임아웃을 반드시 설정해야 하며, 이는 매우 간단합니다.
설정 후 게이트웨이 서버의 application.yml에서 이 속성들을 추가하고 빌드를 실행합니다. 게이트웨이 서버의 HTTP 클라이언트 섹션에 연결 타임아웃(1초)과 응답 타임아웃(2초)을 설정합니다.
설정한 타임아웃을 테스트하기 위해 대출 마이크로서비스 경로에 대한 요청을 다시 시도하면, 이번에는 즉시 게이트웨이 타임아웃 오류 응답을 받게 됩니다. 이 글로벌 설정은 모든 마이크로서비스에 적용되지만, 계정 마이크로서비스와 같이 회로 차단기 패턴이 구성된 경우, 글로벌 설정보다 회로 차단기의 기본 타임아웃 설정이 우선 적용됩니다.
특정 경로나 마이크로서비스에 대한 타임아웃을 개별적으로 설정할 수도 있습니다. 이는 공식 문서에서 설명하고 있으며, 라우팅 설정을 통해 메타데이터로 설정할 수 있습니다.
나중에는 회로 차단기의 기본 타임아웃 설정을 오버라이드하는 방법을 보여드리겠습니다.
이 강의에서는 마이크로서비스에서 타임아웃을 설정하는 방법에 대해 자세히 설명했습니다.

Retry 패턴(재시도 패턴)

이 강의에서는 마이크로서비스의 새로운 복원력 패턴인 Retry Pattern에 대해 소개해 보겠습니다.
Retry Pattern이란 무엇일까요?
이 패턴을 이용하면, 서비스가 일시적으로 실패할 때 여러 번의 재시도를 구성할 수 있습니다.
이 패턴은 클라이언트의 요청이 재시도 후 성공할 수 있는 네트워크 중단과 같은 시나리오에서 특히 유용합니다.
Retry Pattern을 마이크로서비스 내에서 구현하려 할 때, 우리는 몇 번이나 연산을 재시도할 것인지 명확히 해야 합니다.
세 번, 다섯 번, 혹은 열 번 재시도하고 싶은지는 비즈니스 로직에 따라 결정해야 합니다. 그리고 이 재시도 로직은 오류 코드, 예외 또는 응답 상태와 같은 많은 요소들을 기반으로 조건부로 실행될 수 있습니다.
재시도하는 동안, 우리는 backoff 전략을 따를 수 있습니다. 이는 시스템에 부담을 주지 않고 각 재시도 사이의 지연 시간을 점진적으로 늘리는 방식입니다. 이것을 exponential backoff라고 하며, 각 재시도 사이에 더 많은 시간을 주어 네트워크 문제가 해결될 수 있는 충분한 시간을 제공합니다.
필요한 경우, Retry Pattern을 Circuit Breaker와 같은 다른 패턴과 통합할 수도 있습니다. Retry Pattern과 Circuit Breaker 패턴을 결합하면, 연속적으로 실패한 재시도 횟수가 일정 수준에 도달한 후에 회로 차단기가 열리도록 할 수 있습니다.
마이크로서비스 내에서 Retry Pattern을 구현할 때는 멱등한 연산에 대해서만 이 패턴을 구현해야 한다는 점을 주의해야 합니다. 멱등한 연산이란 여러 번 실행해도 부작용이 없는 연산을 말합니다.
이번 강의에서는 Retry Pattern에 대한 간단한 소개를 마쳤습니다. 다음 강의에서는 실제로 마이크로서비스 내에서 이 패턴을 구현하는 방법을 알아보겠습니다.
Spring Cloud Gateway를 활용하여 재시도(retry) 패턴을 구현해 보겠습니다.
현재 보시는 것처럼, 계정 마이크로서비스 설정에서는 회로 차단기(circuit breaker) 패턴을 활성화했습니다. 이번에는 대출 마이크로서비스(loans microservice) 내에서 재시도 패턴을 시도해 볼 계획입니다. addResponseHeaderFilter() 이후에, retry라는 이름의 새 필터를 호출할 것입니다.
재시도 횟수를 지정할 수 있는 여러 재시도 필터가 있지만, 추가적인 설정도 제공하고자 합니다. 따라서 재시도 관련 설정을 받아들이는 첫 번째 필터를 호출할 예정입니다. 이 필터 내에서, 입력 변수 이름인 retry config를 사용한 람다 표현식을 작성하고, setRetries()를 호출하여 Spring Cloud Gateway에 몇 번 재시도할 것인지 알려줍니다. 여기서는 세 번 재시도하겠다고 지정할 것입니다.
HTTP 메소드 중 어떤 것을 재시도에 포함할지 지정하기 위해 set() 메소드를 호출할 수 있습니다. 재시도는 멱등성(idempotent) 연산에만 수행해야 하므로, HTTP GET 메소드만 지정하겠습니다. POST, PATCH, UPDATE와 같은 다른 메소드는 부작용이 있을 수 있으므로, 이들에 대해서는 재시도 패턴을 적용하지 않습니다.
또한, setBackoff() 메소드를 호출하여 백오프(backoff) 설정을 할 수 있습니다. 첫 번째 백오프로 100밀리초를 기다린 후, 최대 백오프 시간을 1000밀리초로 설정하고, 이전 백오프 값에 적용할 계수(factor)와 계수 적용 여부를 boolean 값으로 설정합니다.
이 설정을 통해 대출 마이크로서비스의 HTTP GET 메소드를 지원하는 모든 REST API에 대해 재시도 패턴을 활성화했습니다. 변경사항을 저장하고, LoansController에 로거 문을 추가하여 API 호출 횟수를 확인할 수 있습니다. 이러한 설정을 테스트하기 위해, 빌드 후에 대출 및 게이트웨이 서버를 재시작하고 Postman을 사용하여 API를 호출할 예정입니다.
setBackoff 메서드는 코드의 재시도 로직에서 백오프(backoff) 정책을 설정하는 데 사용됩니다. 백오프 정책은 재시도 사이에 대기하는 시간을 동적으로 조정하여, 연속적인 요청 실패 후 서비스에 과부하를 주지 않도록 하는 전략입니다. 이는 서버나 다른 네트워크 자원이 일시적으로 오버로드되었거나 일시적인 오류를 겪고 있을 때 유용합니다.
setBackoff 메서드는 다음과 같은 매개변수를 받습니다:
a.
firstBackoff: 첫 번째 재시도를 위해 대기하는 초기 지연 시간입니다. 이 예에서는 100ms로 설정되어 있습니다.
b.
maxBackoff: 재시도 간 대기 시간의 최대 한계입니다. 이 값은 재시도 간의 대기 시간이 이 값을 초과하지 않도록 제한합니다. 여기서는 1000ms로 설정되어 있습니다.
c.
factor: 백오프 계수로, 각 재시도 후 대기 시간을 얼마나 늘릴지를 결정합니다. 2로 설정된 경우, 대기 시간은 각 재시도 후 두 배씩 증가합니다.
d.
basedOnPreviousValue: 이 플래그가 true로 설정된 경우, 각 재시도의 대기 시간은 이전 재시도 대기 시간에 백오프 계수를 곱한 값에 기반합니다. false일 경우, 대기 시간은 초기 지연 시간에 재시도 횟수와 백오프 계수의 곱으로 계산됩니다.
@Retry(name = "getBuildInfo", fallbackMethod = "getBuildInfoFallback") @GetMapping("/build-info") public ResponseEntity<String> getBuildInfo() { return ResponseEntity .status(HttpStatus.OK) .body(buildVersion); } public ResponseEntity<String> getBuildInfoFallback(Throwable throwable) { return ResponseEntity .status(HttpStatus.OK) .body("0.9"); }
Java
복사
@Bean public Customizer<ReactiveResilience4JCircuitBreakerFactory> defaultCustomizer() { return factory -> factory.configureDefault(id -> new Resilience4JConfigBuilder(id) .circuitBreakerConfig(CircuitBreakerConfig.ofDefaults()) .timeLimiterConfig(TimeLimiterConfig.custom().timeoutDuration(Duration.ofSeconds(4)).build()).build()); }
Java
복사
resilience4j: circuitbreaker: configs: default: sliding-window-size: 10 permitted-number-of-calls-in-half-open-state: 2 failure-rate-threshold: 50 wait-duration-in-open-state: 10000 retry: configs: default: max-attempts: 3 # 최대 시도 횟수: 실패 후 재시도할 최대 횟수입니다. 여기서는 3회로 설정됩니다. wait-duration: 100 # 기다리는 시간: 재시도 사이에 대기하는 기본 시간(밀리초)입니다. 이 경우, 첫 재시도 전 100ms 대기합니다. enable-exponential-backoff: true # 지수 백오프 활성화: 지수 백오프를 사용하여 재시도 대기 시간을 증가시킬지 여부를 결정합니다. true로 설정되어 있습니다. exponential-backoff-multiplier: 2 # 지수 백오프 배수: 지수 백오프가 활성화된 경우, 이전 대기 시간에 곱할 값입니다. 여기서는 각 재시도 후 대기 시간이 이전의 2배가 됩니다.
YAML
복사
현재 getBuildInfo() 메소드에 문제가 발생할 때마다 재시도 연산을 시도하고 있습니다. 이 메소드 내에서 어떤 종류의 예외가 발생하든, 모든 상황에서 재시도가 시도됩니다.
그러나, 만약 NullPointerException이 발생할 때는 재시도를 하지 말라는 비즈니스 요구사항이 있다면 어떻게 될까요? 입력 데이터에 NullPointerException이 발생한다면, 몇 번을 재시도해도 항상 같은 예외가 발생하기 때문에 이런 상황에서는 재시도를 원하지 않습니다.
이를 해결하기 위해 accounts 마이크로서비스의 application.yml에 새로운 속성을 정의해야 합니다. ignoreExceptions라는 속성을 통해 무시할 예외들을 지정할 수 있으며, 예를 들어 NullPointerException만 지정할 수 있습니다. 이렇게 설정함으로써 NullPointerException이 발생할 때는 재시도를 하지 않도록 구성할 수 있습니다.
변경 후, 빌드를 수행하고 AccountsApplication과 게이트웨이 서버를 재시작합니다. Postman을 통해 요청을 발송했을 때, 재시도가 일어나지 않아야 합니다. AccountsApplication의 콘솔에서 이를 확인할 수 있으며, getBuildInfo 메소드와 관련된 로그가 한 번만 출력되어야 합니다.
또 다른 요구사항으로는 특정 예외에 대해서만 재시도를 수행하고 싶을 수 있습니다. 이 경우 retryExceptions 속성을 사용하여 재시도할 예외를 지정할 수 있습니다. 예를 들어, timeout 예외만 재시도하도록 설정할 수 있습니다. 이 설정을 사용하면 다른 모든 예외는 자동으로 무시됩니다.
이 모든 설정 후, AccountsController에서 NullPointerException 대신 TimeoutException을 발생시키도록 변경할 수 있습니다. 이렇게 변경한 후 Postman에서 API를 다시 호출하면, Resilience4j 프레임워크가 설정에 따라 여러 번 재시도를 시도할 수 있습니다.
이 모든 과정을 통해, 재시도 패턴을 마이크로서비스 내에서 어떻게 구성하고 관리할 수 있는지에 대한 이해를 높일 수 있습니다. 또한, 필요에 따라 Gateway 서버에서도 유사한 설정을 적용할 수 있습니다. 각 설정과 변경 사항을 잘 이해하고 적용하면, 마이크로서비스의 회복력을 효과적으로 높일 수 있습니다.
우리는 현재 getBuildInfo() 메소드에서 문제가 발생할 때마다 재시도 작업을 시도하고 있습니다. 이는 메소드 내에서 발생하는 예외의 종류에 관계없이 모든 상황에서 재시도가 이루어집니다.
그러나 비즈니스 요구사항에 따라 NullPointerException이 발생했을 때는 재시도를 하지 않도록 설정해야 할 수도 있습니다. 왜냐하면 주어진 입력 데이터로 인해 NullPointerException이 발생하는 경우, 몇 번을 재시도하더라도 항상 같은 예외가 발생하기 때문입니다. 이러한 경우에는 재시도를 하지 않도록 설정하는 것이 좋습니다.
이를 위해서는 accounts 마이크로서비스의 application.yml 파일로 이동하여 재시도 설정을 조정해야 합니다. 여기서 ignoreExceptions 속성을 정의하여 특정 예외를 재시도에서 제외할 수 있습니다. 예를 들어, NullPointerException을 재시도에서 제외하려면 이 예외를 ignoreExceptions 리스트에 추가하면 됩니다.
설정 예제:
yamlCopy code retry: ignoreExceptions: - java.lang.NullPointerException
YAML
복사
이 설정을 통해 NullPointerException이 발생할 경우 재시도가 이루어지지 않도록 할 수 있습니다.
또한, 특정 예외에 대해서만 재시도를 하도록 설정할 수도 있습니다. retryExceptions 속성을 사용하여 재시도할 예외를 명시적으로 지정할 수 있습니다. 이 경우, 지정된 예외 외에는 모든 예외가 재시도에서 자동으로 제외됩니다.
예를 들어, TimeoutException에 대해서만 재시도를 하도록 설정하고 싶다면 다음과 같이 설정할 수 있습니다:
yamlCopy code retry: retryExceptions: - java.util.concurrent.TimeoutException
YAML
복사
이 설정을 통해 TimeoutException이 발생했을 때만 재시도가 이루어지며, 다른 모든 예외는 재시도에서 제외됩니다.
이런 방식으로 재시도 패턴을 미세 조정하여, 애플리케이션의 특정 요구사항에 맞게 예외 처리를 최적화할 수 있습니다. 이러한 설정은 서비스의 안정성을 높이고 불필요한 처리를 줄이는 데 도움이 됩니다.
다음 강의에서는 이러한 설정을 통해 어떻게 재시도 패턴이 실제로 작동하는지 보여드릴 예정입니다. 감사합니다.

게이트웨이 서버 내 비율 제한 패턴(RateLimiter) 구현하기

오늘날 디지털 서비스의 중추적 역할을 하는 마이크로서비스 구조에서는, 특히 API를 통한 요청이 폭증하는 상황에서 서비스의 안정성을 보장하는 것이 중요합니다. 이를 위해 '비율 제한 패턴(RateLimiter)'의 구현이 필수적인데, 이번 포스트에서는 실제로 이 패턴을 게이트웨이 서버에 어떻게 적용하는지를 단계별로 살펴보겠습니다.

1. 의존성 추가

먼저, Redis를 활용한 비율 제한 구현을 위해 필요한 의존성을 Maven 프로젝트에 추가합니다. 'spring-boot-starter-data-redis-reactive'라는 아티팩트 ID를 사용하여 의존성을 정의하고, Maven을 통해 의존성을 로드합니다.

2. Bean 설정

GatewayserverApplication 내에서 두 가지 주요 Bean을 설정합니다:
KeyResolver: 사용자별 요청을 식별하기 위한 키 해결자를 설정합니다. 이는 요청 헤더에서 'user'라는 이름의 값을 참조하여 키를 생성하며, 헤더가 없는 경우 'anonymous'라는 기본값을 사용합니다.
RedisRateLimiter: Redis를 사용한 비율 제한을 설정합니다. 이 구성은 'replenishRate', 'burstCapacity', 'defaultRequesterTokens' 등의 파라미터를 통해 각 요청에 대한 토큰 수를 정의합니다.

3. 라우팅 구성

API 게이트웨이의 라우팅 설정에 비율 제한 필터를 적용합니다. RequestRateLimiter 필터를 추가하여 RedisRateLimiter 및 KeyResolver 설정을 연결합니다. 이 과정에서 각 API 요청에 대한 비율 제한이 활성화됩니다.

4. Redis 데이터베이스 설정

비율 제한 로직을 지원하기 위해 Redis 컨테이너를 Docker를 통해 실행합니다. 기본 포트인 6379를 사용하며, 'eazyredis'라는 이름으로 컨테이너를 구동합니다.

5. application.yml 설정

게이트웨이 서버의 application.yml 파일에 Redis 연결 설정을 추가합니다. 이 설정에는 Redis 호스트, 포트, 타임아웃 등이 포함됩니다.

6. 서비스 시작 및 테스트

모든 설정이 완료된 후, 게이트웨이 서버를 시작하고 Apache Benchmark 도구를 사용하여 부하 테스트를 수행합니다. 이 테스트를 통해 비율 제한 패턴이 정상적으로 작동하는지 확인할 수 있습니다.

결론

이번 포스트에서는 마이크로서비스 아키텍처에서 필수적인 비율 제한 패턴을 게이트웨이 서버에 구현하는 방법을 살펴보았습니다. 이 패턴은 시스템을 과도한 요청으로부터 보호하고, 모든 사용자가 공정하게 서비스를 이용할 수 있도록 합니다. 실제 서비스 환경에서는 이러한 설정을 통해 서비스의 안정성과 성능을 크게 향상시킬 수 있습니다.

비율 제한 패턴 구현 과정

1. 메소드 선택 및 어노테이션 적용

특정 메소드(getJavaVersion())에 대해 비율 제한을 적용하고자 합니다. 이를 위해 @RateLimiter 어노테이션을 해당 메소드에 적용하고, 설정 이름으로 메소드 이름을 사용합니다(getJavaVersion()).

2. application.yml에 속성 설정

이제 application.yml 파일에 resilience4j.ratelimiter를 사용하여 비율 제한 관련 속성을 설정합니다. 주요 속성으로는 limitRefreshPeriod, limitForPeriod, 그리고 timeoutDuration이 있습니다. 이 설정은 각각의 리프레시 기간 동안 허용되는 요청 수와 요청이 거부될 때 스레드가 대기할 최대 시간을 정의합니다.

3. 서비스 구동 및 테스트

설정을 완료한 후, 계정 마이크로서비스(AccountsApplication)를 시작합니다. Postman을 사용하여 getJavaVersion() API를 여러 번 호출하면, 일정 수 이상에서는 RateLimiter does not permit further details라는 오류 메시지와 함께 500 내부 서버 오류가 발생합니다.

4. 폴백 메커니즘 정의

비율 제한 때문에 요청이 거부되었을 때 실행될 폴백 메소드(getJavaVersionFallback())를 정의합니다. 이 메소드는 오류가 발생했을 때 'Java 17'이라는 문자열을 반환하도록 설정됩니다.

결론

이 강의를 통해 게이트웨이 서버와 일반 마이크로서비스 양쪽에서 비율 제한 패턴을 구현하는 두 가지 접근 방식을 설명하였습니다. resilience4j 라이브러리와 어노테이션을 활용하여 간편하게 비율 제한 로직을 마이크로서비스에 통합할 수 있으며, 이를 통해 시스템의 안정성을 보장하고 서비스의 품질을 관리할 수 있습니다.