본문으로 바로가기
반응형

1. Servlet 3.0 이전

기존 알고 있었던과 같이 1 Request per 1 Thread 할당 방식이다.

오래걸리는 무거운 작업은 @Async 를 활용해 비동기로 처리할 수 있었지만 이는 말 그대로 해당 스레드 내부에서 유효할 뿐 해당 작업이 끝나기 까지 그 스레드가 반환되지 못하는건 매한가지였다.

톰캣의 NIO 지원은 HTTP Connection 관련 부분을 비동기로 처리하는 것 뿐 서블릿 동작과는 별개의 문제

이 방식의 문제점은 짧게 끝나는 작업이 오래 걸리는 작업이 스레드를 오래 점유하여 덩달아 오래걸리게 되는 문제가 있었다.

2. Servlet 3.0

Servlet 3.0 부터는 이런 문제점을 어느정도 해결하여 오래 걸리는 작업을 별로 스레드에 할당하여 처리하고,

해당 스레드는 빨리 반환하여 다른 요청을 받을 수 있도록 개선하였다.

기존의 HTTP Request, Response를 담당하는 스레드는 서블릿 스레드, 작업을 처리하는 스레드는 작업 스레드(워커 스레드)로 구분된다.

서블릿 스레드 내부에서 비동기 작업(별도 스레드)이 실행되면 해당 작업은 작업 스레드가 처리하고, 현재 서블릿 스레드는 즉시 풀에 반납된다.

Spring 에선 컨트롤러 레이어에서 Callable 등이 반환되면 해당 작업이 작업 스레드에서 시작된다.

3. Servlet 3.1

Servlet 3.0에선 서블릿 스레드의 Request Read, Response Write 작업은 동기방식으로 진행됐었는데, 3.1 부턴 이 작업까지 비동기로 진행하도록 개선되었다.

4. DeferredResult와 Queue 활용

DefferedResult<T> 는 네이밍 그대로 지연된 결과 라는 뜻을 담고 있다.

Spring 컨트롤러 레이어에서 DefferedResult가 반환되면 해당 객체에 결과가 세팅될 때 까지 응답을 보내지 않고 기다린다.

보통 한 곳에서 요청하면 (풀링 개념) DefferedResult 객체를 큐에 저장하고 외부에서 이벤트를 주었을 때 해당 객체에 값이 설정되게 하며 풀링하고 있던 클라이언트에선 그 즉시 결과를 받도록 구현할 수 있다.

DefferedResult의 특징은 워커 스레드를 사용하지 않으며 서블릿 스레드도 즉시 반환한다는 점이다.

DefferedResult 객체만 메모리에 유지되면 결과가 세팅되는 시점에 바로 응답을 보낼 수 있다.

아래 아주 간단한 사용사례이다.

@RestController
public static class MyController {
	Queue<DeferredResult<String>> results = new ConcurrentLinkedQueue<>();
	
	// 아래 코드를 통해 클라이언트가 풀링하는 API
	@GetMapping("/dr")
	public DeferredResult<String> deferredResult() {
		DeferredResult<String> dr = new DeferredResult<>();
		results.add(dr);
		return dr;
	}

	@GetMapping("/dr/count")
	public String drCount() {
		return String.valueOf(results.size());
	}

	// 외부에서 이벤트 발생시키는 API 
	@GetMapping("/dr/event")
	public String drEvent(String msg) {
		for (DeferredResult<String> dr :results) {
			dr.setResult("Hello " + msg); // 이 때 결과가 세팅되며, 이 객체를 풀링하고 있는 곳에선 결과를 받을수있음
			results.remove(dr);
		}
		return "OK";
	}

}

비동기 작업 수행 후 결과를 처리할 때도 사용할 수 있다.

public class MyController {

    @GetMapping
    public ListenableFuture<String> api() {
        ListenableFuture<String> future = doAsyncTask();
				// future.get(); => 이 코드가 호출되는 순간 서블릿 스레드가 점유되어 비동기 의미가 없어짐
				return future;
    }

	  @GetMapping
    public DeferredResult<String> api() {
        DeferredResult<String> dr = new DeferredResult<>();
        ListenableFuture<String> future = doAsyncTask();
				// 콜백을 추가하고 콜백 내에서 DeferredResult에 값을 설정해줌
				future.addCallback(res -> {
						dr.setResult(res);
				}, err -> {
						dr.setErrorResult(err);
				});				
				return dr;
    }
    
}

5. ResponseBodyEmitter

SSE (Server Sent Event)는 HTTP 기반 서버 사이드 스트리밍 기술이라 생각하면 된다. (https://hamait.tistory.com/792)

WebSocket이 서버/클라이언트간 양방향 통신을 지원한다면 SSE는 서버 → 클라이언트로만 데이터를 스트리밍하면 되는 시나리오에 유용하게 사용될 수 있다. (이전의 ajax 롱 풀링, 푸시 등)

실제로 구현하려면 꽤 복잡한 구현이 필요하지만 Spring의 ResponseBodyEmitter 를 사용하면 복잡한 HTTP 레벨 구현 없이 쉽게 스트리밍 API를 구현할 수 있다.

아래 예제는 1초에 1건씩 응답을 보내는 Emitter를 구현한 것이다.

@RestController
public static class MyController {

	@GetMapping("/streaming")
	public ResponseBodyEmitter streaming() {
		ResponseBodyEmitter emitter = new ResponseBodyEmitter();
		Executors.newSingleThreadExecutor().execute(() -> {
			for (int i = 0; i <= 50; i++) {
				try {
					emitter.send("<p> Stream " + i + "</p>"); // 클라이언트로 이벤트 보냄
					Thread.sleep(1000);
				} catch (Exception e) {}
			}
		});
		return emitter;
	}

}
반응형