Servlet 3.0 비동기 처리(Asynchronous Processing) 심층 가이드

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와 같은 스레드 풀을 정의하여 사용하는 것이 리소스 관리 측면에서 권장됩니다.

태그: Servlet javaee AsyncProcessing SpringBoot Multithreading

7월 12일 16:11에 게시됨