REST 기반의 간단한 분산 트랜잭션 구현 - 4편 REST Retry

이 글은 읽기 전에 이전 글까지 모두 읽을 필요는 없습니다. 다만 맥락을 이해하기 위해서 1편은 미리 읽는 편이 좋습니다.



지난 글까지는 REST 기반의 분산 트랜잭션 구현 방법 중 하나인 TCCTry-Confirm/Cancel를 다루었다. 분산 환경에서는 네트워크 오류나 일시적인 서비스 중지 등으로 인해 일시적으로 REST 요청이 실패할 수 있다. 이러한 문제는 REST 요청을 재시도Retry하여 보안할 수 있다.

그림. TCC 시나리오

그림. TCC 시나리오

이번 글은 스프링 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 버전을 관리하고 있기 때문에 별도로 버전을 기술할 필요는 없다.

참고 자료


Popit은 페이스북 댓글만 사용하고 있습니다. 페이스북 로그인 후 글을 보시면 댓글이 나타납니다.