Spring이 동시요청을 처리하는 방법
Spring과 멀티 스레드
멀티 스레드란, 일반적으로 하나의 프로세스에 하나의 스레드가 작업하는 것과 달리 하나의 프로세스 내에서 여러 스레드가 동시에 작업을 수행하는 것을 말한다.
우리가 웹 애플리케이션을 이용할 때 여러 Client의 요청이 날아왔을 때, 모든 사용자가 자신이 원하는 작업을 원활히 할 수 있는 이유는 멀티 스레드 때문이다. 만약 싱글 스레드 환경이라면, 웹 애플리케이션에 접속해 요청을 전달한 최초 사용자에게 응답을 보내기 전까지는 다른 사용자들이 해당 서비스를 이용할 수 없을 것이다. 때문에 대다수의 웹 서버는 멀티 스레드를 이용한다고 하며, Spring의 Tomcat 역시 이에 속한다.
즉, 요약하자면 Spring의 Tomcat을 포함한 대다수의 웹 서버는 멀티 스레드 방식을 따르고 Client의 요청이 있을 때마다 Thread를 생성하여 해당 요청을 처리한다고 한다. 단, 이러한 방식에서 요청하는 Client의 숫자가 많아지게 되면 그만큼 Thread를 생성/수거하는 비용 및 오버헤드가 발생하게 된다.
Spring Web MVC에서 요청 마다 Thread가 생성되어 Controller를 통해 요청을 수행할텐데, 어떻게 1개의 Controller만 생성될 수 있을까요?
앞서 설명했듯 Spring에서는 Request별로 Thread가 따로 생성 되고, 이에 따라 각각의 ServletContext를 갖는데 어떻게 Controller가 1개만 생성되는 것일까? 정답은 생성한 Controller 클래스에 대한 정보가 JVM 메모리 영역 중 Method Area(메서드 영역)에 올라가기 때문이다.
Controller 객체 하나를 생성하면 객체 자체는 Heap에 생성되지만, 해당 Class의 정보는 Method Area(메서드 영역 = data영역)에 저장된다. 메소드 영역으로는 모든 Thread가 접근 가능하기 때문에 객체의 Binary Code 정보를 공유할 수 있다.
그래서 각각의 Thread는 singleton으로 생성된 Bean( ⇒ Controller 포함 ) 들을 참고하여 일을 한다. 사실상 이 Thread들은 그 1개의 Singleton Controller 객체를 공유하기에 최종적으로 1개의 Controller만 사용하는 것이다. 즉, 하나의 Singleton Controller가 수많은 request를 처리한다기 보다는, 각각의 Thread가 singleton으로 생성된 Controller를 참고하여 실행한다고 보면 된다.
이렇게 이해를 하다보면, 자원 공유 및 동기화 시에 발생할 수 있는 문제가 떠오를 것이다.
- 원래 동기화를 해주는 이유는 프로세스(Thread)들간 알고있는 정보(상태)를 일치하기 위해서인데, Controller가 내부적으로 상태를 갖는 것이 없으니, 그냥 메소드 호출만 하면 된다. → 공유하는 데이터 즉, 클래스변수, 전역변수를 컨트롤러에서 사용하지 않기 때문에 상태를 갖는 것이 없음
- 그로 인해 굳이 동기화할 이유도 없고 → 그저 처리 로직만 ‘공유되어’ 사용되는 것이다.
- 따라서 스레드는 Controller의 메소드를 공유하고 제각각 호출할 수 있기 때문에 → 들어오는 요청이 1만 개의 요청이든 10만 개의 요청이든 상관없게 된다
Spring Boot에서 다중 요청을 처리하는 방법 - 스레드 풀
여기서 말하는 Thread Pool은 프로그램 실행에 필요한 Thread들을 미리 생성해놓는다는 개념이다.
Tomcat 3.2 이전 버전에서는, 유저의 요청이 들어올 때 마다 Servlet을 실행할 Thread를 하나씩 생성하고 요청이 끝나면 destory했다. 그러나 이 방침은 아래의 두 가지 문제를 야기했다.
- 모든 요청에 대해 스레드를 생성하고 소멸시키는 것은 OS와 JVM에 대해 많은 부담을 안겨준다.
- 동시에 일정 이상의 다수 요청이 들어올 경우 리소스(CPU와 메모리 자원) 소모에 대한 억제가 어렵다. 즉 순간적으로 서버가 다운되거나 동시다발적인 요청을 처리하지 못해서 생기는 문제가 야기 될 수 있다.
그래서 해당 문제를 해결하기 위해, 톰캣은 스레드풀을 활용하기 시작했다.
스레드풀의 기본 플로우는 다음과 같다.
- 첫 작업이 들어오면, core size만큼의 스레드를 생성한다.
- 유저 요청(Connection, Server socket에서 accept한 소캣 객체)이 들어올 때마다 작업 큐(queue)에 담아둔다.
- core size의 스레드 중, 유휴상태(idle)인 스레드가 있다면 작업 큐에서 작업을 꺼내 스레드에 작업을 할당하여 작업을 처리한다.
3-1. 만약 유휴상태인 스레드가 없다면, 작업은 작업 큐에서 대기한다.
3-2. 그 상태가 지속되어 작업 큐가 꽉 찬다면, 스레드를 새로 생성한다.
3-3. 3번과정을 반복하다 스레드 최대 사이즈 에 도달하고 작업큐도 꽉 차게 되면, 추가 요청에 대해선 connection-refused 오류를 반환한다. - 태스크가 완료되면 스레드는 다시 유휴상태로 돌아간다.
4-1. 작업큐가 비어있고 core size이상의 스레드가 생성되어있다면 스레드를 destory한다.
그래서 최종적으로 Spring boot에서 다중 요청이 들어올 때, 다음과 같은 순서로 처리된다.
- 스프링부트는 내장 서블릿 컨테이너인 Tomcat을 이용한다.
- Tomcat은 다중 요청을 처리하기 위해서, 부팅할 때 스레드의 컬렉션인 Thread Pool을 생성한다.
- 유저 요청(HttpServletRequest)가 들어오면 Thread Pool에서 하나씩 Thread를 할당한다. 해당 Thread에서 스프링부트에서 작성한 Dispatcher Servlet을 거쳐 유저 요청을 처리한다.
- 작업을 모두 수행하고 나면 스레드는 스레드풀로 반환된다.
출처