[Reactive Java #4] 웹 서버에서의 비동기

YoungHo-Cha·2022년 10월 2일
2

Web Flux

목록 보기
5/6
post-thumbnail

지금까지는 비동기에 대한 워밍업이라고 생각하면 된다. 이제부터 비동기 웹 서버에 대해서 살펴볼 예정이다.

목차

  • 서버의 문제
  • SyncController 작성
  • Callable
  • DeferredResult
  • 추가 내용

서버의 문제

이 시리즈의 첫번째 글을 참고하고오면 좋다.

✅ 참고 글

조금만 더 상세히 보자.

우리가 흔하게 사용하는 전통적인 방식의 서블릿은 다음과 같이 움직인다.

Http Request -> 서블릿 쓰레드 할당 -> request -> 웹 어플리케이션 서버 Worker Thread 할당(할당 시)

근데 여기서 문제는

서블릿 쓰레드는 기본적으로 Blocking I/O 방식이다. Request를 보내고 웹 어플리케이션 서버에서 Response가 올 때까지, blocking에 빠지게 된다.

그래서 사람들은 다음과 같이 생각했다.

worker 쓰레드가 동작하는 동안 서블릿 쓰레드가 다른 요청을 처리할 수 는 없을까? 놀고있자나?

그래서 다음과 같이 비동기 서블릿을 서블릿 3.0, 3.1 버전에서 제공했다.

해결방법

실제로 해결한 내용은 다음과 같다.

서블릿은 request를 빠르게 생성하고 웹 어플리케이션 서버로 request를 넘긴다. 넘긴 후 빠르게 서블릿 쓰레드를 반납시킨다.

그림을 보면 다음과 같다.


출처 : ✅ 토비님의 스트리밍

코드로 살펴보기

Spring에서 어떻게 해결했는지 살펴보자.
이제부터는 Spring이 필요하다.

아래와 같이 프로젝트를 준비하자.

Sync Controller 작성

다음 내용은 Sync로 이루어질 Controller이다.

@RestController
public class AsyncControllerV1 {

     @GetMapping("/async/v1")
    public String asyncV1() throws InterruptedException {
        Thread.sleep(2000);
        log.info("Thread = {}", Thread.currentThread().getName() );
        return "hello";
    }
}

서버를 Run해주고, 웹에 요청해보자.

다음과 같이 나온다.

2초 후, "hello"가 리턴된 것을 볼 수 있다.

로그는 다음과 같이 찍힌다.

이 작업을 수행하는 동안 대기하는 것 또한 알 수 있다.

Callable

이를 비동기적으로 Spring에서 제공해주는 방법이 있다.

정답은 Callable로 리턴해주면 된다.

controller에 다음의 메서드를 추가해보자.

@GetMapping("/async/v2")
    public Callable<String> callable(){
        log.info("함수 실행 --- Thread = {}", Thread.currentThread().getName());
        return () ->{
            Thread.sleep(2000);
            log.info("callable 끝 --- Thread = {}", Thread.currentThread().getName());
            return "hello callable";
        };

    }

위 메서드를 실행하면, 다음과 같이 결과가 나온다.

로그는 다음과 같이 찍힌다.

위의 2가지가 다른 점은 다른 쓰레드에서 동작한다는 것이다.

이렇게하면 헷갈리니 100개의 요청을 한번에 보내보자.

100개의 요청 동시에 요청하기

다음의 클래스를 만들어서 서버에 요청하도록 해보자.

@Slf4j
public class AnotherApplication {
    static AtomicInteger counter = new AtomicInteger(0);
    public static void main(String[] args) throws InterruptedException {
        ExecutorService es = Executors.newFixedThreadPool(100);

        RestTemplate rt = new RestTemplate();
        String url = "http://localhost:8080/async/v2";

        StopWatch main = new StopWatch();
        main.start();

        for (int i = 0; i < 100; i++) {
            es.execute(() -> {
                int idx = counter.addAndGet(1);
                log.info("Thread {}", idx);

                StopWatch sw = new StopWatch();
                sw.start();

                rt.getForObject(url, String.class);

                sw.stop();
                log.info("Elapsed: " + idx + " -> " + sw.getTotalTimeSeconds());
            });
        }
            es.shutdown();
            es.awaitTermination(100, TimeUnit.SECONDS);
            main.stop();
            log.info("Total Time :  -> " + main.getTotalTimeSeconds() );

    }
}

서버의 servlet 설정을 극단적으로 1로 설정해보자.

server.tomcat.threads.max=1

이제 다음으로 요청을 총 2번해보자.

http://localhost:8080/async/v1 // 1

http://localhost:8080/async/v2 // 2

1번 요청은 다음과 같은 시간이 걸린다.

총 203초

2번 요청은 다음과 같은 시간이 걸린다.

총 28초

이유가 뭘까?

정답은 1번은 1개의 요청을 하고 요청 응답까지 sync 형식으로 응답하기 위해서 대기한다. 하지만 2번은 Async로 수행되기 때문에, 먼저 요청을 계~속 보내고 응답이 올 때마다 처리를 한다.

오류

위 코드를 자세히 살펴보면 오류를 발견할 것이다.

servlet 쓰레드는 1개로 고정했지만, worker 쓰레드는 101개 정도로 구성되는 것이다.

이를 해결하기 위해 자바는 "DefferedResult 큐"를 제공한다.

DefferedResult 큐

DefferedResult 큐는 위에서 worker Thread가 무한히 생성되는 것을 방지하기 위해서 큐에 일련의 작업들을 저장하고 한번에 돌린다.

다음 코드를 추가하자.

@RestController
public class AsyncControllerV1 {
    Queue<DeferredResult<String>> results = new ConcurrentLinkedQueue<>();

    @GetMapping("/async/v3")
    public DeferredResult<String> deferredResult(){
        DeferredResult<String> dr = new DeferredResult<>(500000L);
        results.add(dr);
        return dr;
    }

    @GetMapping("/async/v3/deferred-count")
    public String count(){
        return String.valueOf(results.size());
    }

    @GetMapping("/async/v3/deferred-event")
    public String event(String msg){
        for (DeferredResult<String> dr : results){
            dr.setResult("Hello " + msg);
            results.remove(dr);
        }
        return "ok";
    }
}

그리고 코드를 실행해서 확인하자.

  1. "/async/v3" 요청

    요청하면 위와 같이 계속 빙글빙글 돌아가는 것을 볼 수 있다.

  2. "/async/v3/deferred-event?msg=result" 요청

다음과 같이 요청하면, reponse가 ok로 오는 것을 볼 수 있다.

그리고 빙글빙글 돌아가는 요청으로 돌아가서 확인하면 다음과 같이 나온다.

이렇게 할 수 있는 이유는 Spring에서 queue에 이벤트를 저장해두고 이벤트가 발생하면 그걸 쳐다보고있는 곳에 response를 하게 된다.

DeferredResult 100개 요청하기

아까 작성해주었던 요청 클래스에 url을 수정해주자.

@Slf4j
public class AnotherApplication {
    static AtomicInteger counter = new AtomicInteger(0);
    public static void main(String[] args) throws InterruptedException {
        // ... 중략 ...
        String url = "http://localhost:8080/async/v3";

		// ... 중략 ...
	}
}

요청하고 count를 요청해보자.

큐에 100개가 저장되어있는 것을 볼 수 있다.

이제 이벤트를 날려보자.

http://localhost:8080/async/v3/deferred-event?msg=첫번째이벤트

8초만에 모든 것을 해결한 것을 볼 수 있다.

추가 내용

위의 DeferredResult는 한번 응답하면 모든 것이 끝난다. 하지만 스프링에서 제공해주는 ResponseBodyEmitter를 이용하면 여러번 응답할 수 있다.

다음 내용

위에서 100개의 요청에는 오류가 1개 존재한다. 다음 글에서 확인해보자.
그리고 RestTemplate에 대해서 알아보자.

참고

profile
관심많은 영호입니다. 궁금한 거 있으시면 다음 익명 카톡으로 말씀해주시면 가능한 도와드리겠습니다! https://open.kakao.com/o/sE6T84kf

0개의 댓글