java Exception 발생 재시도
- Exception 발생시 특정 횟수만큼 재시도를 진행하고자 한다.
- 저같은 경우 주로 다른 애플리케이션을 API로 통신시 네트워크등의 이슈로 문제가 발생시 재시도를 진행하고자 할때 사용한다.
- 방법은 두가지가 존재한다.
spring에서 지원하는 spring-retry 와 net.jodah.failsafe 의 RetryPolicy이다.
● git 주소
technology-team / failsafe-retry — Bitbucket
● @Retryable 어노테이션
1. dependencies 설정
- spring-retry 사용 시 aspectjweaver 가 필요하다.
implementation 'org.springframework.boot:spring-boot-starter'
implementation 'org.springframework.boot:spring-boot-starter-web:2.5.4'
implementation 'org.springframework.retry:spring-retry:1.3.1'
runtimeOnly 'org.aspectj:aspectjweaver:1.9.7'
2. EnableRetry 설정
- SpringBootApplication 에 @EnableRetry 를 추가하여 활성화 시켜준다.
@SpringBootApplication
@EnableRetry
public class FailsafeRetryApplication {
public static void main(String[] args) {
SpringApplication.run(FailsafeRetryApplication.class, args);
}
}
3. @Retryable 으로 재시도 진행
@Retryable(maxAttempts = 2, backoff = @Backoff(2000), value = IllegalStateException.class,
exclude = { NullPointerException.class, NullPointerException.class })
- value, include : 여기에 설정된 특정 Exception이 발생했을 경우 retry한다.
- exclude : 설정된 Exception 재시도 제외
- maxAttempts : 최대 재시도 횟수 (기본 3회)
- backoff : 재시도 pause 시간
- @Recover 의 경우 발생한 Exception에 대한 return 처리를 진행할 수 있다. 단, 리턴타입은 @Retryable에 에 정의한 리턴타입과 동일해야 한다.
아래 코드에서는 IllegalStateException일 경우 재시도, NullPointerException, NumberFormatException 의 경우 리턴을 재정의하였습니다.
package net.suby.failsaferetry.retryable;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import lombok.RequiredArgsConstructor;
@RestController
@RequiredArgsConstructor
public class RetryableController {
private final RetryableService retryableService;
@GetMapping("/retryable")
public String getRetryable(@RequestParam(required = false) Integer intValue) {
return retryableService.getRetryable(intValue);
}
}
package net.suby.failsaferetry.retryable;
import java.util.Random;
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.util.ObjectUtils;
@Service
public class RetryableService {
@Retryable(maxAttempts = 2, backoff = @Backoff(2000), value = IllegalStateException.class,
exclude = { NullPointerException.class, NullPointerException.class })
public String getRetryable(Integer intValue) {
int rand = new Random().nextInt();
if (!ObjectUtils.isEmpty(intValue)) {
rand = intValue;
}
if (rand < 0) {
throw new NullPointerException("값이 음수일수 없습니다.");
} else if (rand == 0) {
throw new NumberFormatException("0의 값입니다.");
} else if (rand % 3 != 0) {
throw new IllegalStateException("fail retry");
}
return String.format("성공 : %s", rand);
}
@Recover
String recover(NullPointerException e) {
return e.getMessage();
}
@Recover
String recover(NumberFormatException e, Integer intValue) {
return String.format("%s : %s", e.getMessage(), intValue);
}
}
● Failsafe.RetryPolicy
1. dependencies 설정
implementation 'net.jodah:failsafe:2.4.3'
2. RetryPolicy 으로 재시도 진행
new RetryPolicy<>()
.handle(IllegalStateException.class) // 재시도 할 Exception
.withDelay(Duration.ofSeconds(5)) // 딜레이 시간
.handleResultIf(result -> result == "fail") // 특정 결과값이면 재시도
.withMaxRetries(3); // 최대 재시도 횟수
package net.suby.failsaferetry.failsafe;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import lombok.RequiredArgsConstructor;
@RestController
@RequiredArgsConstructor
public class FailSafeController {
private final FailSafeService failSafeService;
@GetMapping("/fail-safe")
public String getRetryable(@RequestParam(required = false) Integer intValue) {
return failSafeService.getRetryPolicy(intValue);
}
}
package net.suby.failsaferetry.failsafe;
import java.time.Duration;
import java.util.Random;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;
import net.jodah.failsafe.Failsafe;
import net.jodah.failsafe.RetryPolicy;
@Service
public class FailSafeService {
public String getRetryPolicy(Integer intValue) {
RetryPolicy<Object> retryPolicy = new RetryPolicy<>().handle(IllegalStateException.class, NumberFormatException.class,
NullPointerException.class)
.withDelay(Duration.ofSeconds(5))
.handleResultIf(result -> result == "fail")
.withMaxRetries(3);
return Failsafe.with(retryPolicy).get(() -> getRetryable(intValue));
}
private String getRetryable(Integer intValue) {
int rand = new Random().nextInt();
if (!ObjectUtils.isEmpty(intValue)) {
rand = intValue;
}
if (rand < 0) {
throw new NullPointerException("값이 음수일수 없습니다.");
} else if (rand == 0) {
throw new NumberFormatException("0의 값입니다.");
} else if (rand % 3 != 0) {
throw new IllegalStateException("fail retry");
}
return String.format("성공 : %s", rand);
}
}
● @Retryable vs Failsafe.RetryPolicy
어노테이션으로 사용할 것인지 소스 코드 안에서 사용할 것인지의 차이며 주기능은 동일하며 옵션만 조금 다르다.
개인적으로는 @Retryable이 더 깔끔하고 직관적이여서 읽기도 편한것 같다.
● 참고 URL