스프링의 디스패처 서블릿은 http 요청을 적절할 컨트롤러에 위임하는 역할을 한다.
디스패처 서블릿을 제대로 이해하기 위해서는 자바 서블릿, 서블릿 컨테이너, MVC에 대한 사전 지식을 필요로 한다.
이에 대한 설명을 먼저 진행하고 디스패처 서블릿에 대해 알아보도록 하겠다.
(이왕 하는거 REST 설명까지 넣었다. 물론 서블릿과 연관이 있는것은 아니다.)
자바 서블릿 (jaca docs)
자바를 사용하여 웹페이지를 동적으로 생성하는 서버측 프로그래밍 기술을 서블릿이라고 한다.
서블릿은 초기화 init 메서드, 종료 시 destroy 메서드, 클라이언트 요청 request를 처리하고 응답 response를 반환하는 service메서드로 이루어져있다.
서블릿의 주 역할이 클리이언트 요청에 대한 응답을 반환하는 것이기에 요청 응답 모델이라고도 한다.
[servlet life-cycle]
- 서블릿이 생성된 이후 초기화 과정은 init메서드를 통해 이루어진다.
- 클라이언트의 요청은 service 메서드를 통해 처리된다.
- 컨테이너 종료시 destory 메서드를 통해 제거 전 처리 작업을 진행
[servlet 동작방식]
- 클라이언트 요청
- was에 의해 Request, Response 생성되고 서블릿 컨테이너로 전달됨
- 서블릿 컨테이너는 web.xml을 분석하여 클라이언트 요청 uri 와 매핑되는 Servlet의 service 메서드 호출
- service메서드에서 request 정보를 추출하여 get, post에 따라 doGet, doPost 등을 실행함
- 응답 반환
[servlet 예제]
@WebServlet("/public/hello")
class HelloServlet : HttpServlet() {
override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) {
resp.writer.write("hello")
}
}
기존에 있는 스프링 프로젝트에서 테스트를 하였다.
부트스트랩 클래스에 @ServletComponentScan를 붙이고 위 클래스를 구현하면 서블릿 동작을 확인할 수 있다. @ServletComponentScan를 붙이면 web.xml에 서블릿을 등록해준다.
따라서 [servlet 동작방식] 2번에 해당하는 처리를 해준다.
서블릿은 위와같이 uri마다 구현해야한다.
참고로 가장 많이 사용되는 HttpServlet 클래스에는 init메서드와 destroy 메서드의 구현부는 아무 처리를 하고 있지 않다.
Servlet을 직접 구현하는 경우 자신의 서비스에 맞는 처리를 직접 구현하면 될 것 같다.
서블릿 컨테이너
서블릿 컨테이너는 was 내부의 서블릿을 관리하는 역할을 한다.
was는 크게 서블릿 컨테이너 외부 영역과 내부 영역으로 나뉜다.
- 서블릿 컨테이너 외부 - 웹서버와 통신 및 스레드 관리
웹서버로 부터 요청이 들어오면 소켓을 통해 요청 데이터(request header)를 읽어들이고 worker 스레드에게 작업을 넘긴다.
(이 부분을 서블릿과 큰 연관성이 없으므로 [I/O 이해하기 3] 톰캣의 요청 수신 방식 글을 참고하기 바란다.) - 서블릿 컨테이너 내부 - 필터 실행 및 서블릿 관리
데이터(request header)를 읽고 worker스레드에게 작업을 넘긴 이후 서블릿 컨텍스트 영역으로 요청이 전달되는데
서블릿 컨텍스트에 요청이 전달되기 전에 ServletRequest와 ServletResponse가 만들어져 전달이 된다.
필터단이 실행되고 클라이언트 요청 uri에 매핑되는 서블릿이 실행되는 영역이다.
MVC 패턴
사용자 인터페이스와 비즈니스 로직을 분리하여 서로 영향없이 프로그래밍을 하는 디자인패턴이다.
모델-뷰-컨트롤러로 구성되어 있으며 각각의 역할은 다음과 같다.
- 모델 : 비즈니스 로직 수행 결과 데이터(뷰에 전달될 데이터)
- 뷰 : 사용자 인터페이스(화면)
- 컨트롤러 : 모델과 뷰로 명령 전달
요청의 흐름은 컨트롤러로부터 시작된다. 컨트롤러는 사용자 요청을 받고 비즈니스 로직을 수행하고 모델의 데이터를 변경시킨다.
컨트롤러는 뷰를 반환하고 뷰는 사용자에게 보여질 화면에 모델의 데이터를 가져온다.
jsp 기반 프로젝트에서 구현은 아래와 같다.
@Controller
public class ProductController {
@GetMapping("/products")
public String listProducts(Model model) {
// 비즈니스 로직 수행 (예: 서비스 호출)
List<Product> products = productService.getAllProducts();
// 모델에 데이터 추가
model.addAttribute("products", products);
// "products.jsp" 뷰를 반환
return "products";
}
}
Rest API 통신을 기반으로한 서버사이드 프로그램에서 모델은 dto나 vo로 대체되고 뷰는 프론트엔드 애플리케이션이 대체되어 사용된다.
REST(Representational State Transfer)
Representational Status Transfer의 약자로 자원의 상태를 일관된 표현으로 전송한다는 개념이다.
- 자원 : 서버에 요청하는 데이터이며 식별할 수 있는 이름인 uri를 갖는다.
- 상태 : 요청하는 시점의 데이터 상태
- 표현 : 자원의 현재 상태를 나타내는 데이터 형식(xml, json)
클라이언트와 서버간의 주고 받는 자원을 URI라는 식별자로 구분하고 Http Method를 통해 crud 작업을 구분하며 정해진 표현으로 일관된 응답 데이터를 받을 수 있게 설계된 아키텍쳐이다.
REST의 5가지 조건
- 인터페이스 일관성 : URI와 http 메서드를 통한 자원을 주고 받는 형식이 일관되야한다.
- 무상태(Stateless): 각 요청 간 클라이언트의 상태가 서버에 저장되어서는 안 된다.
-> 클라이언트의 상태와 무관하게 일관된 처리를 하기 위함 - 캐시 처리 가능 : 클라이언트는 응답을 캐싱할 수 있어야 한다. 잘 관리되는 캐싱은 클라이언트-서버 간 상호작용을 부분적으로 또는 완전하게 제거하여 scalability와 성능을 향상시킨다.
- 계층화: 서버는 여러 계층으로 구성될 수 있다. proxy, gateway와 같은 중간 매체를 이용할 수 있어 시스템 규모를 확장시킬 수 있는 구조를 갖출 수 있다.
- 클라이언트/서버 구조 : 클라이언트는 사용자 화면과 인증 정보 관리 등, 서버는 api 제공과 같이 명확한 역할 분리를 통한 의존성 분리하여 서로 영향을 받지 않도록 함
Code On Demand : 서버가 클라이언트가 실행시킬 수 있는 로직을 전송할 수 있음(클라이언트/서버 구조를 위배하기에 필수 조건이 아님)
Rest 규칙을 지켜 설계한 API를 REST API라고 한다. 위의 5가지 조건을 잘 지키는 것도 중요하지만 무엇보다 URI를 통한 자원을 구분하고 행위를 HTTP Method로 표현하는 것이 중요하다. 단순히 요청에 대한 규칙만 중요하게 여겨서는 안되며 응답 상태 코드 또한 그 규칙을 지키는 것이 중요하다.
상태코드 | |
200 | 성공 |
400 | 요청 형식에 맞지 않음 |
401 | 인증되지 않은 클라이언트 |
403 | 접근 권한 없음(인증은 됬지만 권한이 없는 경우) |
404 | 제공하는 URI 자원 없음 |
500 | 서버 문제 |
스프링 디스패처 서블릿
스프링 웹 프로그래밍에서 사용자의 요청이 어떻게 전달되는지 간단하게 보겠다.
- 클라이언트의 요청이 들어오면 was에 의해 request와 response가 만들어져 서블릿 컨텍스트로 전달된다.
- 서블릿 컨텍스트는 필터를 차례대로 호출하고 디스패처 서블릿에 request와 response를 전달하며 service 메서드를 호출한다.
- 서블릿은 uri와 http 메서드에 대응되는 컨트롤러 메서드를 찾고 인터셉터와 함께 컨트롤러 메서드를 실행시킨다.
이전에 서블릿을 설명할 때, 서블릿이 uri마다 생성되어야하는 것을 예제를 통해 확인했다.
허나, 스프링의 디스패처 서블릿은 컨텍스트에 하나만 존재한다.
이 디스패처 서블릿이 모든 요청을 처리한다.
스프링 디스패처 서블릿의 역할은 모든 클라이언트의 요청을 적합한 컨트롤러 메서드에 위임하는 역할을 한다.
디스패처 서블릿은 프론트 컨트롤러의 역할을 한다. 프론트 컨트롤러란 웹 어플리케이션에서 모든 요청을 받아서 처리하는 컨트롤러를 말한다. 디스패처 서블릿은 프론트 컨트롤러로서 모든 요청을 받아 컨트롤러 메서드에 위임하는 처리를 진행하는 것이다.
코드를 통해 디스패처 서블릿이 어떻게 돌아가는지, 어떤 역할을 하는지에 대해 파악을 해보겠다.
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
processedRequest = this.checkMultipart(request);
multipartRequestParsed = processedRequest != request;
// 1. 적절한 핸들러 가져오기
mappedHandler = this.getHandler(processedRequest);
if (mappedHandler == null) {
this.noHandlerFound(processedRequest, response);
return;
}
// 2. HandlerAdapter 반환
HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());
String method = request.getMethod();
boolean isGet = HttpMethod.GET.matches(method);
if (isGet || HttpMethod.HEAD.matches(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if ((new ServletWebRequest(request, response)).checkNotModified(lastModified) && isGet) {
return;
}
}
// 3. 인터셉터 preHandle 메서드 실행
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// 4. 컨트롤러 메서드 실행
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
this.applyDefaultViewName(processedRequest, mv);
// 5. 인터셉터 postHandle 메서드 실행
mappedHandler.applyPostHandle(processedRequest, response, mv);
} catch (Exception var20) {
dispatchException = var20;
} catch (Throwable var21) {
dispatchException = new ServletException("Handler dispatch failed: " + var21, var21);
}
// 응답 반환 (exception 존재하는 경우 exception에 맞는 뷰 반환)
this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);
} catch (Exception var22) {
// interceptor의 after 메서드 실행
triggerAfterCompletion(processedRequest, response, mappedHandler, var22);
} catch (Throwable var23) {
// interceptor의 after 메서드 실행
triggerAfterCompletion(processedRequest, response, mappedHandler, new ServletException("Handler processing failed: " + var23, var23));
}
} finally {
if (asyncManager.isConcurrentHandlingStarted()) {
if (mappedHandler != null) {
// 비동기 요청의 경우 비동기 interceptor의 after 메서드 실행
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
} else if (multipartRequestParsed) {
// multipart/file 요청의 경우 multipartFile 제거
this.cleanupMultipart(processedRequest);
}
}
}
1. 적절한 핸들러 반환
getHandler 메서드를 통해 적절한 핸들러를 가져온다. 여기서 Handler란 컨트롤러이다.
getHandler의 반환 타입은 HandlerExecutionChain으로 핸들러(컨트롤러)와 인터셉터 리스트로 이루어져있다.
getHandler 메서드를 추적하다보면 AbstractHandlerMethodMapping 클래스의 mappingRegistry 속성에서 핸들러를 가져온다.
AbstractHandlerMethodMapping<T>.MappingRegistry mappingRegistry = new MappingRegistry();
mappingRegistry는 uri와 http 메서드를 키로 컨트롤러와 메서드 정보를 value로 가지고 있다.
2. HandlerAdapter 반환
이전에 추출한 Handler를 통해 handlerAdapter를 가져온다.
핸들러 어댑터는 핸들러 메서드 정보를 인자값으로 받고 request body를 추출하여 자바 객체로 변환하고 restApi의 경우 응답 메시지를 작성하는 처리를 한다.
3. 인터셉터 preHandle 메서드 실행
mappedHandler.applyPreHandle(processedRequest, response)
4. 컨트롤러 메서드 실행
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
위 메서드를 통해 컨트롤러 메서드가 실행된다.
코드를 추적하다보면 RequestMappgingHandlerAdapter의 invokeHandlerMethod를 실행하게 되는데
해당 구현부에서 argumentResolvers를 설정한다.
그리고 다시 핸들러를 실행하는 메서드를 추적하면 InvocableHandlerMethod의 invokeForRequest 메서드를 실행하는데
해당 로직에서 컨트롤러 메서드의 인자값을 추출하고 validator를 통해 인자값을 검증하고 최종적으로 doInvoke 메서드를 통해 핸들러를 실행한다.
invokeForRequest 실행 이후 returnValueHandlers.handleReturnValue를 통해 응답값이 처리되는데 rest api로 설계한 경우 이 메서드를 통해 응답이 바로 반환된다.
따라서 ha.handle을 통해 반환되는 modleAndView 값은 Null이다.
5. 인터셉터 postHandle 메서드 실행
mappedHandler.applyPostHandle(processedRequest, response, mv);
6. 예외 처리
doDispatch 메서드의 경우 삼중 try문으로 작성되어있는데 가장 바깥쪽 try문은 catch 블록이 없다.
finally를 통해 비동기 요청의 경우 비동기 인터셉터의 after 메서드를 실행하고 multipart요청의 경우 multipartFile을 메모리에서 삭제하는 처리를 할 뿐이다.
두번째 try문의 catch 블록은 인터셉터의 after 메서드를 실행한다.
그리고 가장 안쪽 try문의 catch 블록에서는 dispatchException 변수에 exception을 할당하는데 이후 로직에서 processDispatchResult 메서드를 실행한다.
processDispatchResult 메서드의 구현부를 보면 view를 결정하고 인터셉터의 after 메서드를 실행한다.
rest api로 설계한 경우 view가 null이기에 인터셉터의 after 메서드 실행 처리만 한다고 생각하면 된다.
정리
스프링 디스패처 서블릿은 프론트 컨트롤러로서 모든 요청을 최초에 받고 적절한 핸들러(컨트롤러 메서드)를 찾아 요청을 위임한다.
스프링 웹에서 핸들러는 요청에 매핑되어있는 컨트롤러 메서드 뿐만 아니라 인터셉터까지 의미하며 디스패처 서블릿이 인터셉터의 pre, post, after 메서드를 직접 실행시켜 준다.
중첩 try문을 통해 컨트롤러 메서드 실행중 exception이 터지더라도 반드시 실행되는 인터셉터의 after 메서드가 실행됨을 확인하였다.
[참고] 핸들러를 저장하는 mappingRegistry의 동작원리
스프링 WebMvcAutoConfiguration의 아래 메서드에 의해 RequestMappingHandlerMapping가 등록됨
protected RequestMappingHandlerMapping createRequestMappingHandlerMapping() {
return new RequestMappingHandlerMapping();
}
빈 생성과정에서 빈 초기화 메서드를 실행하면 초기화 과정 중에
AbstractAutowireCapableBeanFactory.invokeInitMethods() 메서드를 실행하고 이를 추적하면 아래와 같이 실행됨
-> ((InitializingBean)bean).afterPropertiesSet();
-> AbstractHandlerMethodMapping.initHandlerMethods
-> processCandidateBean
-> detectHandlerMethods
-> registerHandlerMethod
protected void registerHandlerMethod(Object handler, Method method, T mapping) {
this.mappingRegistry.register(mapping, handler, method);
}
따라서 스프링 구동 시점에 빈을 생성하며 @Controller가 붙은 컨트롤러를 handler로 mappingRegistry에 저장함을 알 수 있다.
'Framework & Lib & API > 스프링' 카테고리의 다른 글
스프링 빈 생성 과정 분석 [4] - 디버깅 참고 자료 (0) | 2024.08.07 |
---|---|
[스프링] 스태틱 메서드가 아닌 스프링 싱글톤 빈을 사용해야하는 이유 (0) | 2024.06.29 |
스프링의 트랜잭션 추상화 (1) | 2024.06.25 |
AbstractRoutingDataSource에서 Transactional readonly값 false만 리턴하는 오류 해결 (2) | 2024.06.16 |
스프링 빈 생성 과정 분석 [3] - BeanFactoryPostProcessor, BeanPostProcessor (2) | 2024.04.20 |