REST 기반의 간단한 분산 트랜잭션 구현 - 4편 REST Retry
이 글은 읽기 전에 이전 글까지 모두 읽을 필요는 없습니다. 다만 맥락을 이해하기 위해서 1편은 미리 읽는 편이 좋습니다.
- REST 기반의 간단한 분산 트랜잭션 구현 -1편 TCC 개관
- REST 기반의 간단한 분산 트랜잭션 구현 - 2편 TCC Cancel, Timeout
- REST 기반의 간단한 분산 트랜잭션 구현 - 3편 TCC Confirm(Eventual Consistency)
- REST 기반의 간단한 분산 트랜잭션 구현 - 4편 REST Retry
지난 글까지는 REST 기반의 분산 트랜잭션 구현 방법 중 하나인 TCCTry-Confirm/Cancel를 다루었다. 분산 환경에서는 네트워크 오류나 일시적인 서비스 중지 등으로 인해 일시적으로 REST 요청이 실패할 수 있다. 이러한 문제는 REST 요청을 재시도Retry하여 보안할 수 있다.
이번 글은 스프링 Retry 라이브러리를 사용하여 위의 TCC 시나리오에서 REST 요청 실패 시 재시도 하도록 구현한다.
스프링 Retry
스프링 Retry는 스프링 애플리케이션에서 작업이 실패하였을 때 자동으로 재 시도하는 메커니즘을 제공한다. 최초에 스프링 배치Spring Batch에서 시작되었으며, 현재는 분리되어 스프링 배치와 상관없이 독립적으로 사용할 수 있게 되었는데, 주로 템플릿/콜백 패턴Template/Callback Pattern[1]을 구현한 RetryTemplate을 제공하여 프로그래밍할 수 있게 했다.
1 2 3 4 5 6 7 8 9 10 11
// Set the max attempts including the initial attempt before retrying // and retry on all exceptions (this is the default): SimpleRetryPolicy policy = new SimpleRetryPolicy(5, Collections.singletonMap(Exception.class, true)); // Use the policy... RetryTemplate template = new RetryTemplate(); template.setRetryPolicy(policy); template.execute(new RetryCallback<Foo>() { public Foo doWithRetry(RetryContext context) { // business logic here } });
RetryTemplate 이외에도 Java 어노테이션 방식도 지원한다.
1 2 3 4 5 6 7 8 9 10 11
@Service class Service { @Retryable(RemoteAccessException.class) public void service(String str1, String str2) { // ... do something } @Recover public void recover(RemoteAccessException e, String str1, String str2) { // ... error handling making use of original args if required } }
이 글에서는 RetryTemplate를 사용한다.
OrderService
REST 호출 주체가 OrderService이기 때문에 스프링 Retry를 OrderService에 적용한다.
먼저 스프링 Retry를 사용하기 위해 Maven POMProject Object Model에 의존성을 추가한다.[2]
1 2 3 4 5 6 7 8 9 10 11 12 13
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> // ... <dependencies> // ... <dependency> <groupId>org.springframework.retry</groupId> <artifactId>spring-retry</artifactId> </dependency> </dependencies> </project>
그리고 RetryTemplate을 Spring Bean으로 등록한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
@Configuration public class AppConfig { @Bean public RetryTemplate retryTemplate() { RetryTemplate retryTemplate = new RetryTemplate(); // 재시도 간격을 2초로 설정 FixedBackOffPolicy fixedBackOffPolicy = new FixedBackOffPolicy(); fixedBackOffPolicy.setBackOffPeriod(2000l); retryTemplate.setBackOffPolicy(fixedBackOffPolicy); // 재시도 최대 횟수를 5번으로 설정 SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy(); retryPolicy.setMaxAttempts(5); retryTemplate.setRetryPolicy(retryPolicy); return retryTemplate; } }
OrderService에서 REST 연결을 책임지는 객체는 TccRestAdapterImpl이다. TccRestAdapterImpl은 스프링 RestTemplate을 사용하여 REST를 호출하고 있다. REST 호출하는 부분을 RetryTemplate로 감싸서 예외 발생 시 재시도 하도록 작성한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
@Component public class TccRestAdapterImpl implements TccRestAdapter { // ... @Autowired private RetryTemplate retryTemplate; // ... @Override public void confirmAll(List<ParticipantLink> participantLinks) { participantLinks.forEach(participantLink -> { try { retryTemplate.execute((RetryCallback<Void, RestClientException>) context -> { restTemplate.put(participantLink.getUri(), null); return null; }); } catch (RestClientException e) { log.error(String.format("TCC - Confirm Error[URI : %s]", participantLink.getUri().toString()), e); } }); } }
위의 코드에서는 REST PUT 호출(restTemplate.put()) 시 RestClientException이 발생하면 2초 간격으로 재 시도하며 최대 5번 시도한다.
마치며
이전 글에서 결과적 일관성을 아파치 카프카Apache Kafka를 사용하여 구현하였는데, 이 경우 메시지 중복이 문제가 될 수 있다고 언급하였다. 다음 글은 이를 보안하는 멱등성Idempotence에 대해 다룬다.
GitHub
전체 코드는 필자의 GitHub 저장소에서 확인할 수 있다.
주석
[1] 토비의 스프링 3.1 - 3.5 템플릿과 콜백
템플릿
템플릿(template)은 어떤 목적을 위해 미리 만들어둔 모양이 있는 틀을 가르킨다. 학생들이 도형을 그릴 때 사용하는 도형자 또는 모양자가 바로 템플릿이다. 프로그래밍에서는 고정된 틀 안에 바꿀 수 있는 부분을 넣어서 사용하는 경우 템플릿이라고 부른다.
- 이하 중략
콜백콜백(callback)은 실행되는 것을 목적으로 다른 오브젝트의 메소드의 전달되는 오브젝트를 말한다. 파라미터로 전달되지만 값을 참조하기 위한 것이 아니라 특정 로직을 담은 메소드를 실행 시키기 위해 사용한다.
- 이하 중략
[2] OrderService는 스프링 부트를 사용하고 있다. 스프링 부트 부모 POM에서 이미 스프링 Retry 버전을 관리하고 있기 때문에 별도로 버전을 기술할 필요는 없다.
참고 자료
- https://www.baeldung.com/spring-retry
- 클라우드 네이티브 자바 - Isolating Failures and Graceful Degradation