비동기 (5)

자바에서 비동기 호출하기 (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에 대해서는 다음 포스팅에서 다루도록 하겠습니다.

 

오늘도 즐프행프하세요~

 

 

💻 Programming/Java

SpringBoot에서 ThreadPoolTaskExecutor 설정하기

이번 포스팅에서는 SpringBoot에서 ThreadPoolTaskExecutor를 어떻게 간단하게 설정하여 사용할 수 있는지 알려드립니다.

SpringBoot 프로젝트는 이미 구성되어 있다는 전제하에 Java11, SpringBoot 2.4.2 기준으로 작성했습니다.

참고로 ThreadPoolTaskExecutor의 설정 및 구체적인 사용법에 대해서는 이전에 포스팅한 문서를 참고하시기 바랍니다.

 

ThreadPoolTaskExecutor의 사용법

 

ThreadPoolTaskExecutor를 이용하여 비동기 처리하기(멀티쓰레드)

async(비동기) 처리를 위한 ThreadPoolTaskExecutor ThreadPoolTaskExecutor를 이용하여 비동기처리하는 방법을 알아보겠습니다. ThreadPoolTaskExecutor는 스프링에서 제공해주는 클래스로 org.springframework.s..

keichee.tistory.com

ThreadPoolTaskExecutor Bean 등록하기

우선 ThreadPoolTaskExecutor를 빈으로 등록하도록 하겠습니다. 

TaskExecutorConfig 클래스를 만들고 @Configuration 어노테이션을 추가하여 설정을 위한 클래스를 하나 만들었습니다.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

@Configuration
public class TaskExecutorConfig {

	@Bean(name = "executor")
	public ThreadPoolTaskExecutor executor() {
		ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
		executor.setThreadNamePrefix("my-");
		return executor;
	}
}

그리고 executor() 메서드를 하나 만들어서 ThreadPoolTaskExecutor를 생성하여 반환해주도록 하고 @Bean으로 등록하면서 name 속성을 지정해주었습니다. 참고로 name 속성의 값은 메서드명과 달라고 무관합니다. 또한, 해당 executor를 이용하여 쓰레드를 실행시켰을 때 내가 만든 ThreadPoolTaskExecutor가 사용되는 것인지 확인할 수 있도록 쓰레드명prefix를 "my-"로 세팅해주었습니다. 

이전 포스팅을 읽어보셨거나 또는 ThreadPoolTaskExecutor를 직업 new 키워드로 생성해서 사용해보신 분은 눈치채셨을 수도 있을텐데요, 여기서 initialize()를 하지 않았습니다. 공식문서에는 initialize 메서드를 호출하는 것으로 나와있지만 굳이 하지 않아도 빈으로 등록될 때 initialize를 하더군요.

 

@Autowired 로 ThreadPoolTaskExecutor 사용하기

아래는 빈으로 등록한 executor를 서비스 레이어에서 autowired하여 사용하는 예제입니다.

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Service
public class TestService {

  @Autowired
  @Qualifier("executor")
  private ThreadPoolTaskExecutor executor;

  public void executeThreads() {
      log.info("executing threads....");
      Runnable r = () -> {
          try {
              log.info(Thread.currentThread().getName() + ", Now sleeping 10 seconds...");
              Thread.sleep(10000);
          } catch (InterruptedException e) {
              e.printStackTrace();
          }
      };

      for (int i = 0; i < 10; i++) {
          executor.execute(r);
      }
  }
}

TestService라는 서비스 클래스에서 멤버변수 executor에 위에서 빈으로 등록한 객체를 쓰도록 @Qualifier로 executor 이름을 명시해주었습니다.

위 코드를 실행해보면 쓰레드 명이 my-1 로 출력되는 것을 확인하실 수 있습니다. corePoolSize를 default 값인 1 로 사용하여 싱글 쓰레드로 실행되기 때문에 my-2, my-3은 보이지 않습니다. corePoolSize 를 2 이상으로 세팅해주면 my-2, my-3 등의 쓰레드도 확인가능합니다.

2021-01-26 18:16:23.147  INFO 16390 --- [           my-1] com.keichee.test.service.TestService     : my-1, Now sleeping 10 seconds...
2021-01-26 18:16:33.150  INFO 16390 --- [           my-1] com.keichee.test.service.TestService     : my-1, Now sleeping 10 seconds...
2021-01-26 18:16:43.152  INFO 16390 --- [           my-1] com.keichee.test.service.TestService     : my-1, Now sleeping 10 seconds...
2021-01-26 18:16:53.157  INFO 16390 --- [           my-1] com.keichee.test.service.TestService     : my-1, Now sleeping 10 seconds...

 

@Async로 ThreadPoolTaskExecutor 사용하기

@Async 어노테이션을 이용하여 위에서 빈으로 등록한 ThreadPoolTaskExecutor를 사용하려면 @EnableAsync 도 추가해주어야 합니다.

따라서 TaskExecutorConfig 클래스에 @EnableAsync 어노테이션을 추가해줍니다.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

@EnableAsync
@Configuration
public class TaskExecutorConfig {

	@Bean(name = "executor")
	public ThreadPoolTaskExecutor executor() {
		ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
		executor.setThreadNamePrefix("my-");
		return executor;
	}
}

 

그리고 서비스 레이어의 메서드를 하나 추가하겠습니다.

import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Service
public class TestService {

    @Async("executor")
    public void task() {
        try {
            log.info(Thread.currentThread().getName() + ", Now sleeping 10 seconds...");
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}

이제 controller 쪽에서 task() 메서드를 loop 돌면서 호출해보면 @Autowired 를 이용한 방법과 동일하게 로그가 출력되는 것을 확인하실 수 있을 겁니다.

 

이상으로 SpringBoot에서 ThreadPoolTaskExecutor 설정하여 사용하는 방법에 대해서 알아보았습니다.

간략하게 다시 정리해보자면

 

1. @Configuration 으로 등록한 클래스에 executor @Bean 추가 (@Async 를 이용할 경우 @EnableAsync 도 추가)

2. @Autowired @Qualifier 로 주입하여 사용하거나 또는 메서드 레벨에 @Async 를 붙여 사용

 

이렇게 정리할 수 있겠네요.

 

그럼 오늘도 행복한 코딩하세요~

💻 Programming/웹프로그래밍

AJAX란? - 서버로부터 응답받기

 

서버 응답

서버로부터 응답을 받기 위해서는 XMLHttpRequest객체의 responseText 또는 responseXML 속성을 사용합니다.

속성설명
responseText응답 데이터를 string으로 받습니다.
responseXML응답 데이터를 XML 로 받습니다.


responseText 속성

예제

document.getElementById("demo").innerHTML = xhttp.responseText;
실습하기 »

responseXML 속성

예제

 cd_catalog.xml 파일을 요청하고 응답으로 온 데이터 파싱하기

xmlDoc = xhttp.responseXML;
txt = "";
x = xmlDoc.getElementsByTagName("ARTIST");
for (i = 0; i < x.length; i++) {
  txt += x[i].childNodes[0].nodeValue + "<br>";
  }
document.getElementById("demo").innerHTML = txt;
실습하기 »


응답을 받는거는 했는데 그렇다면 응답이 왔는지는 어떻게 알 수 있나요? 

자, 그 부분에 대해서는 다음 포스팅에 있을 이벤트에 대해서 공부를 하고나면 아실 수 있습니다.

자, 다음 시간에는 AJAX에서 사용되는 이벤트에 대해서 알아보도록 하겠습니다.





'💻 Programming > 웹프로그래밍' 카테고리의 다른 글

AJAX란? - 데이터베이스  (0) 2016.06.12
AJAX란? - 이벤트 종류  (0) 2016.06.12
AJAX란? - 서버로 요청하기  (1) 2016.06.12
Ajax 란? - Ajax 시작하기  (1) 2016.06.12
[jQuery] 2. jQuery Basics ( 기본 )  (0) 2015.08.06

💻 Programming/웹프로그래밍

AJAX란? - 서버로 요청하기

서버로 요청하기


- 서버로 요청을 할 때에는 XMLHttpRequest객체에서 제공하는 open(), send() 메소드를 사용합니다.


예를들어, 아래처럼 사용합니다.


var xhttp = new XMLHttpRequest();

xhttp.open("GET", "URL", true);

xhttp.send();


이런 식으로 말이죠.


이때 open()과 send()의 메소드 설명은 아래와 같습니다.

메소드설명
open(method, url, async)요청 타입을 정합니다.

method: 요청 타입: GET 또는 POST
url: 서버 (파일) 위치
async: true (비동기식) 또는 false (동기식)
send()서버로 요청을 보낸다 (GET방식에서 사용)
send(string)서버로 요청을 보낸다 (POST방식에서 사용)


GET 방식과 POST방식


GET 은 POST보다 단순하고 빠르며 대부분의 경우에 사용할 수 있습니다.

하지만 아래 경우에는 꼭 POST방식으로 써야합니다:

  • A cached file is not an option (서버에 있는 파일이나 데이터베이스를 업데이트하는 경우).
  • 서버로 많은 양의 데이터 전송이 필요한 경우 (GET방식은 전송할 데이터 사이즈 제한이 있으나 POST 방식은 사이즈 제한이 없습니다).
  • (unknown characters를 포함할 수 있는) 사용자 input을 서버로 전송할 때 , POST 방식이 보안상 GET방식보다 좋습니다.

GET Requests

간단한 GET 요청:

예제1

xhttp.open("GET""demo_get.asp"true);
xhttp.send();
실습하기 »

위 예제에서는 이미 캐쉬된 결과를 볼 수도 있습니다. 그런 경우를 피하려면, 유니크 ID를 URL에 넣어주셔야 합니다.

예제2

xhttp.open("GET""demo_get.asp?t=" + Math.random(), true);
xhttp.send();
실습하기 »

GET 방식으로 요청을 보내고 싶으시면, 정보를 URL에 입력해주시면 됩니다.

예제3

xhttp.open("GET""demo_get2.asp?fname=Henry&lname=Ford"true);
xhttp.send();
실습하기 »

POST Requests

간단한 POST 요청:

예제1

xhttp.open("POST""demo_post.asp"true);
xhttp.send();
실습하기 »

HTML 형식으로 POST 요청을 하려면, setRequestHeader()를 이용하여 HTTP 헤더 정보를 넣어주시면 됩니다. 그리고 send() 메소드에 보내려는 데이터정보를 넣어주시면 됩니다.

예제2

xhttp.open("POST""ajax_test.asp"true);
xhttp.setRequestHeader("Content-type""application/x-www-form-urlencoded");
xhttp.send("fname=Henry&lname=Ford");
실습하기 »

The url - 서버 상의 파일 주소

open() 메소드의 url 파라미터는 서버에 위치한 파일의 주소입니다.

xhttp.open("GET""ajax_test.asp"true);

여기서 말하는 파일은 .txt, .xml 파일처럼 평범한 파일들, 또는 .asp, .php 와 같은 서버스크립팅 파일입니다.  (서버스크립팅 파일들은 서버에서 응답을 보내기 전에 특정 일을 수행할 수 있습니다.).


비동기식 - True or False?

ajax를 사용하려면 3번째 파라미터는 무조건 true여야 합니다.

xhttp.open("GET""ajax_test.asp"true);

Async=true

예제

xhttp.onreadystatechange = function() {
  if (xhttp.readyState == 4 && xhttp.status == 200) {
    document.getElementById("demo").innerHTML = xhttp.responseText;
  }
};
xhttp.open("GET""ajax_info.txt"true);
xhttp.send();
실습하기 »



Async=false

xhttp.open("GET""ajax_info.txt"false);

async=false 로 사용하게되면 더이상 AJAX를 사용하는게 아닌게 되어버립니다. 이렇게 서버로 요청을 하게되면 서버에서 응답이 오기 전까지 화면에서 아무것도 할 수 없는 상황이 발생합니다.

예제

xhttp.open("GET""ajax_info.txt"false);
xhttp.send();
document.getElementById("demo").innerHTML = xhttp.responseText;
실습하기 »



자, 지금까지 AJAX를 이용한 서버로 요청 보내기에 대해서 알아보았습니다.

다음 포스팅에서는 응답을 받는 것에 대해서 알아보도록 하겠습니다.



💻 Programming/웹프로그래밍

Ajax 란? - Ajax 시작하기

AJAX가 뭘까요? 


AJAX는 Asynchronous Javascript And Xml의 약자입니다.


이름에서 알 수 있듯이 비동기식 자바스크립트와 XML을 말합니다. 


이름에 XML이 나오기는 하지만 XML을 알 필요는 없습니다.  


AJAX를 사용하기 위해서는 기본적으로 HTML과 Javascript를 알고있어야 합니다.


그럼 가장먼저 AJAX가 어떻게 동작하는지에 대해서 알아보도록 할까요?


AJAX는 아래와 같은 순서를 거칩니다.


1. 클라이언트(IE 7+, 크롬, 사파리, 파이어폭스, etc. )에서 XMLHttpRequest객체를 생성하고 메시지를 담아 서버로 보낸다.

2. 서버에서 메시지를 받으면 메시지에 해당하는 정보를 읽어서 다시 브라우저한테 응답해준다.

3. 클라이언트 브라우저가 서버로부터 응답을 받으면 해당 정보를 특정 영역에 refresh해준다.


AJAX를 사용함에 있어서 가장 기본은 XMLHttpRequest 객체입니다. 이 객체는 서버로부터 데이터를 전송받을 때 사용이 됩니다. ( IE 5, 6 버전에서는 ActiveXObject 라는 객체가 사용됩니다. )


그럼 이 객체를 어떻게 만드는지 예제를 통해서 알아보겠습니다.

예제1 - XMLHttpRequest 객체 생성하기

var xhttp;
if (window.XMLHttpRequest) {
    xhttp = new XMLHttpRequest();
    } else {
    // code for IE6, IE5
    xhttp = new ActiveXObject("Microsoft.XMLHTTP");
}
연습하기 »


자, 이제 객체를 생성했으니까 이 객체를 이용해서 메시지를 서버로 전송해야겠죠?


다음 포스팅에서 서버로 메시지를 전송해보도록 하겠습니다.