Asynchronous (1)

자바에서 비동기 호출하기 (AsyncRestTemplate, ListenableFuture)

안녕하세요, 최근에 관리자용 앱을 개선하다가 비동기 처리에 대해서 공유하고 싶은 것이 있어 포스팅을 작성합니다.

제가 관리중인 관리자용 앱은 Spring Framework 4, Java 8, Bootstrap 3 기반으로 작성되어있습니다.

오래된 라이브러리들을 업그레이드 하고 싶지만 메인 업무가 아니다보니 짬날때 조금씩 업그레이드 해주고 있는데 부트스트랩이나 스프링 버전을 올리기엔 작업량이 많아보여서 안건드리고 있네요.

 

아무튼 자바에서 비동기로 호출을 하는 방법은 크게 두 가지가 있는 것 같습니다.

 

첫 번째는 callee 메서드 쪽에서 @Async 를 붙여서 비동기로 동작하게 하는 방법이 있고요

이 방법은 callee가 외부 팀이거나 외부 회사일 경우 불가능하거나 일이 빨리 진행하기 어려울 수 있습니다.

굳이 특별한 이유가 있지 않은이상 callee쪽에 요청을 할 필요는 전혀 없습니다.

하지만 callee가 내부에 있다면 아키텍처 구조상 이 방법을 선택할 수도 있습니다.

 

두 번째는 caller가 호출방식을 직접 비동기로 호출하는 방법입니다.

callee가 어떻게 처리하던지 caller는 신경쓸 필요가 없죠. 

단지 응답만 잘 받아서 처리해주면 됩니다.

 

오늘 소개해 드릴 내용은 두 번째 방식인 caller 입장에서 비동기로 호출하는 방법입니다.

스프링을 사용하신다면 API 호출하실 때 RestTemplate을 사용하실 겁니다.

 

근데 스프링에서는 비동기 호출을 위해서 이미 AsyncRestTemplate 을 제공하고 있습니다.

AsyncRestTemplate 을 이용해서 API를 호출하는 방법은 RestTemplate과 다를게 없죠.

그리고 당연히도 응답을 기다리지 않고 다음 프로세스를 처리하게 됩니다.

그리고 그 응답은 ListenableFuture<ResponseEntity<T>> 타입으로 받습니다.

간단한 예제를 한번 보겠습니다.

 

    // Autowired로 주입된 상태라 가정합니다.
    private final AsyncRestTemplate asyncRestTemplate;
	
    public void test() {
        URI uri = UriComponentsBuilder.fromUriString("http://localhost")
                .path("/test")
                .build()
                .toUri();

        HttpHeaders headers = new HttpHeaders();
        headers.add(HttpHeaders.CONTENT_TYPE, "application/json;charset=UTF-8");

        HttpEntity<String> requestEntity = new HttpEntity<>(headers);
        HttpMethod httpMethod = HttpMethod.POST;

        ListenableFuture<ResponseEntity<Void>> res = 
        	asyncRestTemplate.exchange(uri, httpMethod, requestEntity, Void.class);
        log.info("호출 성공!!");
    }

위 코드는 http://localhost/test 를 POST 방식으로 호출하고 있습니다. 헤더에 application/json;charset=UTF-8 정보도 넣어주고 있고요 응답메시지에 대해서는 Void로 처리하고 있습니다. 어떤 메시지를 응답으로 주던지 신경안쓴다는 거겠죠.

그리고 호출에 대한 결과를 ListenableFuture<ResponseEntity<Void>> res 에 받게 됩니다.

하지만 응답을 받기 전에 호출하자마자 호출 성공!! 로그를 출력하게 됩니다.

 

그러면 그렇게 받은 응답의 결과에 따라 정상과 실패를 구분해서 예외처리를 해줘야 할텐데 그건 어떻게 할까요?

res 에 callback 함수를 넣어주면 됩니다.

콜백을 추가한 예제를 보겠습니다.

 

    // Autowired로 주입된 상태라 가정합니다.
    private final AsyncRestTemplate asyncRestTemplate;

    public void test() {
        URI uri = UriComponentsBuilder.fromUriString("http://localhost")
                .path("/test")
                .build()
                .toUri();

        HttpHeaders headers = new HttpHeaders();
        headers.add(HttpHeaders.CONTENT_TYPE, "application/json;charset=UTF-8");

        HttpEntity<String> requestEntity = new HttpEntity<>(headers);
        HttpMethod httpMethod = HttpMethod.POST;

        ListenableFuture<ResponseEntity<Void>> res = asyncRestTemplate.exchange(uri, httpMethod, requestEntity, Void.class);
        res.addCallback(new ListenableFutureCallback<ResponseEntity<Void>>() {
            @Override
            public void onFailure(Throwable th) {
                log.error("실패");
            }

            @Override
            public void onSuccess(ResponseEntity<Void> voidResponseEntity) {
                if (HttpUtil.isNot2xxSuccessful(voidResponseEntity)) {
                    log.error("실패, 응답코드:{}", voidResponseEntity.getStatusCodeValue();
                } else {
                    log.info("성공");
                    // TODO: 추가 작업
                }
            }
        });

    }

여기서는 annonymous ListenableFutureCallback 클래스를 만들어서 addCallback 메서드에 넘겨주었습니다.

onFailure 와 onSuccess 메서드를 구현해주면 되는데요 이때 주의할 점이 하나 있습니다.

onSuccess 메서드가 호출되는 케이스가 2xx 성공인 케이스만이 아니라는 점입니다.

4xx 케이스도 onSuccess 메서드를 호출하게 됩니다.

onFailure는 SocketTimeout과 같은 예외가 발생하면 호출됩니다.

따라서 실질적으로 우리가 원하는 성공 은 response entity에서 응답코드로 확인해줘야 합니다.

 

참고로 위 코드에서 HttpUtil.isNot2xxSuccessful(voidResponseEntity) 메서드는 아래처럼 코딩되어 있습니다.

public class HttpUtil {

    public static boolean is2xxSuccessful(ResponseEntity responseEntity) {
        return HttpStatus.Series.valueOf(responseEntity.getStatusCodeValue())
        	.equals(HttpStatus.Series.SUCCESSFUL);
    }

    public static boolean isNot2xxSuccessful(ResponseEntity responseEntity) {
        return !is2xxSuccessful(responseEntity);
    }
}

ListenableFuture<ResponseEntity<T>> 에서 제공하는 addCallback 메서드 시그니처는 아래 두 가지가 있습니다.

 

void addCallback(ListenableFutureCallback<? super T> var1);
void addCallback(SuccessCallback<? super T> var1, FailureCallback var2);

 

위 예제에서는 첫 번째 addCallback 메서드를 사용했습니다만, 만약 SuccessCallback와 FailureCallback을 나눠서 구현하여 사용하고 싶다면 각각 구현해서 addCallback 메서드에 파라미터로 전달해주셔도 됩니다.

 

추가로 AsyncRestTemplate 을 스프링에서 제공해주는 기본 클래스를 사용해도 되지만 타임아웃 등 설정을 바꿔서 사용해야한다면 아래 코드를 참고하시면 도움이 되실겁니다.

    @Bean
    public AsyncRestTemplate asyncRestTemplate2() {
        SimpleAsyncTaskExecutor simpleAsyncTaskExecutor = new SimpleAsyncTaskExecutor();
        simpleAsyncTaskExecutor.setTaskDecorator(new MdcTaskDecorator());

        SimpleClientHttpRequestFactory simpleClientHttpRequestFactory = new SimpleClientHttpRequestFactory();
        simpleClientHttpRequestFactory.setConnectTimeout(30_000);    // 밀리초 단위
        simpleClientHttpRequestFactory.setReadTimeout(30_000);       // 밀리초 단위
        simpleClientHttpRequestFactory.setTaskExecutor(simpleAsyncTaskExecutor);

        AsyncRestTemplate asyncRestTemplate = new AsyncRestTemplate(simpleClientHttpRequestFactory);

        return asyncRestTemplate;
    }

    static class MdcTaskDecorator implements TaskDecorator {

        @Override
        public Runnable decorate(Runnable runnable) {
            // Web thread context
            Map<String, String> contextMap = MDC.getCopyOfContextMap();
            return () -> {
                try {
                    // @Async thread context
                    MDC.setContextMap(contextMap);
                    runnable.run();
                } finally {
                    MDC.clear();
                }
            };
        }
    }

 

이상으로 AsyncRestTemplate을 이용하여 자바에서 비동기 호출하기에 대해서 알아봤습니다.

 

하지만 AsyncRestTemplate은 Spring 5로 넘어가면서 deprecated 되었고, WebClient를 사용하도록 권장하고 있습니다.

Deprecated as of Spring 5.0, in favor of org.springframework.web.reactive.function.client.WebClient

WebClient에 대해서는 다음 포스팅에서 다루도록 하겠습니다.

 

오늘도 즐프행프하세요~