728x90


이때까지 여러분들은 Servlet을 동기로 사용했다.

아... 물론 여러분들이 Servlet을 알거라는 전재로 시작했다.

만약 Servlet조차 모른다면 여기의 포스팅들을 참고하길 바란다.

동기와 비동기의 차이를 먼저 아는게 묻는게 먼저일라나?


동기 - 실행 순서가 보장된다. => 순서가 보장되므로 실행 흐름을 읽는게 가능

비동기 - 실행 순서가 보장되지 않는다. => 순서가 보장되지 않으므로 실행흐름을 읽는게 불가능


동기와 비동기의 차이에 대해서 자세히 언급하진 않는다.

사실 위의 설명도 매우 오해의 소지가 다분한 설명이지만 callback이 어쩌구 저쩌구 하는것 보단 나은 설명...인지 솔직히 잘 모르겠다.

어쨋든 이걸 사용하고 싶은 사람이라면 비동기에 대해서 잘 이해하겠지, 언젠가 비동기에 대해서 포스팅을 한번 해야할 것 같다.


사실 사용하는 법은 매우 간단하다. 아래의 예제를 보도록하자.


package example;

import javax.servlet.AsyncContext;
import javax.servlet.AsyncEvent;
import javax.servlet.AsyncListener;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebServlet(urlPatterns = {"/async"}, asyncSupported = true)
public class AsyncServlet extends HttpServlet {

@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

}

@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
final AsyncContext asyncContext = request.startAsync(request, response);
asyncContext.setTimeout(5000);
asyncContext.addListener(new AsyncListener() {
@Override
public void onComplete(AsyncEvent asyncEvent) throws IOException {
System.out.println("AsyncServlet.onComplete");
System.out.println("asyncEvent = [" + asyncEvent + "]");
}

@Override
public void onTimeout(AsyncEvent asyncEvent) throws IOException {
System.out.println("AsyncServlet.onTimeout");
System.out.println("asyncEvent = [" + asyncEvent + "]");
}

@Override
public void onError(AsyncEvent asyncEvent) throws IOException {
System.out.println("AsyncServlet.onError");
System.out.println("asyncEvent = [" + asyncEvent + "]");
}

@Override
public void onStartAsync(AsyncEvent asyncEvent) throws IOException {
System.out.println("AsyncServlet.onStartAsync");
System.out.println("asyncEvent = [" + asyncEvent + "]");
}
});
asyncContext.start(() -> System.out.println("lambda"));
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
asyncContext.complete();
}
}

코드의 전문이다.

항상하는 말이지만 코드가 너무 많다고 당황하지 말자. 단계별로 보면 별거 아니다.


※너무나 당연한 말이지만 번호순서대로 따라가면된다. 0.0001% 정도는 모를거 같아서 첨부해둔다.


1.AsyncContext객체에 request.startAsync로 생성된 인스턴스를 연결한다.


2.AsyncContext가 유지될 시간을 정한다. (사실 안해도됨)


3.AsyncContext의 상태에 따라서 작동될 이벤트 리스너를 연결한다. 콜백루틴이라고 생각하면된다. (이것도 필수아님)


4.AsyncContext에 연결된 쓰레드를 실행시킨다. Runnable이나 Thread를 연결시켜주면된다. (이것도 필수아님)


5.적절한 타이밍에 AsyncContext를 종료시킨다.


여기서 중요한건 사실 1번과 5번이다.

적절한 타이밍에 열어서 적절한 타이밍에 닫는 것이다.

나머지는 있으면 좋고 없으면 기본값으로 설정되는 것이다.


1.AsyncContext객체에 request.startAsync로 생성된 인스턴스를 연결한다.

final AsyncContext asyncContext = request.startAsync(request, response);

먼저 request객체를 async로 전환한다. 이 때 request정보와 response객체를 같이 값으로 넘겨준다.

이제 Servlet의 doGet이나 doPost등의 method함수들은 더이상 큰 의미를 가지지 못한다.

아래 부분들은 사실 더이상 응답과는 무관해진다는 이야기이다.


2.AsyncContext가 유지될 시간을 정한다.

asyncContext.setTimeout(5000);

이제 타임아웃을 정해준다.

이 타임아웃이 지나면 응답은 실패한것이된다.

여러분이 timeout을 정하지 않았다면 자동으로 timeout은 30초가된다.


3.AsyncContext의 상태에 따라서 작동될 이벤트 리스너를 연결한다. 콜백루틴이라고 생각하면된다.

asyncContext.addListener(new AsyncListener() {
@Override
public void onComplete(AsyncEvent asyncEvent) throws IOException {
System.out.println("AsyncServlet.onComplete");
System.out.println("asyncEvent = [" + asyncEvent + "]");
}

@Override
public void onTimeout(AsyncEvent asyncEvent) throws IOException {
System.out.println("AsyncServlet.onTimeout");
System.out.println("asyncEvent = [" + asyncEvent + "]");
}

@Override
public void onError(AsyncEvent asyncEvent) throws IOException {
System.out.println("AsyncServlet.onError");
System.out.println("asyncEvent = [" + asyncEvent + "]");
}

@Override
public void onStartAsync(AsyncEvent asyncEvent) throws IOException {
System.out.println("AsyncServlet.onStartAsync");
System.out.println("asyncEvent = [" + asyncEvent + "]");
}
});

콜백루틴을 달아주는 것이다.

콜백 상황은총 4가지가 있는데 이름만 들어도 각각이 뭘 하는지 알것이다.

이름보고 뭐하는지 모르는 사람이 가뭄에 콩나듯이 존재할것 같긴한데 어짜피 가뭄에 콩안나므로 설명하진 않겠다.

아 참고로 StartAsync는 꽤 사실상 보기가 힘든데 그 이유는 이미 위에서 startAsync가 되는 순간 작동하는데

여러분이 리스너를 단 시기는 이미 지난후기 때문이다.


4.AsyncContext에 연결된 쓰레드를 실행시킨다. Runnable이나 Thread를 연결시켜주면된다.

asyncContext.start(() -> System.out.println("lambda"));

그리고 쓰레드를 실행시킨다. 사실 막상 asnycContext를 써보면 굳이 쓰레드를 쓰는 상황이 많이 나오지는 않는다.

엄밀히 말하면 쓰레드를 안쓰려고 asyncContext를 쓰는건데 쓰레드를 쓰면 성능하락이일어나므로 안쓰는게 좋다.

100명이 접속하면 100개의 쓰레드를 돌리는거라고 생각하면된다... CGI시절도 아니고 비효율 적이다.


쓰레드를 다는 즉시 실행하기 때문에(start메소드를 호출하니까 바로 실행한다고 생각하면된다.) callback보다 항상 먼저 작동한다.

그래서 callback을 호출하기 전에 하고싶은 action을 쓰레드로 동작시킬수 있다.

다시 말하지만 쓰레드를 명시적으로 호출하는건 엄청난 비용이 듦으로 잘생각해서 사용해야 한다.


5.적절한 타이밍에 AsyncContext를 종료시킨다.

asyncContext.complete();

complete를 호출하는 순간 종료된다.

더 설명이 필요한가?

만약 이를 호출하지 않으면 timeout시간이 지나야 종료된다.


이까지 설명했다면 더 이상 설명할게 없다.

이제 내가첨부한 코드를 보며 어떻게 동작할지에 대해서 간략하게 설명하도록 하겠다.


try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}

굉짱히 짧다 실질적으로 예외처리를 제외하면 코드는 한줄이다.


즉 필자가 짠 코드는 lambda라는 글자를 출력하는 쓰레드를 호출한 후 3초동안 Thread를 정지시킨다.

3초후에 바로 asyncContext를 종료시킨다. timeout은 5초이므로 timeout이 작동하지 않고 정상적으로 3초뒤에 종료되게된다.

이제 결과를 보자.


위처럼 작동한다면 예상된 결과다.


이런 비동기를 이용해서 long polling을 구현할 수 있고

(사실 long polling은 thread로도 구현이 가능하지만 아주아주아주비효율적이다.)

따라서 채팅 및 io를 효율적으로 구현할 수 있다.

해당 예제에 대해서는 추후에 블로그에서 다루도록 하겠다.

+ Recent posts