이전글: 2022.02.04 - [개발/spring] - [retry] spring-retry for auto retry
[retry] spring-retry for auto retry
spring-retry? exception이 난 작업에 대해 자동으로 재시도 해주는 스프링 라이브러리이다. 일시적인 connection error 등에 유용하게 사용할 수 있다. 환경: springboot2.6.2 implementation 'org.springframewo..
bangpurin.tistory.com
저번 시간에는 실전 코딩을 해보았다면 이번에는 spring-retry 관련 테스트 코드를 작성해본다.
가. 어노테이션 이용 - @SpringBootTest
1. retry
우선 아래는 service 코드 이다.
@Retryable(
value = RuntimeException.class,
maxAttempts = 3,
backoff = @Backoff(delay = 3000)
)
public HolderAccountSummary getHolderAccountSummary(String holderID) {
log.debug(">>> getHolder : {} ", holderID);
return repository.findByHolderId(holderID)
.orElseThrow(() -> new NoSuchElementException("소유주가 존재하지 않습니다."));
}
테스트 코드
@Test
void retryTest_with_bean(){
//when
Mockito.when(accountRepository.findByHolderId(ArgumentMatchers.anyString())).thenThrow(RuntimeException.class);
assertThrows(RuntimeException.class, () -> retryService.getHolderAccountSummary(ArgumentMatchers.anyString()));
//then
Mockito.verify(accountRepository, Mockito.times(3)).findByHolderId(ArgumentMatchers.anyString());
}
>> 성공; 3번 재시도해도 실패가 나서 최종적으로 RuntimeException이 난다.
2. recovery
역시 service 코드 -> 테스트 코드 순이다.
@Retryable(
value = RuntimeException.class,
maxAttempts = 3,
backoff = @Backoff(delay = 3000)
)
public HolderAccountSummary getHolderAccountSummary(String holderID) {
log.debug(">>> getHolder : {} ", holderID);
return repository.findByHolderId(holderID)
.orElseThrow(() -> new NoSuchElementException("소유주가 존재하지 않습니다."));
}
@Recover
private HolderAccountSummary recover(RuntimeException e, String holderID) {
return HolderAccountSummary.builder()
.name("recovery")
.build();
}
@Test
void retry_with_bean(){
//when
Mockito.when(accountRepository.findByHolderId(ArgumentMatchers.anyString())).thenThrow(RuntimeException.class);
assertThrows(RuntimeException.class, () -> retryService.getHolderAccountSummary(ArgumentMatchers.anyString()));
//then
Mockito.verify(accountRepository, Mockito.times(3)).findByHolderId(ArgumentMatchers.anyString());
}
@Test
void recovery_with_bean(){
//when
Mockito.when(accountRepository.findByHolderId(ArgumentMatchers.anyString())).thenThrow(RuntimeException.class);
HolderAccountSummary result = retryService.getHolderAccountSummary(ArgumentMatchers.anyString());
//then
Mockito.verify(accountRepository, Mockito.times(3)).findByHolderId(ArgumentMatchers.anyString());
Assertions.assertThat(result.getName()).isEqualTo("recovery");
}
이렇게 실행하면 recovery test는 성공하나 retry test가 실패한다.
되던게 안 된다고? 라고 당황해하며 검색을 해봤으나 도무지 말이 안되서 생각해보니..ㅎ
recovery가 생기고 나서는 3번 실패 이후 recovery함수를 타기 때문에 exception이 날 일이 없기 때문이었다. 하하하..
@Recovery 어노테이션을 주석으로 처리하고 다시 돌리면 성공한다.
나. retryTemplate 이용 - @SpringBootTest
0. retryConfig 생성
@Configuration
@EnableRetry
public class RetryConfig {
@Bean
public RetryTemplate retryTemplate() {
RetryTemplate retryTemplate = new RetryTemplate();
FixedBackOffPolicy fixedBackOffPolicy = new FixedBackOffPolicy();
fixedBackOffPolicy.setBackOffPeriod(2000l); //long type; 딜레이 시간(ms)
retryTemplate.setBackOffPolicy(fixedBackOffPolicy);
SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy();
retryPolicy.setMaxAttempts(3); //횟수
retryTemplate.setRetryPolicy(retryPolicy);
retryTemplate.registerListener(new RetryListener());
return retryTemplate;
}
@Slf4j
public static class RetryListener extends RetryListenerSupport {
@Override
public <T, E extends Throwable> boolean open(RetryContext context, RetryCallback<T, E> callback) {
log.info(">>>> before retry");
return super.open(context, callback);
}
@Override
public <T, E extends Throwable> void close(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
log.info(">>>> after retry");
}
@Override
public <T, E extends Throwable> void onError(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
log.info(">>>> error on retry");
}
}
}
1. retry
public HolderAccountSummary getHolderAccountSummaryTemplate(String holderID) {
log.info("retry going on");
return retryTemplate.execute(context -> {
return repository.findByHolderId(holderID)
.orElseThrow(() -> new NoSuchElementException("소유주가 존재하지 않습니다."));
});
}
기존 테스트 코드에서 위와 같이 서비스 함수만 바꿔주고 실행하면, 아래와 같이 로그가 지나가면서 retryConfig에 설정한대로 적용된 것을 볼 수 있다.(2초 간격으로 3번 재실행, 에러날 때마다 onError 실행 등)
13:04:30.883 INFO 5638 --- [main] com.cqrs.query.service.RetryService : retry going on
13:04:30.884 INFO 5638 --- [main] c.c.q.config.RetryConfig$RetryListener : >>>> before retry
13:04:30.887 INFO 5638 --- [main] c.c.q.config.RetryConfig$RetryListener : >>>> error on retry
13:04:32.892 INFO 5638 --- [main] c.c.q.config.RetryConfig$RetryListener : >>>> error on retry
13:04:34.895 INFO 5638 --- [main] c.c.q.config.RetryConfig$RetryListener : >>>> error on retry
13:04:34.895 INFO 5638 --- [main] c.c.q.config.RetryConfig$RetryListener : >>>> after retry
2. recovery
가. 에서는 retry와 recovery를 완벽히 분리하여 작성하였지만, retryTemplate을 사용 할 시에는 execute 함수에 recovery context도 같이 실어 넘기는 방법으로 작성해야한다.
public HolderAccountSummary getHolderAccountSummaryTemplateAndRecovery(String holderID){
log.info("retry going on");
return retryTemplate.execute(context -> {
return repository.findByHolderId(holderID)
.orElseThrow(() -> new NoSuchElementException("소유주가 존재하지 않습니다."));
}, recovery -> {
log.info("recovery start");
return HolderAccountSummary.builder()
.name("recovery")
.build();
});
}
기존 recovery 테스트 코드에서 함수명만 바꿔주고 실행하면, 아래와 같이 로그가 지나가면서 3회 시도 후 recovery가 진행된 것을 알 수 있다.
14:35:32.460 INFO 7166 --- [ main] com.cqrs.query.service.RetryService : retry going on
14:35:32.462 INFO 7166 --- [ main] c.c.q.config.RetryConfig$RetryListener : >>>> before retry
14:35:32.465 INFO 7166 --- [ main] c.c.q.config.RetryConfig$RetryListener : >>>> error on retry
14:35:34.471 INFO 7166 --- [ main] c.c.q.config.RetryConfig$RetryListener : >>>> error on retry
14:35:36.474 INFO 7166 --- [ main] c.c.q.config.RetryConfig$RetryListener : >>>> error on retry
14:35:36.475 INFO 7166 --- [ main] com.cqrs.query.service.RetryService : recover start
14:35:36.475 INFO 7166 --- [ main] c.c.q.config.RetryConfig$RetryListener : >>>> after retry
다. retryTemplate 이용 - @MockitoExtension
위 가/나 방법 모두 @SpringBootTest 어노테이션을 사용하기 때문에 테스트를 하기 위하여 모든 spring context불러와 테스트가 무겁고 느리다는 단점이 있다. retryTemplate를 사용했을 때에도 @Configuration 빈을 활용하였기 때문에 @SpringBootTest 어노테이션을 사용했는데 이를 아래와 같이 수정하면 @SpringBootTest 어노테이션을 사용하지 않고도 테스트 코드를 작성할 수 있다.
@Service
//@RequiredArgsConstructor
public class RetryService {
private final AccountRepository repository;
private final RetryTemplate retryTemplate = new RetryTemplate();
public RetryService(AccountRepository repository) {
this.repository = repository;
FixedBackOffPolicy fixedBackOffPolicy = new FixedBackOffPolicy();
fixedBackOffPolicy.setBackOffPeriod(2000l); //long type; 딜레이 시간(ms)
retryTemplate.setBackOffPolicy(fixedBackOffPolicy);
SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy();
retryPolicy.setMaxAttempts(3); //횟수
retryTemplate.setRetryPolicy(retryPolicy);
retryTemplate.registerListener(new RetryConfig.RetryListener());
}
...
}
기존에 retryTemplate 빈을 통해 자동 주입하던 것을 수동 주입으로 바꾸고 설정 값을 세팅한다.
@ExtendWith(MockitoExtension.class)
public class RetryServiceTest_template {
@InjectMocks
RetryService retryService;
@Mock
AccountRepository accountRepository;
//@SpringBootTest
//class RetryServiceTest_template {
//
// @Autowired
// private RetryService retryService;
// @MockBean
// private AccountRepository accountRepository;
테스트 코드에서도 @SpringBootTest 어노테이션을 mockito extension으로 바꾸면 전체 spring context를 불러오지 않고도 테스트가 정상적으로 실행되는 것을 볼 수 있다.
허나 이럴 경우 테스트는 가벼워졌지만, retryTemplate을 매 서비스마다 수동 주입해줘야 하기 때문에 장단점이 있다는 것을 인지해야한다(물론 서비스마다 서로 다른 설정을 해야한다면 오히려 더 나은 선택일 수도 있다).
'개발 > spring' 카테고리의 다른 글
[annotation] NotNull NotBlank NonNull NotEmpty... (0) | 2022.02.11 |
---|---|
[개념] interceptor vs filter 그리고 ContentCachingRequestWrapper (0) | 2022.02.10 |
[retry] spring-retry for auto retry (0) | 2022.02.04 |
[spring] graceful shutdown default as of 2.3 (0) | 2022.02.03 |
[actuator] git info를 health에 포함하기 (0) | 2022.01.28 |