개요
스프링 시큐리티를 사용하지 않고 인증을 구현해보는 작업을 하던 도중, 싱글톤을 주도적으로 사용하는 스프링이 어떻게 비동기적인 유저의 요청에 따른 각각의 컨텍스트(서블릿)들에 대해서 처리(멀티 스레딩)할 수 있는지 궁금해졌다.
싱글톤
싱글톤은 일종의 소프트웨어 디자인 패턴으로서, 생성자가 여러번 호출되더라도 실제로 생성된 객체는 하나이고, 최초의 생성자가 생성한 객체를 리턴하는 방식을 얘기한다. 즉, 프로그램 런타임동안 하나의 인스턴스만 사용한다는 의미이다.
장점
•
단일성: 단일 인스턴스를 사용하기 때문에, 메모리를 효율적으로 사용할 수 있다.
•
접근성: 전역에서 접근할 수 있으므로, 애플리케이션의 모든 부분에서 사용할 수 있다.
단점
•
객체 생성 및 관리가 객체 자체와 긴밀하게 연결되어 의존성 주입이 어려울 수 있다. 싱글톤 인스턴스는 생성자나 다른 메커니즘을 통해 의존성으로 제공되지 않고 시스템의 다른 개체에서 직접 액세스되기 때문이다.
•
테스트시에 각 테스트가 독립적으로 실행되어야 하는데, 싱글톤 인스턴스는 전역적으로 사용되므로 각 테스트시에 독립적인 인스턴스를 만들기 어렵게하므로 테스트 구현에 어려움을 줄 수 있다.
멀티 스레딩
이진코드로 존재하는 실행가능한 파일은 프로그램.
그 프로그램을 실행하여 메모리에 얹게되면 프로세스.
그리고 그 프로세스에서 공유 자원(메모리)를 사용하는 하나의 실행 흐름(단위)는 스레드.
내가 이해하고 있는 바는 위와 같다.
이 상황에서 하나의 프로세스가 여러 개의 작업흐름, 즉 여러 개의 스레드를 통해 실행되고 있다면 그것을 멀티 스레딩이라고 할 수 있다.
ex) 크롬 브라우저라는 하나의 프로세스에는 스크롤이라는 스레드, 즐겨찾기라는 스레드, 화면을 출력하는 스레드 등이 있다고 생각할 수 있다.
자바에서의 멀티스레딩
•
자바
자바에서 멀티 스레딩은 java.lang.Thread 클래스를 사용하여 구현된다.
Thread 클래스를 상속받거나 Runnable 인터페이스를 구현하여 스레드를 생성하고 실행할 수 있는데, 스레드의 우선순위 및 동기화 등을 수동으로 관리할 수 있다.
•
스프링
한편, 스프링은 자바의 Thread 클래스를 직접 사용하지 않으며, 스레드 풀(스레드를 미리 여러 개 만들어 둔 것)을 사용하여 멀티 스레딩을 지원한다.
스프링에서는 TaskExecutor 인터페이스를 사용하여 스레드 풀을 생성하고 사용할 수 있으며, 해당 인터페이스를 구현하여 직접 스레드 풀을 관리할 수도 있다.
또한, @Async 어노테이션을 이용하여 비동기적으로 실행되는 메서드를 간편하게 만들 수 있다.
서블릿 컨테이너(싱글톤)와 서블릿(스레드)
상술했듯이, 스프링에서는 스레드 풀을 이용해서 멀티 스레딩을 수행한다.
스레드 풀을 사용하는 이유는, 스레드를 생성하는 것 자체에 대한 오버헤드(비용)가 꽤 크기 때문에, 미리 생성해두어서 원활하고 빠른 서비스를 제공하기 위함이다.
스프링에서 스레드 풀을 관리하는 것은 Tomcat이다. Tomcat은 WAS(Web Application Server)로서, 간단히 얘기하면 스프링을 통해 애플리케이션을 실행하고 있는 동안, 클라이언트-서버간의 요청과 응답을 원활하게 만들어주는 일종의 다용도 서버라고 생각할 수 있다.
서블릿(Servlet)
서블릿은 동적인 웹 페이지 생성을 위한 자바 클래스다. 서블릿은 서버에서 동작하며, HTTP 요청을 받아들이고, HTTP 응답을 생성하여 클라이언트에게 반환한다. 서블릿은 HttpServlet 클래스를 상속받아 구현되며, 서블릿 컨테이너(Servlet Container)에서 관리된다.
서블릿 컨테이너(Servlet Container)
서블릿 컨테이너는 서블릿의 생명주기를 관리하며, 클라이언트 요청을 받아들이고, 서블릿을 실행하여 요청에 대한 응답을 생성한다. 서블릿의 로딩, 초기화, 실행, 소멸 등의 생명주기를 관리하고, 스레드 풀과 같은 기술을 사용하여 효율으로 처리 할 수 있다. Tomcat이 이 역할을 하는 것이다.
유저의 요청(HttpServletRequest)에 따라 Tomcat은 미리 생성한 스레드 풀에서 스레드를 한 개씩 할당해준다. 그리고 해당 스레드는 작업 흐름에 따라서 Dispatcher Servlet(배송지에 알맞게 배정해주는 부분)을 통해서 요청을 처리하게 된다.
하나의 connection(요청에 따른)에 대해서 스레드가 계속 할당되어 있다면 BIO(Blocking I/O) Connector이며, 대규모 요청이 들어오는 경우 유연한 대처가 힘듦으로 이에 대처하기 위해 스레드를 더 생성하게 되므로 부하가 발생하게 된다.
그렇지 않고, NIO(Non-Blocking I/O) Connector를 이용하여 스레드를 관리하면 여러 개의 스레드가 각각 필요에 따른 작업을 수행하게 되므로 부하를 줄이고 더욱 빠르게 대처할 수 있다.
하지만, NIO Connector 자체는 Thread-Safety를 보장하지는 못한다. 왜냐하면, 서블릿의 컨텍스트는 여전히 각 스레드 자체에 독립적으로 존재하고, 스레드 간에 공유되지 않기 때문이다. 따라서 적절한 동기처리를 설정해주어야 한다.
결론
스프링에서 유저의 비동기적인 요청은 Tomcat의 서블릿 컨테이너에서 스레드 풀로써 관리함을 알 수 있었다.
대부분의 경우에는 기본적으로 BIO Connector를 이용한 멀티 스레딩으로 서블릿을 처리하는 것 같다.
이후에 더 큰 규모의 서비스가 된다면 우선적으로는 스레드 풀을 늘려보고, 코드적으로 개선을 해보고, 더 나아가서는 NIO Connector를 도입해서 적절한 동기화를 설정해주는 방법도 고려해볼 수 있겠다.