오라클은 2017년 자바EE 8 릴리즈를 마지막으로 오픈소스 SW를 지원하는 비영리 단체인 이클립스 재단에 자바EE 프로젝트를 이관했습니다. 썬 마이크로시스템즈를 인수한 오라클이 사실상 자바EE의 수익화에 실패하면서 기술 주도권을 포기한 것으로 판단됩니다.
오라클이 자바EE 프로젝트는 이관했지만 자바 상표권은 여전히 보유하고 있기 때문에 자바 네임스페이스 사용에 제약이 있었습니다. 이러한 이유로 자카르타EE에서는 자바 네임스페이스가 Jakarta로, API 패키지명은 javax. 에서 Jakarta. 로 변경되었습니다.
자바 서블릿(Java Servlet)은 자바를 사용하여 웹페이지를 동적으로 생성하는 서버측 프로그램을 말하는데요.
쉽게 말하면 클라이언트가 서버에 요청을 보낼 때와 응답을 받을때 필요한 HTTP 작업을 도와주는 녀석이 서블릿 입니다.
아래의 예시를 보겠습니다.
<form action="/save" method = "post">
<input type = "text" name="username" />
<input type = "text" name="password" />
<button type = "submit">전송</button>
</form>
회원의 이름과 비밀번호를 받아서 전송할 수 있는 로직을 수행하는데요.
이 form 을 이용하여 클라이언트가 HTTP Request를 보내면 어떻게 될까요?
POST /save HTTP/1.1
Host: localhost:8080
Content-Type: application/x-www-form-urlencoded
username = "sunghyeon"&password="1234"
그런데 우리가 개발을 할때는 저 비지니스 로직 실행을 제외한 나머지를 일일히 처리한 적이 없다는 것을 알 수 있습니다.
바로 서블릿이 대신 처리해 주기 때문이죠
서블릿은 추상 클래스 HttpServlet
을 extend 해서 만들어집니다.
HttpServlet doc 이 문서를 보는것을 추천한다. 메서드 종류를 보면 직관적이라 이해가 갈것이다.
이 HttpServlet에는 생명주기 관리를 위한 init()
, service()
, destory()
메서드가 있고,
각 메서드의 요청을 처리하기 위한 doGet()
, doPost()
, doPut()
, doDelete()
등의 메서드가 있다.
서블릿을 관리하는 서블릿 컨테이너가 service
메서드를 호출하면 내부 로직에 따라 알맞은 메서드로 연결지어 처리한다.
HttpServletRequest
, HttpServletResponse
두 객체를 생성한다.web.xml
에는 어떤 URL에 어떤 서블릿 컨테이너에 mapping할지 쓰여있고 이에 따라 서블릿으로 이를 전달해준다. @WebServlet
어노테이션을 이용해서도 매핑을 할 수 있다.service
메서드를 호출하고 클라이언트의 요청 종류(Get,Post..)에 따라 doGet
, doPost
... 를 호출한다.doGet
, doPost
... 메서드는 동적으로 페이지를 생성한 후 HttpServletResponse 에 응답을 보낸다.HttpServletRequest
, HttpServletResponse
두 객체를 소멸시킨다.서블릿 객체들을 관리하는 컨테이너인 서블릿 컨테이너가 등장한다.
톰캣처럼 서블릿을 지원하는 WAS를 서블릿 컨테이너라고 한다.
서블릿과 웹서버가 쉽게 통신할 수 있게 해준다.
서블릿 컨테이너는 서블릿의 탄생과 죽음을 관리한다.
요청이 들어왔을 때 서블릿이 로드되어있지 않으면, init()
메서드를 호출하여 서블릿을 인스턴스화 한다.
그리고 서블릿의 service()
를 호출하여 처리한다.
이후에 서블릿이 더이상 요청을 받지 않을 때 destroy()
메서드를 호출하여 서블릿을 제거한다.
서블릿 컨테이너에서는 쓰레드를 만들어 서블릿 객체를 호출하도록 한다.
서블릿 컨테이너는 여러 클라이언트가 서버에 요청을 보내게 되면 여러개의 쓰레드를 생성하고 각 쓰레드는 서블릿 객체를 호출하여 작업을 처리한다.
하지만 모든 요청마다 쓰레드를 생성하게 되면 동시 요청은 처리할 수는 있지만 쓰레드 생성 비용과 쓰레드의 컨텍스트 스위칭 비용이 발생할 수 있다.
그리고 너무 많은 요청이 들어오면 쓰레드를 무한정 생성해야 하기 때문에 CPU의 한계를 넘어서서 서버가 다운될 수 있는 상황까지 초래하게 된다.
그래서 WAS는 아래처럼 쓰레드 풀이라는 개념을 사용한다.
쓰레드 풀은 처음에 일정한 수의 쓰레드를 미리 생성해놓고 클라이언트의 요청이 오면 쓰레드 풀 안에 있는 쓰레드를 할당하고 요청을 처리하고 나면 해당 쓰레드를 다시 반납 받는다.
톰캣은 최대 200개의 쓰레드를 쓰레드 풀에 생성한다.
이로 인해 쓰레드 생성과 종료 비용이 절약되고, 응답 시간이 빨라질 수 있다.
쓰레드가 모두 사용 중인 상황에서는 기다리는 요청에 대해 특정 숫자만큼만 대기하도록 설정할 수 있도록 만들수도 있죠.
결국 핵심은 WAS가 멀티 쓰레드 지원을 통한 동시 요청 처리를 해준다는 것이다.
각 요청 URL마다 별개의 서블릿을 만들면 어떤 문제가 있을까?
새로운 요청이 생길때 마다 새롭게 HttpServlet
을 상속한 클래스를 정의해야하고, Web.xml
에 매핑 내용을 추가해줘야 한다.
이 과정에서 많은 코드의 중복이 발생한다.
이를 해결하기 위해 웹 어플리케이션에서 사용하는 소프트웨어 디자인 패턴이 프론트 컨트롤러 패턴이다.
프론트 컨트롤러(Front Controller)는 이름 그대로 앞 단에서 서블릿 하나로 클라이언트의 모든 요청을 받는다. 그리고 프론트 컨트롤러가 요청에 맞는 컨트롤러를 찾아서 호출하는 패턴이다.
스프링 MVC의 DispatcherServlet이 FrontController 패턴으로 구현 되어있다.
이를 통해 프론트 컨트롤러는 각 컨트롤러에서 부담해야 하는 공통적인 로직을 혼자 처리해줄 수 있게 되었고, 나머지 컨트롤러들은 불필요한 서블릿을 사용하지 않아도 되는 장점이 생기게 되었습니다.
클라이언트의 HTTP 요청은 FrontController에서 받습니다.
FrontController는 requestServlet에서 파라미터를 파싱하고, 사용자 요청에 맞는 컨트롤러를 호출하면서 paramMap과 model 객체를 넘겨줍니다.
사용자 요청에 해당하는 컨트롤러는 paramMap에서 데이터를 받아서 비즈니스 로직을 수행하고 model 객체에 데이터를 다시 담아서 넘겨줍니다. 이 때, 클라이언트에게 돌려줄 ViewName을 같이 보내줍니다. 이 때, ViewName은 Prefix나 Suffix를 제외한 부분만을 리턴합니다.
viewResolver는 컨트롤러가 반환한 ViewName을 정확한 원본 ViewName(MyView)로 만들어줍니다.
FrontController는 MyView 객체를 통해 HttpServletRequest에 model에 대한 데이터를 담고 dispatcher를 통해 view를 Forwarding합니다.
Spring MVC의 DispatcherServlet도 프론트 컨트롤러 패턴을 따르고 있다.
그래서 아래 사진과 같이 서블릿을 하나만 두고 모든 요청을 다 받은 후 그 요청을 처리하기 위해 다른 클래스의 메서드를 호출하는 구조로 되어있다.
여기서 굉장히 많이 헷갈렸는데, 이해를 위해 아주아주 쉽게 생각하면(완전히 정확한 설명은 아니다)
위에서 설명했듯이 Servlet
을 관리하는 Container. 대표적으로 Tomcat
이 있다.
위에서 설명했듯이 Front Controller Pattern
에 따라 만들어진 Servlet
모든 요청을 받아 적절한 class에 위임한다.
Servlet Context
로 보면 된다.Servlet Container
에 servlet
을 등록하면 해당 servlet
이 같은 하나의 작은 컨테이너 역할을 하는 객체DispatcherServlet
이 Servlet
이므로 내부에 Servlet Container
를 갖고 있다.Apllication Context
)를 부모 Context로 사용한다. (상속받는다)Servlet Context
의 부모 (스프링 사용시)BeanFactory
를 사용받는 ContextServlet WebApplicationContext
가 여러개가 있을 수 있다.@Transactional
으로 트랜잭션을 이용해야할 때 ApplicationContext에 있는 Service에서만 트랜잭션이 정상 작동합니다.사실 따로 주제를 빼기는 애매하지만 내가 이 내용을 공부하게 된 계기가 Spring Security에 DelegatingFilter 가 이해가 되지 않아서 공부하기 시작했기 때문에 한번 확실히 적는다.
Servlet Filter
를 사용한다.몇몇 포스팅과 예전 책들을 보면 필터(Filter)는 서블릿 기술이라서 Spring의 빈으로 등록할 수 없다는 내용이 나온다.
상식적으로 스프링 컨테이너보다 큰 범위인 서블릿 컨테이너의 필터가 스프링 컨테이너에 의해 관리된다는 것은 이상하다.
하지만 테스트를 해보면 필터 역시 스프링 빈으로 등록이 가능하며, 빈을 주입 받을 수도 있다.
이러한 잘못된 설명이 나오는 이유는 과거에 실제로 필터(Filter)가 스프링 컨테이너에 의해 관리되지 않았기 때문이다. 그래서 빈으로 등록할 수도 빈을 주입받을 수도 없었다.
하지만 DelegatingFilteProxy
가 ㄷ3ㅡㅇ장하면서 이제 이러한 설명은 더 이상 유효하지 않는데, 해당 내용을 살펴보자
필터는 서블릿이 제공하는 기술이브로 서블릿 컨테이너에 의해 생성 되며 서블릿 컨테이너에 등록이 된다.
그렇기 때문에 스프링의 빈으로 등록할 수도, 주입 받을 수도 없었다.
하지만 필터에서도 DI와 같은 스프링 기술을 필요로 하는 상황이 발생하면서, 스프링 개발자는 스프링 빈을 주입 받을 수 있도록 대안을 마련했는데, 그것이 바로 DelegatingFilterProxy
이다.
Spring 1.2 부터 추가되면서 필터가 스프링에서 관리가 가능해졌다.
DelegatingFilterProxy
는 서블릿 컨테이너에서 관리되는 프록시용 필터로써 우리가 만든 필터를 가지고 있다.
우리가 만든 필터는 스프링 컨테이너의 빈으로 등록되는데, 요청이 오면 DelegationgFilterProxy가 요청을 받아서 우리가 만든 필터(Spring Bean)에게 요청을 위임한다.
1. Filter 구현체가 스프링 빈에 등록됨
2. ServletContext가 Filter 구현체를 갖는 DelgatingFilterProxy를 생성
3. ServletContext가 DelegationgFilterProxy를 서블릿 컨테이너에 필터로 등록
4. 요청이 오면 DelegatingFilterProxy 가 필터 구현체에게 요청을 위임하여 필터 처리
이 그림은 SpringSecurity 공식 레퍼런스 문서에서 가져온 것이다.
필터를 등록하려면 직접 서블릿 컨텍스트에 addFilter로 추가해주어야 했다.
public class MyWebApplicationInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
public void onStartup(ServletContext servletContext) throws ServletException {
super.onStartup(servletContext);
servletContext.addFilter("myFilter", DelegatingFilterProxy.class);
}
}
SpringBoot에서는 DelegatingFilterProxy 조차 필요가 없다.
왜냐하면 SpringBoot가 내장 웹서버를 지원하면서 톰캣과 같은 서블릿 컨테이너까지 SpringBoot가 제어 가능하기 때문이다.😮
SpringBoot가 서블릿 필터의 구현체 빈을 찾으면 DelegatingFilterProxy없이 바로 필터 체인(FilterChain)에 필터를 등록해주기 때문이다. (충격)