์ž๋ฐ”์—์„œ ๋น„๋™๊ธฐ ํ˜ธ์ถœํ•˜๊ธฐ (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์— ๋Œ€ํ•ด์„œ๋Š” ๋‹ค์Œ ํฌ์ŠคํŒ…์—์„œ ๋‹ค๋ฃจ๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

 

์˜ค๋Š˜๋„ ์ฆํ”„ํ–‰ํ”„ํ•˜์„ธ์š”~