-
[Spring] Spring MVC - 구조 이해 및 기본 기능Study/Spring 2025. 4. 19. 15:44
프론트 컨트롤러
- 프론트 컨트롤러 패턴 특징
- 프론트 컨트롤러를 담당하는 서블릿 하나로 클라이언트의 모든 요청을 받음
- 요청이 들어오는 입구를 하나로 만들어 공통 처리 가능함
- 프론트 컨트롤러가 요청에 맞는 컨트롤러를 찾아서 호출함
- 프론트 컨트롤러를 제외한 나머지 컨트롤러는 서블릿을 사용하지 않아도 됨
- HttpServletRequest, HttpServletResponse 등 서블릿과 관련된 코드를 사용하지 않아도 된다는 의미
- 프론트 컨트롤러에서 호출하기 때문
- 프론트 컨트롤러를 담당하는 서블릿 하나로 클라이언트의 모든 요청을 받음
- Spring Web MVC와 프론트 컨트롤러
- 스프링 웹 MVC의 핵심도 바로 ‘프론트 컨트롤러’
- 스프링 웹 MVC의 ‘DispatcherServlet’이 프론트 컨트롤러 역할을 수행하는 것
스프링 MVC 전체 구조
1) Dispatcher Servlet 구조 살펴보기
- `org.springframework.web.servlet.DispatcherServlet`
- 스프링 MVC도 프론트 컨트롤러 패턴으로 구현되어 있음
- 스프링 MVC의 프론트 컨트롤러가 바로 ‘DispatcherServlet’임
- DispatchServlet 서블릿 등록
- 부모 클래스에서 HttpServlet을 상속 받아서 사용하고, 서블릿으로 동작함
- DispatcherServlet -> FrameworkServlet -> HttpServletBean -> HttpServlet
- 스프링부트는 DispatcherServlet을 서블릿으로 자동으로 등록하면서 모든 경로(urlPatterns=” /”)에 대해서 매핑함
- 부모 클래스에서 HttpServlet을 상속 받아서 사용하고, 서블릿으로 동작함
- 요청 흐름
- 서블릿이 호출되면 HttpServlet이 제공하는 service()가 호출됨
- 스프링 MVC는 DispatcherServlet의 부모인 FrameworkServlet에서 service()를 오버라이딩 해두었음
- FrameworkServlet.service()를 시작으로 여러 메서드가 호출되면서 DispatchServlet.doDispatch()가 호출됨
- 서블릿이 호출되면 HttpServlet이 제공하는 service()가 호출됨
참고) Dispatcher.doDispatch()
더보기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 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; } } if (!mappedHandler.applyPreHandle(processedRequest, response)) { return; } // 3. 핸들러 어댑터 실행 -> 4. 핸들러 어댑터를 통해 핸들러 실행 -> 5. ModelAndView 반환 mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); if (asyncManager.isConcurrentHandlingStarted()) { return; } this.applyDefaultViewName(processedRequest, mv); mappedHandler.applyPostHandle(processedRequest, response, mv); } catch (Exception ex) { dispatchException = ex; } catch (Throwable err) { dispatchException = new ServletException("Handler dispatch failed: " + String.valueOf(err), err); } this.processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); } catch (Exception ex) { triggerAfterCompletion(processedRequest, response, mappedHandler, ex); } catch (Throwable err) { triggerAfterCompletion(processedRequest, response, mappedHandler, new ServletException("Handler processing failed: " + String.valueOf(err), err)); } } ... } ... private void processDispatchResult(HttpServletRequest request, HttpServletResponse response, @Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv, @Nullable Exception exception) throws Exception { ... if (mv != null && !mv.wasCleared()) { // 뷰 렌더링 호출 this.render(mv, request, response); ... } else if (this.logger.isTraceEnabled()) { this.logger.trace("No view rendering, null ModelAndView returned."); } ... } ... protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception { ... View view; if (viewName != null) { // 6. 뷰 리졸버를 통해 뷰 찾기 -> 7. View 반환 view = this.resolveViewName(viewName, mv.getModelInternal(), locale, request); if (view == null) { String var10002 = mv.getViewName(); throw new ServletException("Could not resolve view with name '" + var10002 + "' in servlet with name '" + this.getServletName() + "'"); } } else { view = mv.getView(); if (view == null) { String var9 = String.valueOf(mv); throw new ServletException("ModelAndView [" + var9 + "] neither contains a view name nor a View object in servlet with name '" + this.getServletName() + "'"); } } ... try { if (mv.getStatus() != null) { request.setAttribute(View.RESPONSE_STATUS_ATTRIBUTE, mv.getStatus()); response.setStatus(mv.getStatus().value()); } // 8. 뷰 랜더링 view.render(mv.getModelInternal(), request, response); } catch (Exception var8) { if (this.logger.isDebugEnabled()) { this.logger.debug("Error rendering view [" + String.valueOf(view) + "]", var8); } throw var8; } }
2) Spring MVC 구조
- 요청 전달 : HTTP 요청이 DispatcherServlet으로 전달됨
- 핸들러 조회 : 핸들러 매핑을 통해 요청 URL에 매핑된 핸들러(컨트롤러)를 조회함
- 핸들러 어댑터 조회 : 핸들러를 실행할 수 있는 핸들러 어댑터를 조회함
- 핸들러 어댑터 실행 : 핸들러 어댑터를 실행함
- 핸들러 실행 : 핸들러 어댑터가 실제 핸들러를 실행함
- ModelAndView 반환 : 핸들러 어댑터는 핸들러가 반환하는 정보를 ModelAndView로 변환해서 반환함
- viewResolver 호출 : 뷰 리졸버를 찾고 실행함
- View 반환 : 뷰 리졸버는 뷰의 논리 이름을 물리 이름으로 바꾸고, 렌더링 역할을 담당하는 뷰 객체를 반환함
- 뷰 렌더링 : 뷰를 통해서 뷰를 렌더링함
* 스프링의 핸들러 매핑과 핸들러 어댑터
- 스프링은 어노테이션을 활용한 유연하고, 실용적인 컨트롤러를 만들었는데 이것이 바로 @RequestMapping 어노테이션을 사용하는 컨트롤러임
- @RequestMapping을 사용할 때 가장 우선순위가 높은 핸들러 매핑과 핸들러 어댑터는 RequestMappingHandlerMapping, RequestMappingHandlerAdapter 임
- 이것이 스프링에서 주로 사용하는 어노테이션 기반의 컨트롤러를 지원하는 핸들러 매핑과 어댑터임
HTTP 응답 - HTTP API, 메시지 바디에 직접 입력
- HTTP API를 제공하는 경우에는 HTML이 아니라 데이터를 전달해야 하므로, HTTP 메시지 바디에 JSON 같은 형식으로 데이터를 실어 보냄
- HTML이나 뷰 템플릿을 사용해도 HTTP 응답 메시지 바디에 HTML 데이터가 담겨서 전달됨
- 여기서 설명하는 내용은 정적 리소스나 뷰 템플릿을 거치지 않고, 직접 HTTP 응답 데이터를 전달하는 경우를 말함
응답 반환 방식
더보기package hello.springmvc.basic.response; import hello.springmvc.basic.HelloData; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @Controller public class ResponseBodyController { // 1) @GetMapping("/response-body-string-v1") public void responseBodyV1(HttpServletResponse response) throws IOException { response.getWriter().write("ok"); } // 2) @GetMapping("/response-body-string-v2") public ResponseEntity<String> responseBodyV2() { return new ResponseEntity<>("ok", HttpStatus.OK); } // 3) @ResponseBody @GetMapping("/response-body-string-v3") public String responseBodyV3() { return "ok"; } // 4) @GetMapping("/response-body-json-v1") public ResponseEntity<HelloData> responseBodyJsonV1() { HelloData helloData = new HelloData(); helloData.setUsername("userA"); helloData.setAge(20); return new ResponseEntity<>(helloData, HttpStatus.OK); } // 5) @ResponseStatus(HttpStatus.OK) @ResponseBody @GetMapping("/response-body-json-v2") public HelloData responseBodyJsonV2() { HelloData helloData = new HelloData(); helloData.setUsername("userA"); helloData.setAge(20); return helloData; } }
1) 서블릿을 직접 다룰 때처럼 HttpServletResponse 객체를 통해서 HTTP 메시지 바디에 직접 ok 응답 메시지를 전달함
2) ResponseEntity는 HttpEntity를 상속 받았는데, HttpEntity는 HTTP 메시지의 헤더, 바디 정보를 가지고 있음
- ResponseEntity는 여기에 더해 HTTP 응답 코드를 설정할 수 있음
3) @ResponseBody나 ResponseEntity를 사용하면 View를 사용하지 않고, HTTP 메시지 컨버터를 통해 HTTP 메시지를 직접 입력할 수 있음
4) ResponseEntity를 반환함. HTTP 메시지 컨버터를 통해 JSON 형식으로 변환되어서 반환됨
5) ResponseEntity는 HTTP 응답 코드를 설정할 수 있는데, @ResponseBody를 사용하면 이런 것을 설정하기 까다로움
- 대신, @ResponseStatus(HttpStatus.OK) 어노테이션을 사용하면 응답 코드도 설정할 수 있음
- 어노테이션이기 때문에 응답 코드를 동적으로 변경할 수는 없음
Q. @RestController은?
- @Controller 대신 @RestController 어노테이션을 사용하면, 해당 컨트롤러에 모두 @ResponseBody가 적용되는 효과가 있음
- @RestController는 합성 어노테이션으로 @Controller와 @ResponseBody를 포함하고 있음
- @Controller는 @Component를 포함하고 있으므로 컴포넌트 스캔 대상이 되어 스프링 빈 자동 등록 대상이 됨
- @ResponseBody는 HTTP 메시지 컨버터를 사용해 데이터가 HTTP 메시지 바디에 직접 쓰여지므로 View 템플릿을 사용하지 않고 JSON 타입의 응답 데이터를 반환할 수 있음
HTTP 메시지 컨버터
- 뷰 템플릿으로 HTML을 생성해서 응답하는 것이 아니라, HTTP API처럼 JSON 데이터를 HTTP 메시지 바디에서 직접 읽거나 쓰는 경우 HTTP 메시지 컨버터를 사용하면 편리함
- 스프링 MVC는 다음의 경우에 HTTP 메시지 컨버터를 적용함
- HTTP 요청 : @RequestBody, HttpEntity(RequestEntity)
- HTTP 응답 : @ResponseBody, HttpEntity(ResponseEntity)
1) HTTP 메시지 컨버터 인터페이스
코드)
더보기package org.springframework.http.converter; public interface HttpMessageConverter<T> { boolean canRead(Class<?> clazz, @Nullable MediaType mediaType); boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType); List<MediaType> getSupportedMediaTypes(); default List<MediaType> getSupportedMediaTypes(Class<?> clazz) { return !this.canRead(clazz, (MediaType)null) && !this.canWrite(clazz, (MediaType)null) ? Collections.emptyList() : this.getSupportedMediaTypes(); } T read(Class<? extends T> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException; void write(T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException; }
- HTTP 메시지 컨버터는 HTTP 요청, HTTP 응답 둘 다 사용됨
- canRead(), canWrite() : 메시지 컨버터가 해당 클래스, 미디어 타입을 지원하는지 체크
- read(), write() : 메시지 컨버터를 통해서 메시지를 읽고 쓰는 기능
2) 스프링부트 기본 메시지 컨버터
0 = ByteArrayHttpMessageConverter 1 = StringHttpMessageConverter 2 = MappingJackson2HttpMessageConverter // 일부 생략
- 스프링부트는 다양한 메시지 컨버터를 제공하고, 대상 클래스 타입과 미디어 타입 둘을 체크해 사용 여부를 결정함
- 만약 만족하지 않으면 다음 메시지 컨버터로 넘어감
주요 메시지 컨버터
더보기- ByteArrayHttpMessageConverter: byte[] 데이터 처리
- 클래스 타입 : byte[], 미디어타입 : */* (all)
- 예)
- 요청 : @RequestBody byte[] data
- 응답 : @ResponseBody return byte[], 쓰기 미디어 타입 : application/octet-stream
- StringHttpMessageConverter : String 문자로 데이터 처리
- 클래스 타입 : String, 미디어 타입 : */* (all)
- 예)
- 요청 : @RequestBody String data
- 응답 : @ResponseBody return "ok", 쓰기 미디어 타입 : text/plain
- MappingJackson2HttpMessageConverter
- 클래스 타입 : 객체 또는 HashMap, 미디어 타입 : application/json 관련
- 예)
- 요청 : @RequestBody HelloData data
- 응답 : @ResponseBody return helloData, 쓰기 미디어 타입 : application/json 관련
3) HTTP 요청/응답 처리
1. HTTP 요청 데이터 읽기
- HTTP 요청이 오고, 컨트롤러에서 @RequestBody or HttpEntity 파라미터를 사용함
- 메시지 컨버터가 메시지를 읽을 수 있는지 확인하기 위해 canRead()를 호출함
- 대상 클래스 타입을 지원하는가
- ex) @RequestBody를 명시한 변수의 타입이 byte[], String, 객체 또는 HashMap인지 확인
- HTTP 요청의 Content-Type 미디어 타입을 지원하는가
- ex) text/plain, application/json, */*
- 대상 클래스 타입을 지원하는가
- canRead() 조건을 만족하면 read()를 호출해서 객체 생성 후 반환함
2. HTTP 응답 데이터 생성
- 컨트롤러에서 @ResponseBody, HttpEntity로 값이 반환됨
- 메시지 컨버터가 해당 메시지를 쓸 수 있는지 확인하기 위해 canWrite()를 호출함
- 대상 클래스 타입을 지원하는가
- ex) return의 대상 클래스 타입 확인 (byte[], String, 객체 또는 HashMap)
- HTTP 요청의 Accept 미디어 타입을 지원하는가 (정확히는 @RequestMapping의 produces)
- ex) text/plain, application/json, */*
- 대상 클래스 타입을 지원하는가
- canWrite() 조건을 만족하면 write()를 호출해서 HTTP 응답 메시지 바디에 데이터를 생성함
요청 매핑 핸들러 어댑터 구조
- 스프링은 어노테이션 기반의 컨트롤러를 사용하고, @RequestMapping이 이를 가능하게 해줌
- @RequestMapping을 처리하는 핸들러 어댑터인 RequestMappingHandlerAdapter가 요청/응답 데이터를 처리해줌
1) RequestMappingHandlerAdapter 동작 방식
요약
- 요청값 변환 및 처리 : ArgumentResolver, 응답값 변환 및 처리 : ReturnValueHandler
ArgumentResolver
- 어노테이션 기반의 컨트롤러는 매우 다양한 메서드 파라미터를 사용할 수 있음
- 이렇게 파라미터를 유연하게 처리할 수 있는 이유가 바로 ArgumentResolver 덕분임
- 어노테이션 기반 컨트롤러를 처리하는 RequestMappingHandlerAdapter는 ArgumentResolver를 호출해서 컨트롤러(핸들러)가 필요로 하는 다양한 파라미터의 값(객체)을 생성하고, 파라미터의 값이 모두 준비되면 컨트롤러를 호출하면서 값을 넘겨줌
동작 방식
코드)
더보기package org.springframework.web.method.support; public interface HandlerMethodArgumentResolver { boolean supportsParameter(MethodParameter parameter); @Nullable Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception; }
- ArgumentResolver의 supportsParameter()를 호출해서 대상 ArgumentResolver 구현체가 해당 파라미터를 지원하는지 체크하고, 지원하면 resolveArgument()를 호출해서 실제 객체를 생성함
- 생성된 객체가 컨트롤러 호출 시 전달됨
ReturnValueHandler
- ArgumentResolver와 비슷하고, 대신 응답값을 변환하고 처리함
- 컨트롤러에서 String으로 뷰 이름만 반환해도 동작하는 이유가 바로 ReturnValueHandler가 처리해주기 때문임
코드)
더보기package org.springframework.web.method.support; public interface HandlerMethodReturnValueHandler { boolean supportsReturnType(MethodParameter returnType); void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception; }
Q. 그렇다면 HTTP 메시지 컨버터는 스프링 MVC 구조의 어디에서 사용되는 것일까?
요약
- 요청 : @RequestBody, HttpEntity 타입의 파라미터를 처리하는 ArgumentResolver가 HTTP 메시지 컨버터를 호출해 필요한 객체를 생성함
- 응답 : @ResponseBody, HttpEntity 타입의 반환값을 처리하는 ReturnValueHandler가 HTTP 메시지 컨버터를 호출해 응답 결과를 만듦
HTTP 메시지 컨버터 위치
- 요청의 경우
- @RequestBody와 HttpEntity를 처리하는 각각의 ArgumentResolver가 있음
- ArgumentResolver들이 HTTP 메시지 컨버터를 사용해 필요한 객체를 생성한 후 핸들러(컨트롤러)에게 전달함
- 응답의 경우
- @ResponseBody와 HttpEntity를 처리하는 각각의 ReturnValueHandler가 있음
- ReturnValueHandler가 HTTP 메시지 컨버터를 호출해 응답 결과를 만듦
- 스프링 MVC는 ArgumentResolver, ReturnValueHandler를 둘 다 구현한 구현체를 사용함
- @RequestBody와 @ResponseBody가 있으면, RequestResponseBodyMethodProcessor 사용
- HttpEntity가 있으면, HttpEntityMethodProcessor 사용
참고
'Study > Spring' 카테고리의 다른 글
[Spring] 스프링 핵심 원리 - ThreadLocal (1) 2025.04.19 [Spring] Spring MVC - 필터, 인터셉터 (0) 2025.04.19 [Spring] Spring MVC - 서블릿 (0) 2025.04.17 [Spring] 스프링 핵심 원리 - 빈 스코프 (0) 2025.04.17 [Spring] 스프링 핵심 원리 - 빈 생명주기 콜백 (0) 2025.04.16 - 프론트 컨트롤러 패턴 특징