스프링 배치 공식 레퍼런스를 한글로 번역한 문서입니다.
전체 목차는 여기에 있습니다.
목차
실패하더라도 몇 번 더 시도하면 성공할 수 있는 작업은,
자동으로 다시 실행하면 좀 더 견고하고 실패하지 않는 프로그램을 만들 수 있다.
간헐적인 실패는 보통 일시적이다.
예제에서는 네트워크 결함이나 데이터베이스 업데이트 중 DeadlockLoserDataAccessException
으로
실패할 수 있는 웹 서비스를 호출한다.
9.1. RetryTemplate
재시도 기능은 스프링 배치 2.2.0 버전부터 제외됐다. 현재는 Spring Retry 라이브러리에 포함되어 있다.
스프링 배치는 자동으로 재시도해주는 RetryOperations
전략이 있다.
RetryOperations
인터페이스는 다음과 같다:
public interface RetryOperations {
<T, E extends Throwable> T execute(RetryCallback<T, E> retryCallback) throws E;
<T, E extends Throwable> T execute(RetryCallback<T, E> retryCallback, RecoveryCallback<T> recoveryCallback)
throws E;
<T, E extends Throwable> T execute(RetryCallback<T, E> retryCallback, RetryState retryState)
throws E, ExhaustedRetryException;
<T, E extends Throwable> T execute(RetryCallback<T, E> retryCallback, RecoveryCallback<T> recoveryCallback,
RetryState retryState) throws E;
}
콜백 인터페이스에 재시도를 위한 비지니스 로직을 넣을 수 있다. 콜백 인터페이스 정의는 다음과 같다:
public interface RetryCallback<T, E extends Throwable> {
T doWithRetry(RetryContext context) throws E;
}
실패하면 (Exception
이 발생함으로써) 콜백이 실행되고,
성공하거나 혹은 콜백 구현체가 그만둘 때까지 재시도한다.
RetryOperations
인터페이스에는 오버로드할 execute
메소드가 많이 있다.
이 메소드로 더 이상 재시도하지 못할 때를 위한 다양한 복구 로직과,
클라이언트와 구현체에서 필요한 각 호출 상태를 담고 있는 retry state를 처리한다
(자세한 내용은 이 챕터 뒤에서 다룬다).
RetryTemplate
은 RetryOperations
의 가장 간단한 구현체다.
아래처럼 사용할 수 있다:
RetryTemplate template = new RetryTemplate();
TimeoutRetryPolicy policy = new TimeoutRetryPolicy();
policy.setTimeout(30000L);
template.setRetryPolicy(policy);
Foo result = template.execute(new RetryCallback<Foo>() {
public Foo doWithRetry(RetryContext context) {
// Do stuff that might fail, e.g. webservice operation
return result;
}
});
위 예제는 웹 서비스를 호출해 결과를 돌려준다. 웹 서비스 호출에 실패하면 타임아웃에 도달할 때까지 재시도한다.
9.1.1. RetryContext
RetryCallback
은 메소드 파라미터로 RetryContext
를 받는다.
컨텍스트를 무시하는 콜백도 많지만,
원한다면 실행을 반복하는 동안 필요한 데이터를 컨텍스트 속성에 저장할 수도 있다.
같은 쓰레드에서 실행한 retry 블록이 중첩돼 있다면 RetryContext
는 부모 컨텍스트를 가진다.
각 실행에서 공유할 데이터를 부모 컨텍스트에 저장하는 경우도 있다.
9.1.2. RecoveryCallback
더 이상 재시도할 수 없으면 RetryOperations
는 RecoveryCallback
이라는
다른 콜백에 제어를 넘긴다.
이 기능을 사용하려면 아래 예제처럼 같은 메소드에 콜백을 함께 넘겨야 한다:
Foo foo = template.execute(new RetryCallback<Foo>() {
public Foo doWithRetry(RetryContext context) {
// business logic here
},
new RecoveryCallback<Foo>() {
Foo recover(RetryContext context) throws Exception {
// recover logic here
}
});
템플릿이 재시도를 그만둘 때까지 비지니스 로직을 성공하지 못하면 recovery 콜백으로 다른 처리를 할 수 있다.
9.1.3. Stateless Retry
가장 간단한 재시도는 단순한 while 루프다.
최종적으로 성공하거나 실패할 때까지 RetryTemplate
으로 단순히 실행을 반복할 수 있다.
재시도할지 중단할지 결정하기 위한 상태를 RetryContext
에 저장하긴 하지만,
이 상태는 스택에 있고 전역적으로 저장할 필요가 없으므로
이를 상태가 없는(stateless) 재시도라고 부른다.
RetryPolicy
구현체에 따라
상태가 없는(stateless) 재시도와 상태가 있는(stateful) 재시도로 나뉜다.
(RetryTemplate
은 둘 다 처리할 수 있다).
상태가 없는(stateless) 재시도는 항상 실패했을 때와 동일한 쓰레드에서 retry 콜백을 실행한다.
9.1.4. Stateful Retry
실패로 인해 트랜잭션이 망가질 수 있다면 다른 점을 함께 고려해야 한다. 원격 호출은 트랜잭션 리소스가 없기 때문에(일반적으로) 논외지만, 데이터베이스에 업데이트하는 경우라면, 특히 하이버네이트를 사용한다면 더 주의해야 한다. 이럴 때는 실패를 유발한 exception을 즉시 다시 던져야 트랜잭션을 롤백하고 유효한 새 트랜잭션을 시작할 수 있다.
트랜잭션과 묶여 있어서 예외를 다시 던지고 롤백한다면,
RetryOperations.execute()
에서 나와 더 이상 스택에 있던 컨텍스트에 접근할 수 없기 때문에
상태가 없는(stateless) 재시도로는 해결할 수 없다.
이를 방지하기 위해 스택을 벗어나 (최소한) 힙에 저장할 수 있는 스토리지 전략을 소개하겠다.
스프링 배치가 제공하는 스토리지 전략은 RetryContextCache
로, RetryTemplate
에
주입할 수 있다.
RetryContextCache
의 디폴트 구현체는 간단한 Map
을 사용해서 메모리에 저장한다.
클러스터 환경에서 멀티 프로세스를 사용하고 있다면 일종의 클러스터 캐시를 사용해서
RetryContextCache
를 구현하는 것을 고려해 보라
(하지만 클러스터 환경이라고 해도 이 기능은 과할 수 있다).
RetryOperations
책임 중 하나는 돌아와서 다시 실행할 때
(보통 새 트랜잭션으로 감싸져 있다) 실패한 명령을 찾아내는 것이다.
스프링 배치는 이를 위한 RetryState
인터페이스를 제공한다.
RetryOperations
인터페이스에는 이 RetryState
를 받는 execute
메소드가 있다.
어떤 작업이 실패했는지는 각 재시도의 상태를 보고 알 수 있다.
이 상태를 식별하려면 유니크 키를 리턴하는 RetryState
가 필요하다.
RetryContextCache
인터페이스에서 이 식별자를 키로 사용한다.
RetryState
가 리턴하는 키에서Object.equals()
메소드와Object.hashCode()
를 구현한다면 매우 조심해야 한다. item을 식별할 수 있는 비지니스 키를 사용하는 것이 가장 좋다. JMS 메세지라면 메세지 ID를 사용할 수 있다.
더 이상 재시도하지 못한다면 RetryCallback
을 호출하는 대신
(이제는 더 호출해도 실패할 것으로 예상되는), 다른 방법으로 실패한 아이템을 처리할 수도 있다.
상태가 없는(stateless) 재시도와 마찬가지로
RetryOperations
의 execute
메소드에 넘겨주는 RecoveryCallback
으로 구현한다.
사실상 재시도 결정은 보통 RetryPolicy
에 위임하므로,
일반적인 제한이나 타임아웃은 RetryPolicy
에 주입할 수 있다
(이 챕터 뒤에서 설명한다).
9.2. Retry Policies
RetryTemplate
의 execute
메소드에서 재시도할지 실패로 처리할지는
RetryContext
의 팩토리이기도 한 RetryPolicy
가 결정한다.
RetryTemplate
의 책임은 현재 policy로 RetryContext
를 생성해
매 시도마다 RetryCallback
에 전달하는 것이다.
콜백이 실패하면 RetryTemplate
은 RetryPolicy
를 호출해
상태(RetryContext
에 저장된) 변경을 요청하고 한 번 더 시도 할지 물어봐야 한다.
policy는 더 이상 재시도할 수 없을 때(limit에 도달하거나 타임아웃 난 경우) 필요한
처리도 담당한다.
간단한 구현체는 RetryExhaustedException
을 던져 감싸고 있는 모든 트랜잭션을 롤백시킨다.
더 정교하게 구현하면 트랜잭션을 유지한 채 복구 액션을 시도할 수도 있다.
재시도로 해결되는 실패인지 아닌지는 이미 정해져 있다. 비지니스 로직에서 같은 exception이 계속 발생한다면 재시도할 필요 없다. 따라서 모든 예외에서 재시도하지 마라. 그보단 재시도할 수 있는 예외에만 초점을 둬라. 재시도를 많이 하는 게 일반적으로 비즈니스 로직에 해가 되진 않지만, 항상 실패한다면, 이미 해결할 수 없는 걸 알고 있는데도 재시도하는 것이기 때문에 시간 낭비다.
스프링 배치는 SimpleRetryPolicy
, TimeoutRetryPolicy
(앞의 예제에서 사용했다)같은
상태가 없는(stateless) RetryPolicy
구현체를 제공한다.
SimpleRetryPolicy
는 알고 있는 예외일 때만 고정된 횟수만큼 재시도한다.
절대 재시도하면 안 되는 “fatal” exception 리스트도 지원하며,
이 리스트는 재시도할 리스트보다 우선시되기 때문에
아래 예제처럼 좀 더 정교하게 설정할 수 있다:
SimpleRetryPolicy policy = new SimpleRetryPolicy();
// Set the max retry attempts
policy.setMaxAttempts(5);
// Retry on all exceptions (this is the default)
policy.setRetryableExceptions(new Class[] {Exception.class});
// ... but never retry IllegalStateException
policy.setFatalExceptions(new Class[] {IllegalStateException.class});
// Use the policy...
RetryTemplate template = new RetryTemplate();
template.setRetryPolicy(policy);
template.execute(new RetryCallback<Foo>() {
public Foo doWithRetry(RetryContext context) {
// business logic here
}
});
구현체 중에는 ExceptionClassifier
인터페이스를 통해
exception마다 다른 재시도 로직을 지원하는, 좀 더 유연한 ExceptionClassifierRetryPolicy
도 있다.
이 policy는 classifier를 호출해 exception을 위임할 RetryPolicy
로 변환한다.
예를 들어 특정 exception에 다른 정책을 매핑해서, 실패했을 때
다른 exception보다 더 많이 재시도해볼 수 있다.
좀 더 세분화해서 결정하려면 자체 retry policy를 구현해야 한다. 예를 들어 커스텀 retry policy로 해결 방법이 명확한 특정 예외를 따로 분류하고, 재시도하거나 혹은 재시도하지 않도록 만들 수 있다.
9.3. Backoff Policies
실패가 일시적이었다면
보통 기다려야만 해결될 때가 많으므로 재시도 전 잠시 기다리는 것도 좋은 방법이다.
RetryCallback
이 실패하면 RetryTemplate
은
BackoffPolicy
에 따라 잠시 실행을 중단시킨다.
BackOffPolicy
인터페이스 정의는 다음과 같다:
public interface BackoffPolicy {
BackOffContext start(RetryContext context);
void backOff(BackOffContext backOffContext)
throws BackOffInterruptedException;
}
BackoffPolicy
의 backOff 메소드를 원하는 방식대로 구현하면 된다.
스프링 배치가 제공하는 모든 policy는 Object.wait()
를 사용한다.
여러 번의 재시도로 step이 지연되고 전부 실패하지 않도록
보통은 재시도할 때마다 매번 더 많은 시간을 기다린다
(이건 이더넷에서 얻은 교훈이다).
스프링 배치는 이를 위한 ExponentialBackoffPolicy
를 제공한다.
9.4. Listeners
가끔은 재시도할 때마다
횡단 관심사(cross cutting concerns)에서도 추가로 콜백을 받고 싶을 수 있다.
스프링 배치는 이를 위한 RetryListener
인터페이스를 지원한다.
RetryTemplate
에 RetryListeners
를 등록하면
매번 RetryContext
, Throwable
과 함께 콜백 받을 수 있다.
RetryListener
인터페이스 정의는 다음과 같다:
public interface RetryListener {
<T, E extends Throwable> boolean open(RetryContext context, RetryCallback<T, E> callback);
<T, E extends Throwable> void onError(RetryContext context, RetryCallback<T, E> callback, Throwable throwable);
<T, E extends Throwable> void close(RetryContext context, RetryCallback<T, E> callback, Throwable throwable);
}
가장 간단하게는 재시도를 할 때마다 전후에 open
, close
로 콜백받고 onError
는 RetryCallback
을 호출할 때마다 적용한다. 에러가 발생한 경우에 close
는 RetryCallback
이 마지막으로 던진 Throwable
을 받는다.
9.5. Declarative Retry
때로는 매번 재시도해야 하는 비지니스 로직도 있다.
외부 서비스를 호출하는 게 가장 대표적인 예시다.
스프링 배치는 이럴 때 사용할,
메소드 호출을 AOP 인터셉터로 감싸고 있는 RetryOperations
구현체를 지원한다.
RetryOperationsInterceptor
는 메소드 호출을 가로채서
RetryTemplate
에 제공한 RetryPolicy
에 따라 재시도한다.
아래 예제는 자바 기반 설정을 사용해서
재시도할 때마다 remoteCall
메소드로 서비스를 호출하게 설정했다
(AOP 인터페이스를 설정에 대한 자세한 정보는 Spring User Guide를 참고하라):
@Bean
public MyService myService() {
ProxyFactory factory = new ProxyFactory(RepeatOperations.class.getClassLoader());
factory.setInterfaces(MyService.class);
factory.setTarget(new MyService());
MyService service = (MyService) factory.getProxy();
JdkRegexpMethodPointcut pointcut = new JdkRegexpMethodPointcut();
pointcut.setPatterns(".*remoteCall.*");
RetryOperationsInterceptor interceptor = new RetryOperationsInterceptor();
((Advised) service).addAdvisor(new DefaultPointcutAdvisor(pointcut, interceptor));
return service;
}
위에 있는 인터셉터는 내부에서 디폴트 RetryTemplate
을 사용한다.
policy나 listener를 바꾸고 싶으면 RetryTemplate
인스턴스를 인터셉터에 주입하면 된다.
Next :Unit Testing
스프링 배치 유닛 테스트 한글 번역
전체 목차는 여기에 있습니다.