반응형

이전글: 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을 매 서비스마다 수동 주입해줘야 하기 때문에 장단점이 있다는 것을 인지해야한다(물론 서비스마다 서로 다른 설정을 해야한다면 오히려 더 나은 선택일 수도 있다).

728x90
반응형

+ Recent posts