Servlet 3.0 비동기 처리 개요
기존의 Servlet 모델(3.0 이전)에서는 클라이언트의 요청이 들어오면 서블릿 컨테이너가 관리하는 스레드 풀에서 스레드 하나를 할당하여 해당 요청을 처음부터 끝까지 처리했습니다. 이 방식은 비즈니스 로직이 길어지거나 I/O 작업이 지연될 경우, 스레드가 응답을 보낼 때까지 차단(Blocking) 상태로 유지되어 스레드 풀이 고갈되는 문제를 일으킬 수 있었습니다.
Servlet 3.0에서 도입된 비동기 처리 기능은 메인 스레드가 요청 처리를 다른 작업 스레드에 위임하고 즉시 스레드 풀로 복귀할 수 있게 해줍니다. 이를 통해 적은 수의 스레드로 더 많은 동시 요청을 처리할 수 있는 고성능 웹 애플리케이션 구현이 가능해졌습니다.
1. 비동기 처리 활성화 및 구현 단계
Step 1: 서블릿 설정
@WebServlet 어노테이션의 asyncSupported 속성을 true로 설정해야 합니다.
@WebServlet(urlPatterns = "/async-example", asyncSupported = true)
public class AsyncProcessServlet extends HttpServlet { ... }
Step 2: 비동기 컨텍스트 시작
request.startAsync() 메서드를 호출하여 AsyncContext를 획득합니다. 이 시점부터 서블릿 컨테이너의 메인 스레드는 자유로워집니다.
AsyncContext ctx = request.startAsync();
Step 3: 비즈니스 로직 수행 및 종료
별도의 스레드에서 작업을 수행한 뒤 ctx.complete()를 호출하여 처리가 완료되었음을 컨테이너에 알립니다.
2. 비동기 처리 예제 실습
예제 1: 별도 스레드에서 비동기 실행
다음은 커스텀 스레드를 생성하여 3초간의 작업을 비동기로 처리하는 예제입니다.
@WebServlet(name = "BasicAsyncServlet", urlPatterns = "/basic-async", asyncSupported = true)
public class BasicAsyncServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
long startTime = System.currentTimeMillis();
System.out.println("메인 스레드 시작: " + Thread.currentThread().getName());
AsyncContext asyncContext = req.startAsync();
new Thread(() -> {
try {
System.out.println("작업 스레드 시작: " + Thread.currentThread().getName());
// 비즈니스 로직 시뮬레이션 (3초 소요)
Thread.sleep(3000);
asyncContext.getResponse().setContentType("text/plain;charset=UTF-8");
asyncContext.getResponse().getWriter().write("비동기 처리 완료!");
// 작업 완료 알림
asyncContext.complete();
System.out.println("작업 스레드 종료");
} catch (Exception e) {
e.printStackTrace();
}
}).start();
long endTime = System.currentTimeMillis();
System.out.println("메인 스레드 종료 (풀 반환): " + (endTime - startTime) + "ms 소요");
}
}
예제 2: Dispatch 방식을 이용한 결과 반환
Spring MVC의 비동기 처리 방식과 유사하게, 별도 스레드에서 결과를 처리한 후 dispatch()를 사용하여 다시 서블릿으로 요청을 전달할 수 있습니다.
@WebServlet(urlPatterns = "/dispatch-async", asyncSupported = true)
public class DispatchAsyncServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
if (req.getDispatcherType() == DispatcherType.ASYNC) {
// 비동기 작업 결과 출력
String taskResult = (String) req.getAttribute("taskResult");
resp.getWriter().write("결과: " + taskResult);
} else {
AsyncContext asyncCtx = req.startAsync();
asyncCtx.start(() -> {
try {
Thread.sleep(2000);
req.setAttribute("taskResult", "성공적으로 데이터 로드");
// 다시 서블릿으로 제어권을 넘김
asyncCtx.dispatch();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
}
}
3. 타임아웃 및 리스너 활용
비동기 작업이 무한정 대기하지 않도록 타임아웃을 설정하고, 상태 변화를 감지하는 리스너를 등록할 수 있습니다.
AsyncContext asyncCtx = req.startAsync();
// 타임아웃 5초 설정
asyncCtx.setTimeout(5000);
asyncCtx.addListener(new AsyncListener() {
@Override
public void onComplete(AsyncEvent event) {
System.out.println("처리 완료");
}
@Override
public void onTimeout(AsyncEvent event) throws IOException {
System.out.println("시간 초과 발생");
event.getAsyncContext().getResponse().getWriter().write("Timeout Error");
event.getAsyncContext().complete();
}
@Override
public void onError(AsyncEvent event) {
System.out.println("에러 발생");
}
@Override
public void onStartAsync(AsyncEvent event) {
System.out.println("비동기 시작");
}
});
4. 실무적 관점의 비동기 처리
비동기 서블릿은 다음과 같은 상황에서 매우 유용합니다.
- 장시간 소요되는 I/O 작업: 파일 업로드/다운로드, 외부 API 호출 등.
- 메시징 시스템 연동: MQ(Message Queue)로부터 결과를 기다려야 하는 경우.
- 서버 푸시 기술: SSE(Server-Sent Events) 또는 롱 폴링(Long Polling) 구현 시 리소스 최적화.
다만, 단순히 스레드를 새로 생성하는 것보다는 ThreadPoolExecutor와 같은 스레드 풀을 정의하여 사용하는 것이 리소스 관리 측면에서 권장됩니다.