서비스를 개발하다보면 일시적인 혹은 지속적인 장애가 발생한다.
에를 들면 api를 단순히 호출하는 경우, batch에서 step을 실행하던 경우, db에 접근하던 도중.. 등등 많은 경우 장애가 발생할 수 있다.
그럼 사용자에게 서비스 장애를 지속시키지 않고 빠르고 안정적으로 서비스를 다시 제공하기 위한 안전장치는 어떤 것들이 있을까?
fault-tolerance(장애허용)을 위해 사용하는 기술로 retry 와 circuitBreaker 을 알아볼 수 있다.
(* 여기서는 자세한 동작,사용 방법보다 큰 틀의 개념만 바라보고자 작성하였습니다. )
" 하나의 요청에 대해, 요청이 실패했을 경우 해당 요청을 다시 시도한다. 계속 실패할 경우 다른 응답을 반환한다. "
말 그대로, 요청이 실패하면 정의된 재시도 정책에 따라 지정된 횟수만큼 요청을 재시도를 시도하는 것이다.
일정 시간 간격 또는 백오프 전략을 사용하여 재시도 간격을 조절할 수 있다.
" 요청들의 추이를 지켜보다가 지속적으로 실패하는 경우, 잠시 요청을 차단해서 장애를 전파하지 않도록 한다. "
retry 와 다르게 오히려 요청을 다시 요청하는 것이 아니라, 서비스의 장애가 지속되면 해당 서비스에 대한 요청을 차단함으로써 전체 시스템의 과부하를 방지하는 것이다.
때문에, 서비스의 상태를 모니터링하여 상태가 회복될 때까지 차단 상태로 전환시키다가 이후 일정 시간 동안 서비스 호출을 차단한 후에 다시 시도를 할 수 있다.
circuit breaker 는 크게 3가지 상태를 기반으로 동작하는데 플로우는 다음과 같이
close → open → fallback → half open → close or open 와 같은 순서로 진행된다.
CLOSED : 서킷 브레이커로 감싼 내부 프로세스가 요청과 응답을 정상적으로 주고 받을 수 있는 상태 = 정상 상태OPEN : 서킷 브레이커로 감싼 내부 프로세스가 요청과 응답을 정상적으로 주고 받을 수 없는 상태 = 장애 발생 가능HALF_OPEN : fall back 응답을 수행하고 있지만 실패율을 측정해서 CLOSE 또는 OPEN 으로 변경될 수 있는 상태아래와 같은 프로젝트의 요구 사항의 경우, retry, 서킷 브레이커를 동시에 적용할 수 있다.
이를 통해 Retry를 통해 일시적인 문제에 빠르게 대응하고, Circuit Breaker를 통해 지속적인 문제에 대응하여 서비스의 안정성을 유지할 수 있다.
프로젝트 요구 사항
☑︎ 해당 프로젝트는 모두 외부 api를 호출한 것들을 통해서만 구성되는 서비스이다.
☑︎ 데이터는 오로지 외부 API를 통해 가져오며, 이 API는 일시적인 네트워크 문제로 가끔 실패할 수 있습니다.
☑︎ 데이터 요청은 상대적으로 자주 발생하며, 실패 시 빠르게 대응하여 서비스를 유지해야 합니다.
☑︎ 그러나 만약 외부 서비스가 지속적으로 응답하지 않는 경우, 이로 인한 서비스 전체의 성능 저하를 방지하기 위한 대책이 필요합니다.
Retry 는 spring-retry 라이브러리를 사용하였고
implementation 'org.springframework.retry:spring-retry'
implementation 'org.springframework:spring-aspects'
CircuitBreaker 는 spring-cloud-starter-circuitbreaker-resilience4j 라이브러리를 사용한 예제이다.
implementation 'org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j'
implementation 'org.springframework.boot:spring-boot-starter-aop'
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Recover;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
@Service
public class DataService {
// 재시도를 시도한다. 이때 최대 3번까지 재시도하고, 각 재시도 간에는 2초의 딜레이가 있다.
@Retryable(maxAttempts = 3, backoff = @Backoff(delay = 2000))
// 지정한 실패율이 달성되면 요청이 중단되고 fallbackMethod 로 응답을 처리한다.
@CircuitBreaker(name = "externalService", fallbackMethod = "fallback")
public String fetchData(String url) {
RestTemplate restTemplate = new RestTemplate();
return restTemplate.getForObject(url, String.class);
}
// Circuit Breaker가 열렸을 때 (OPEN 상태일 때) 호출되는 fallback 메서드
public String fallback(String url, Exception e) {
return "Fallback data";
}
// Retry가 모든 재시도에 실패한 경우 호출되는 메서드 (최종 대체 응답 반환)
// 파라미터 첫 번째 인자는 @Retryable 에서 발생한 Exception 이 전달
// 이때, @Retryable 어노테이션의 메서드와 동일한 파라미터, 반환타입을 가져야 한다.
@Recover
public String recover(Exception e) {
return "Recovery data";
}
}
때문에, 두 패턴 중 무엇 하나만 가장 적합하다기보다는,
서로 보완적으로 사용하면 전체 시스템의 안정성과 견고성을 향상시킬 수 있으므로 두 패턴을 조합하여 사용하는 것이 서비스의 안정성에 효과적일 수 있다.
[참고]
https://hyeon9mak.github.io/spring-retry/
https://hyeon9mak.github.io/spring-circuit-breaker/
https://yangbongsoo.tistory.com/99
https://findmypiece.tistory.com/328
https://velog.io/@mu1616/Webclient에-적용된-CircuitBreaker와-Retry-테스트하기 // 전체