왜 쓸까?
Spring Retry에선 특정 메서드를 지정한 만큼 재시도 처리할 수 있도록 하는 기능을 제공한다.
일반적인 유스케이스는 외부 인프라(DB, API, 메시지 큐 등등..)와의 소통 및 처리과정에서 재시도 처리가 필요한 로직들일 것이다.
대개는 간헐적이고 일시적으로 발생하는 장애 상황에 대응하기 위한 전략으로 사용된다.
따라서 외부 API 호출 시 함께 사용되는 경우가 많으며, 관련 예제들도 보통은 Feign, RestTemplate 등의 클라이언트 관련 기술들과 함께 작성된 경우가 많다.
사용해보기
Spring Retry를 사용하기 위해선 아래 두 개의 의존성이 필요하다.
dependencies {
...
implementation 'org.springframework.retry:spring-retry'
implementation 'org.springframework.boot:spring-boot-starter-aop'
...
}
aop의 경우 조금 더 라이트한 모듈만 추가해도 되긴해보이는데 그냥 맘편하게 starter를 넣도록 하자.
어차피 일반적으로 많이 사용되는 모듈(spring-data-jpa 등..)에 함께 딸려져 들어오는 경우가 많다.
다음은 @EnableRetry를 Configuration에 등록해줘야한다.
가장 간단한 방법은 @SpringBootApplication 쪽에 해당 어노테이션을 같이 붙여주면된다.
@SpringBootApplication
@EnableRetry
public class RetryApplication {
public static void main(String[] args) {
SpringApplication.run(RetryApplication.class, args);
}
}
@Retryable
해당 어노테이션을 재시도할 메서드에 부착함으로써 특정 Exception에 대하여 동일 로직을 특정 주기, 횟수만큼 재시도하도록 할 수 있다.
@Service
public class RetryableService {
@Retryable
public void retryableSomething() {
...
}
}
사용법은 매우 간단하다.
중요하게 봐야하는 부분은 해당 어노테이션에서 설정할 수 있는 설정값들이다.
@Retryable에는 재시도 처리를 조금 더 디테일하게 다룰 수 있는 여러가지 옵션을 제공한다.
- retryFor (include)
- 재시도 처리를 할 Exception을 정의한다. (기존엔 include 옵션이었으나, Deprecated 되고 retryFor로 대체)
- noRetryFor (exclude)
- 재시도 처리를 하지 않을 Exception을 정의한다. (기존엔 exclude 옵션이었으나, Deprecated 되고 noRetryFor로 대체)
- maxAttempts
- 최대 재시도 횟수를 정의하며, 해당 값은 재시도 횟수 뿐 아닌 최초 시도 횟수를 포함해 카운팅한다.
- Ex) 값이 3라면 최초시도 1회 + 재시도 2회 실행
- 최대 재시도 횟수를 정의하며, 해당 값은 재시도 횟수 뿐 아닌 최초 시도 횟수를 포함해 카운팅한다.
- backoff
- 재시도 간 딜레이를 설정할 수 있다. 단위는 ms이다.
@Slf4j
@Service
public class RetryableService {
private int tryCount = 1;
@Retryable(
maxAttempts = 3,
backoff = @Backoff(delay = 2000),
retryFor = { RetryableException.class }
)
public void retryableDo() {
log.info("시도 횟수 - {}, 현재 시각 - {}", tryCount, LocalDateTime.now());
tryCount++;
throw new RetryableException();
}
}
위와 같이 설정하면 해당 메서드를 총 3번, 2초 간격으로 실행하겠다는 뜻이된다.
동작을 확인하기 위해 아래의 간단한 테스트를 돌려보자.
@SpringBootTest
class RetryableServiceTest {
@Autowired
private RetryableService retryableService;
@Test
void test() {
retryableService.retryableDo();
}
}
기대했던대로 해당 메서드는 2초 간격으로 총 3번 실행되었다.
Backoff 옵션을 조금 더 만져주면 재시도 주기를 더 우아하게 구성할 수도 있다. 이건 좀 유용해보이니 나중에 따로 더 딥하게 파보자.
@Recover
@Recover는 재시도 작업 실패 이후에 필요한 복구 메서드를 트리거해주는 어노테이션이다.
별도 옵션도 따로 없어서 @Retryable보다 훨씬 더 사용하기 간단하다.
@Retryable에서 지정한 최대 실행횟수(maxAttempts)까지 예외가 발생하게 된다면 @Recover가 부착된 메서드가 실행된다.
물론 @Recover 메서드가 여러 개 존재할 수 있고, 어떤 메서드를 실행할지는 @Retryable쪽에서 지정해줘야한다.
@Slf4j
@Service
public class RetryableService {
@Retryable(
...
recover = "recover"
)
public void retryableDo() {
...
}
@Recover
public void recover(RetryableException e) {
log.info("재시도 전체 실패. 리커버 실행");
}
}
@Retryable에선 String 타입의 메서드명을 입력해 메서드를 지정하므로 오류 사실을 컴파일 시점에서 잡을 수 없다.
"오타나지 않게 조심하자"보단 테스트를 잘 짜두면 된다. 휴먼에러는 불가피한 존재다.
@Recover를 설정하고 테스트를 진행하면, 아래와 같은 결과를 얻을 수 있다.
물론 maxAttempts를 초과하기 전에 메서드 수행에 성공하게 되면 리커버 메서드도 실행되지 않는다.
중요한 비즈니스 로직이 특정 횟수 이상 실패 시 관련 내용을 별도 DB 및 큐에 쌓거나 로깅을 하는 용도 정도가 가장 커먼케이스이지 않을까싶다.
한 가지 재미있는 사실은 @Recover 메서드에서 Exception과 함께 @Retryable 메서드의 파라미터값을 그대로 받을 수 있다는 것이다.
@Slf4j
@Service
public class RetryableService {
@Retryable(
...
recover = "recover"
)
public void retryableDo(LocalDateTime initialInvokedDateTime) {
...
}
@Recover
public void recover(RetryableException e, LocalDateTime initialInvokedDateTime) {
log.info("재시도 전체 실패. 리커버 실행. 최초 수행시각: {}", initialInvokedDateTime);
}
}
해당 부분을 잘 활용하면 리커버 처리를 훨씬 더 유용하게 할 수 있겠다.
물론 파라미터 없이 그냥 Exception만 받더라도 동작에는 문제가 없다.
하지만, @Retryable 메서드와 시그니처가 다르면 ExhaustedRetryException 예외가 발생한다.
아래 코드는 모두 해당 예외가 발생할 수 있는 대표적인 예시이다.
1) 파라미터 타입이 다른 경우
@Slf4j
@Service
public class RetryableService {
@Retryable(
...
recover = "recover"
)
public void retryableDo(LocalDateTime initialInvokedDateTime) {
...
}
@Recover
public void recover(RetryableException e, String anotherType) { // 파라미터 타입이 다름
...
}
}
2) 파라미터 순서가 다른 경우
@Slf4j
@Service
public class RetryableService {
@Retryable(
...
recover = "recover"
)
public void retryableDo(LocalDateTime initialInvokedDateTime, String stringValue) {
...
}
@Recover
public void recover(RetryableException e, String stringValue, LocalDateTime initialInvokedDateTime) { // 파라미터 순서가 다름
...
}
}
'프레임워크 > Spring' 카테고리의 다른 글
Jackson Deserializer 커스텀을 통해 LocalDateTime에 타임존 반영하기! (3) | 2024.03.16 |
---|---|
[Spring Batch] 스프링 배치의 기본적인 Job 이해하기 (1) | 2024.01.10 |
[Spring] WireMock 기반의 테스트환경 구축하기 (0) | 2023.11.06 |
뜯어보며 배우는 DispatcherServlet의 @RequestMapping 기반 핸들러 매핑 과정 (2) | 2023.10.22 |
[Spring] 스프링 컨텍스트 로딩 시간을 포함한 전체 테스트 코드 수행시간 측정하기 (0) | 2023.05.18 |