@Retryable

비즈니스 로직 코드를 아무리 기깔나게 작성했어도, 원인 불명으로 오류가 나는 경우가 발생하곤 한다.

  • 간헐적인 네트워크 단절로 인한 외부 API 연동 실패
  • 순간적인 네트워크 지연으로 Timeout 발생
  • 기타 등등…

이런 현상이 발생했을 때, 가장 가성비 좋은 방법은 ‘다시 시도해보는 것’ 이다.

사용자가 실시간으로 반응하는 기능들은 이런 현상이 발생했을 때 간단하게 다시 시도하곤 하지만,
배치성격의 비즈니스 로직 전개나, 상태전이 순간에 발생한다면 다시 시도하는 것은 꽤나 까다롭다.

Spring Batch 를 사용하는 것도 좋은 해결 방법이지만,
기존 레거시에 가장 쉽게 적용할 수 있는 것은 역시나 예외가 발생한 비즈니스 로직만 ‘다시 시도하는 것’이다.

Spring 에선 이런 ‘다시 시도해보는 것’을 @Retryable 으로 간단하게 구현 가능하다.

@Retryable 어떻게 쓰나요?

1. 의존성 추가

1
implementation 'org.springframework.retry:spring-retry'

2. EnableRetry 설정

  • Application 혹은 AppConfig 에 @EnableRetry를 추가해준다.
    1
    2
    3
    4
    5
    6
    7
    8
    import org.springframework.context.annotation.Configuration;
    import org.springframework.retry.annotation.EnableRetry;

    @Configuration
    @EnableRetry
    public class AppConfig {
    // ...
    }

3. @Retryable 으로 재시도 할 메서드 설정

속성 정리

  • maxAttempts : Retry 최대 시도 횟수
  • backoff : Retry 시도 시 시간 간격(ms)
  • retryFor : Retry 시도할 target Exception
    • Default는 모든 Exception 에 대하여 재시도한다.
  • noRetryFor : Retry 에선 제외할 Exception
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;

@Service
public class RetryService {

@Retryable(
maxAttempts = 2,
backoff = @Backoff(2000),
retryFor = IllegalStateException.class,
noRetryFor = {NullPointerException.class, IllegalArgumentException.class}
)
public String getRetryable(boolean retryFlag) {
if (retryFlag) {
throw new IllegalStateException();
} else {
return "retry";
}
}
}

주의사항

AOP Proxy 를 사용하기 때문에 호출자가 Spring Component가 아닌 경우 재실행되지 않는다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;

@Service
public class RetryService {

@Retryable(
maxAttempts = 2,
backoff = @Backoff(2000),
retryFor = IllegalStateException.class,
noRetryFor = {NullPointerException.class, IllegalArgumentException.class}
)
public String getRetryable(boolean retryFlag) {
if (retryFlag) {
throw new IllegalStateException();
} else {
return "retry";
}
}

// 만약 외부에서 getRetryableWrapper --> getRetryable 순으로 호출되었고,
// getRetryable 내부에서 exception 발생 시 @Retryable 은 동작하지 않는다.
public String getRetryableWrapper(boolean retryFlag) {
return getRetryable(retryFlag);
}
}

참고자료