김영한 님의 스프링 MVC 1편 강의를 듣고 정리한 내용을 정리하였습니다
MVC 프레임워크 만들기
프론트 컨트롤러
프론트 컨트롤러 패턴 특징
- 프론트 컨트롤러 서블릿 하나로 클라이언트의 요청을 받은다
- 공통 처리가 가능하다.
- 프론트 컨트롤러를 제외한 나머지 컨트롤러는 서블릿 사용하지 않아도 된다.
프론트 컨트롤러 도입 - V1
ControllerV1
public interface ControllerV1 {
void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}
서블릿과 비슷한 모양의 컨트롤러 인터페이스 도입.
회원등록 컨트롤러
public class MemberFormControllerV1 implements ControllerV1 {
@Override
public void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String viewPath = "/WEB-INF/views/new-form.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
}
}
회원 저장 컨트롤러
public class MemberSaveControllerV1 implements ControllerV1 {
private MemberRepository memberRepository = MemberRepository.getInstance();
@Override
public void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String username = request.getParameter("username");
int age = Integer.parseInt(request.getParameter("age"));
Member member = new Member(username, age);
memberRepository.save(member);
//Model에 데이터를 보관한다.
request.setAttribute("member", member);
String viewPath = "/WEB-INF/views/save-result.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
}
}
회원목록 컨트롤러
public class MemberListControllerV1 implements ControllerV1 {
private MemberRepository memberRepository = MemberRepository.getInstance();
@Override
public void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
List<Member> members = memberRepository.findAll();
request.setAttribute("members", members);
String viewPath = "/WEB-INF/views/members.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
}
}
프론트 컨트롤러
@WebServlet(name = "frontControllerServletV1", urlPatterns = "/front-controller/v1/*")
public class FrontControllerServletV1 extends HttpServlet {
private Map<String, ControllerV1> controllerMap = new HashMap<>();
public FrontControllerServletV1() {
controllerMap.put("/front-controller/v1/members/new-form", new MemberFormControllerV1());
controllerMap.put("/front-controller/v1/members/save", new MemberSaveControllerV1());
controllerMap.put("/front-controller/v1/members", new MemberListControllerV1());
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("FrontControllerServletV1.service");
String requestURI = request.getRequestURI();
ControllerV1 controller = controllerMap.get(requestURI);
if (controller == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
controller.process(request, response);
}
}
url Patterns ="/front-controller/v1/*" 을 통해
/front-controller/v1 을 포함한 하위 요청은 해당 서블릿에서 받아들인다.
controllerMap
key는 매칭 url, 값은 호출될 컨트롤러 클래스를 등록한다.
service()에서 먼저 requestURI를 조회한뒤 실제 호출될 컨트롤러를 controllerMap 에서 찾는다.
없으면 SC_NOT_FOUND 상태 코드를 반환
View 분리 - V2
모든 컨트롤러에서 뷰로 이동하는 부분이 중복이 있고 깔끔하지 않다.
뷰를 처리하는 객체로 해당 부분을 해결한다.
MyView 생성
public class MyView {
private String viewPath;
public MyView(String viewPath) {
this.viewPath = viewPath;
}
public void render(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
}
}
ControllerV2
public interface ControllerV2 {
MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}
컨트롤러가 MyView 뷰 클래스를 반환한다.
회원등록 폼
public class MemberFormControllerV2 implements ControllerV2 {
@Override
public MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
return new MyView("/WEB-INF/views/new-form.jsp");
}
}
회원저장 폼
public class MemberSaveControllerV2 implements ControllerV2 {
private MemberRepository memberRepository = MemberRepository.getInstance();
@Override
public MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String username = request.getParameter("username");
int age = Integer.parseInt(request.getParameter("age"));
Member member = new Member(username, age);
memberRepository.save(member);
//Model에 데이터를 보관한다.
request.setAttribute("member", member);
return new MyView("/WEB-INF/views/save-result.jsp");
}
}
회원목록
public class MemberListControllerV2 implements ControllerV2 {
private MemberRepository memberRepository = MemberRepository.getInstance();
@Override
public MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
List<Member> members = memberRepository.findAll();
request.setAttribute("members", members);
return new MyView("/WEB-INF/views/members.jsp");
}
}
각 컨트롤러에서 dispatcher.foward()를 직접 생성하여 호출하지 않아도 된다.
프론트 컨트롤러 V2
@WebServlet(name = "frontControllerServletV2", urlPatterns = "/front-controller/v2/*")
public class FrontControllerServletV2 extends HttpServlet {
private Map<String, ControllerV2> controllerMap = new HashMap<>();
public FrontControllerServletV2() {
controllerMap.put("/front-controller/v2/members/new-form", new MemberFormControllerV2());
controllerMap.put("/front-controller/v2/members/save", new MemberSaveControllerV2());
controllerMap.put("/front-controller/v2/members", new MemberListControllerV2());
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String requestURI = request.getRequestURI();
ControllerV2 controller = controllerMap.get(requestURI);
if (controller == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
MyView view = controller.process(request, response);
view.render(request, response);
}
}
반환타입이 MyView이므로 controller.process(request, response) 결과로
MyView를 반환받는다.
view.render 호출 시 foward로직을 수행하여 jsp가 실행된다.
public void render(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
}
Model 추가 - V3
- 서블릿 종속성 제거
요청 파라미터 정보는 자바의 Map으로 대신 넘기도록 하면 컨트롤러가 서블릿 기술을 몰라도 동작 가능
request 객체를 Model로 사용하는 대신 별도의 Model객체를 만들어서 반환하면 된다.
이점 : 구현코드의 단순화, 테스트 용이성 증가
- 뷰 이름 중복 제거
컨트롤러에 지정하는 뷰 이름에 중복이 있는걸 확인가능하다.
컨트롤러는 뷰의 논리 이름을 반환하고 실제 물리 위치의 이름은 프론트 컨트롤러에서 처리하도록 단순화 시킨다.
ModelView 추가
컨트롤러에서 서블릿에 종속적인 HttpServletRequest 를 사용했다.
Model도 request.setAttribute()를 통해 데이터 저장후 뷰에 전달했다.
public class ModelView {
private String viewName;
private Map<String, Object> model = new HashMap<>();
public ModelView(String viewName) {
this.viewName = viewName;
}
public String getViewName() {
return viewName;
}
public void setViewName(String viewName) {
this.viewName = viewName;
}
public Map<String, Object> getModel() {
return model;
}
public void setModel(Map<String, Object> model) {
this.model = model;
}
}
뷰의 이름과 뷰를 렌더링할 때 필요한 model 객체를 가지는 ModelView 객체를 생성한다.
model은 단순한 map으로 되어있어, 컨트롤러에서 view에 필요한 데이터를 key , value 로 넣는다
ControllerV3
public interface ControllerV3 {
ModelView process(Map<String, String> paramMap);
}
해당 컨트롤러는 서블릿 기술을 전혀 사용하지 않는다.
구현이 단순해지고, 테스트코드 작성이 쉬워진다.
회원등록 폼
public class MemberFormControllerV3 implements ControllerV3 {
@Override
public ModelView process(Map<String, String> paramMap) {
return new ModelView("new-form");
}
}
회원 저장
public class MemberSaveControllerV3 implements ControllerV3 {
private MemberRepository memberRepository = MemberRepository.getInstance();
@Override
public ModelView process(Map<String, String> paramMap) {
String username = paramMap.get("username");
int age = Integer.parseInt(paramMap.get("age"));
Member member = new Member(username, age);
memberRepository.save(member);
ModelView mv = new ModelView("save-result");
mv.getModel().put("member", member);
return mv;
}
}
파라미터 정보는 map에 담긴다.
map에서 필요한 요청 파라미터를 조회하면 된다.
회원 목록
public class MemberListControllerV3 implements ControllerV3 {
private final MemberRepository memberRepository = MemberRepository.getInstance();
@Override
public ModelView process(Map<String, String> paramMap) {
List<Member> members = memberRepository.findAll();
ModelView mv = new ModelView("members");
mv.getModel().put("members", members);
return mv;
}
}
프론트컨트롤러 V3
@WebServlet(name = "frontControllerServletV3", urlPatterns = "/front-controller/v3/*")
public class FrontControllerServletV3 extends HttpServlet {
private Map<String, ControllerV3> controllerMap = new HashMap<>();
public FrontControllerServletV3() {
controllerMap.put("/front-controller/v3/members/new-form", new MemberFormControllerV3());
controllerMap.put("/front-controller/v3/members/save", new MemberSaveControllerV3());
controllerMap.put("/front-controller/v3/members", new MemberListControllerV3());
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String requestURI = request.getRequestURI();
ControllerV3 controller = controllerMap.get(requestURI);
if (controller == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
Map<String, String> paramMap = createParamMap(request);
ModelView mv = controller.process(paramMap);
String viewName = mv.getViewName();
MyView view = viewResolver(viewName);
view.render(mv.getModel(), request, response);
}
private MyView viewResolver(String viewName) {
return new MyView("/WEB-INF/views/" + viewName + ".jsp");
}
private Map<String, String> createParamMap(HttpServletRequest request) {
Map<String, String> paramMap = new HashMap<>();
request.getParameterNames().asIterator()
.forEachRemaining(paramName -> paramMap.put(paramName, request.getParameter(paramName)));
return paramMap;
}
}
뷰 리졸버
MyView view = viewResolver(viewName)
컨트롤러가 반환한 논리 뷰 이름을 실제 뷰 경로로 변경한다.
그리고 실제 물리경로가 있는 MyView 객체를 반환한다
- 논리 뷰 이름 : members
- 물리 뷰 이름 : /WEB-INF/views/members.jsp
JSP는 request.getAttribute()로 데이터를 조회하기 때문에 모델의 데이터를 꺼내서 request.setAttribute()로 담아둔다.
MyView (수정)
public class MyView {
private String viewPath;
public MyView(String viewPath) {
this.viewPath = viewPath;
}
public void render(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
}
public void render(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
modelToRequestAttribute(model, request);
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
}
private void modelToRequestAttribute(Map<String, Object> model, HttpServletRequest request) {
model.forEach((key, value) -> request.setAttribute(key, value));
}
}
단순하고 실용적 컨트롤러 - V4
v3 컨트롤러는 서블릿 종속성제거, 뷰 경로의 중복을 제거하는 등 잘 설계된 컨트롤러이다.
좀더 실용성 있는 컨트롤러를 개발한다면 이런 구조로 만들 수 있다.
ControllerV4
public interface ControllerV4 {
/**
* @param paramMap
* @param model
* @return viewName
*/
String process(Map<String, String> paramMap, Map<String, Object> model);
}
이번 버전은 인터페이스에 ModelView가 없다.
결과로 View의 이름만 반환한다.
회원 등록 폼
public class MemberFormControllerV4 implements ControllerV4 {
@Override
public String process(Map<String, String> paramMap, Map<String, Object> model) {
return "new-form";
}
}
회원 저장
public class MemberSaveControllerV4 implements ControllerV4 {
private MemberRepository memberRepository = MemberRepository.getInstance();
@Override
public String process(Map<String, String> paramMap, Map<String, Object> model) {
String username = paramMap.get("username");
int age = Integer.parseInt(paramMap.get("age"));
Member member = new Member(username, age);
memberRepository.save(member);
model.put("member", member);
return "save-result";
}
}
모델이 파라미터로 전달되므로 모델을 직접 생성하지 않아도 된다.
회원 목록
public class MemberListControllerV4 implements ControllerV4 {
private final MemberRepository memberRepository = MemberRepository.getInstance();
@Override
public String process(Map<String, String> paramMap, Map<String, Object> model) {
List<Member> members = memberRepository.findAll();
model.put("members", members);
return "members";
}
}
프론트 컨트롤러 V4
@WebServlet(name = "frontControllerServletV4", urlPatterns = "/front-controller/v4/*")
public class FrontControllerServletV4 extends HttpServlet {
private Map<String, ControllerV4> controllerMap = new HashMap<>();
public FrontControllerServletV4() {
controllerMap.put("/front-controller/v4/members/new-form", new MemberFormControllerV4());
controllerMap.put("/front-controller/v4/members/save", new MemberSaveControllerV4());
controllerMap.put("/front-controller/v4/members", new MemberListControllerV4());
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String requestURI = request.getRequestURI();
ControllerV4 controller = controllerMap.get(requestURI);
if (controller == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
Map<String, String> paramMap = createParamMap(request);
Map<String, Object> model = new HashMap<>(); //추가
String viewName = controller.process(paramMap, model);
MyView view = viewResolver(viewName);
view.render(model, request, response);
}
private MyView viewResolver(String viewName) {
return new MyView("/WEB-INF/views/" + viewName + ".jsp");
}
private Map<String, String> createParamMap(HttpServletRequest request) {
Map<String, String> paramMap = new HashMap<>();
request.getParameterNames().asIterator()
.forEachRemaining(paramName -> paramMap.put(paramName, request.getParameter(paramName)));
return paramMap;
}
}
모델 객체 전달
Map<String,Object> model = new HashMap<>();
모델 객체를 프론트 컨트롤러에서 생성해서 넘겨준다.
이를 통해, 컨트롤러에서 모델 객체의 값을 담으면 여기에 그대로 남아있는다.
뷰의 이름을 직접 반환
String viewName = controller.process(paramMap, model);
MyView view = viewResolver(viewName);
컨트롤러가 직접 뷰의 이름을 반환하므로 실제 물리뷰를 찾을 수 있다.
유연한 컨트롤러 - V5
ControllerV3방식과 ControllerV4 방식이 혼합되는 방식을 써야되는 경우 어떻게 해야되는가.
public interface ControllerV3 {
ModelView process(Map<String, String> paramMap);
}
public interface ControllerV4 {
String process(Map<String, String> paramMap, Map<String, Object> model);
}
어댑터 패턴을 사용하면 해결이 가능하다.
해당 패턴을 사용해서 프론트 컨트롤러가 다양한 방식의 컨트롤러를 처리할 수 있게 변경이 가능하다.
핸들러 어댑터
중간에 어댑터 역할을 하는 어댑터가 핸들러 어댑터이다.
여기서 어댑터역할을 함으로써 다양한 종류의 컨트롤러 호출이 가능하다.
핸들러
컨트롤러의 이름을 핸들러로 변경했다.
컨트롤러의 개념뿐 아니라 어떠한 것이든 해당하는 어댑터만 있으면 처리가 가능해지기 때문이다.
MyHandlerAdapter
public interface MyHandlerAdapter {
boolean supports(Object handler);
ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException;
}
boolean supports(Object handler);
handler는 컨트롤러를 말한다.
어댑터가 해당 컨트롤러를 처리할 수 있는지 여부를 판단하는 메서드
ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException;
어댑터는 실제 컨트롤러를 호출하고, 해당 결과로 ModelView를 반환해야한다.
실제 컨트롤러가 ModelView를 반환하지 못하면, 어댑터가 ModelView를 직접 생성해서라도 반환해야한다.
ControllerV3 지원하는 어댑터 클래스
public class ControllerV3HandlerAdapter implements MyHandlerAdapter {
@Override
public boolean supports(Object handler) {
return (handler instanceof ControllerV3);
}
@Override
public ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException {
//MemberFormControllerV3
ControllerV3 controller = (ControllerV3) handler;
Map<String, String> paramMap = createParamMap(request);
ModelView mv = controller.process(paramMap);
return mv;
}
private Map<String, String> createParamMap(HttpServletRequest request) {
Map<String, String> paramMap = new HashMap<>();
request.getParameterNames().asIterator()
.forEachRemaining(paramName -> paramMap.put(paramName, request.getParameter(paramName)));
return paramMap;
}
}
supprorts(Obejct handler) 메서드를 통해 ControllerV3 만 처리한다는걸 알 수 있다.
ControllerV4 지원하는 어댑터 클래스
public class ControllerV4HandlerAdapter implements MyHandlerAdapter {
@Override
public boolean supports(Object handler) {
return (handler instanceof ControllerV4);
}
@Override
public ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException {
ControllerV4 controller = (ControllerV4) handler;
Map<String, String> paramMap = createParamMap(request);
HashMap<String, Object> model = new HashMap<>();
String viewName = controller.process(paramMap, model);
ModelView mv = new ModelView(viewName);
mv.setModel(model);
return mv;
}
private Map<String, String> createParamMap(HttpServletRequest request) {
Map<String, String> paramMap = new HashMap<>();
request.getParameterNames().asIterator()
.forEachRemaining(paramName -> paramMap.put(paramName, request.getParameter(paramName)));
return paramMap;
}
}
supports()메서드를 통해 handler가 ControllerV4인 경우에만 처리함을 알 수 있다.
어댑터 변환
ModelView mv = new ModelView(viewName);
mv.setModel(model);
return mv;
어댑터가 호출하는 ControllerV4는 뷰의 이름을 반환한다.
그런데 어댑터는 뷰의 이름이 아니라 ModelView를 만들어서 반환해야한다.
따라서 ModelView로 만들어서 형식을 맞추어 반환한다.
프론트컨트롤러 V5
@WebServlet(name = "frontControllerServletV5", urlPatterns = "/front-controller/v5/*")
public class FrontControllerServletV5 extends HttpServlet {
private final Map<String, Object> handlerMappingMap = new HashMap<>();
private final List<MyHandlerAdapter> handlerAdapters = new ArrayList<>();
public FrontControllerServletV5() {
initHandlerMappingMap(); // 핸들러 매핑 초기화
initHandlerAdapters(); // 어댑터 초기화
}
private void initHandlerMappingMap() {
handlerMappingMap.put("/front-controller/v5/v3/members/new-form", new MemberFormControllerV3());
handlerMappingMap.put("/front-controller/v5/v3/members/save", new MemberSaveControllerV3());
handlerMappingMap.put("/front-controller/v5/v3/members", new MemberListControllerV3());
//V4 추가
handlerMappingMap.put("/front-controller/v5/v4/members/new-form", new MemberFormControllerV4());
handlerMappingMap.put("/front-controller/v5/v4/members/save", new MemberSaveControllerV4());
handlerMappingMap.put("/front-controller/v5/v4/members", new MemberListControllerV4());
}
private void initHandlerAdapters() {
handlerAdapters.add(new ControllerV3HandlerAdapter());
handlerAdapters.add(new ControllerV4HandlerAdapter());
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
Object handler = getHandler(request);
if (handler == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
MyHandlerAdapter adapter = getHandlerAdapter(handler);
//어댑터 호출
ModelView mv = adapter.handle(request, response, handler);
String viewName = mv.getViewName();
MyView view = viewResolver(viewName);
view.render(mv.getModel(), request, response);
}
// 핸들러 매핑
private Object getHandler(HttpServletRequest request) {
String requestURI = request.getRequestURI();
return handlerMappingMap.get(requestURI);
}
//핸들러 처리할 수 있는 어댑터 조회
private MyHandlerAdapter getHandlerAdapter(Object handler) {
for (MyHandlerAdapter adapter : handlerAdapters) {
if (adapter.supports(handler)) {
return adapter;
}
}
throw new IllegalArgumentException("handler adapter를 찾을 수 없습니다. handler=" + handler);
}
private MyView viewResolver(String viewName) {
return new MyView("/WEB-INF/views/" + viewName + ".jsp");
}
}
정리
V1 : 프론트 컨트롤러 도입
프론트 컨트롤러 도입
V2 : View 분류
- 뷰 로직 분리
V3 : Model 추가
- 서블릿 종속성제거
- 뷰이름 중복 제거
V4 : 단순하고 실용적인 컨트롤러
- 컨트롤러에서 viewName 반환
V5 : 유연한 컨트롤러
- 어댑터 도입
- 어댑터 추가하여 프레임워크 유연하고확장성 있게 설계
스프링MVC - 구조 이해
스프링 MVC 전체 구조
직접만든 프레임워크와 스프링 MVC 비교
직접 만든 프레임워크 | 스프링 MVC |
FrontController | DispatcherServlet |
handlerMappingMap | HandlerMapping |
MyHandlerAdapter | HandlerAdapter |
viewResolver | viewResolver |
MyView | View |
DispatcherServlet 구조
스프링 MVC도 프론트 컨트롤러 패턴으로 구현되어있다.
스프링 MVC의 프론트 컨트롤러가 디스패처 서블릿(DispatcherServlet)이다.
디스패처 서블릿도 부모 클래스에서 HttpServlet을 상속받아 사용하고 서블릿으로 동작한다.
DispatcherServelt -> FrameworkServlet -> HttpServletBean -> HttpServlet
스프링 부트는 DispatcherServelt을 서블릿으로 자동 등록하면서 모든경로 ( urlPatterns="/" ) 에 대해 매핑한다.
(더 자세한 경로가 우선순위가 높다.)
DispatcherServlet의 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 {
ModelAndView mv = null;
Exception dispatchException = null;
try {
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
// 1. 핸들러 조회
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}
// 2. 핸들러 어댑터 조회
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// Process last-modified header, if supported by the handler.
String method = request.getMethod();
boolean isGet = "GET".equals(method);
if (isGet || "HEAD".equals(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;
}
applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
// As of 4.3, we're processing Errors thrown from handler methods as well,
// making them available for @ExceptionHandler methods and other scenarios.
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
triggerAfterCompletion(processedRequest, response, mappedHandler,
new NestedServletException("Handler processing failed", err));
}
finally {
if (asyncManager.isConcurrentHandlingStarted()) {
// Instead of postHandle and afterCompletion
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
}
else {
// Clean up any resources used by a multipart request.
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}
}
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
@Nullable Exception exception) throws Exception {
boolean errorView = false;
if (exception != null) {
if (exception instanceof ModelAndViewDefiningException) {
logger.debug("ModelAndViewDefiningException encountered", exception);
mv = ((ModelAndViewDefiningException) exception).getModelAndView();
}
else {
Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
mv = processHandlerException(request, response, handler, exception);
errorView = (mv != null);
}
}
// Did the handler return a view to render?
if (mv != null && !mv.wasCleared()) {
//뷰 렌더링 호출
render(mv, request, response);
if (errorView) {
WebUtils.clearErrorRequestAttributes(request);
}
}
else {
if (logger.isTraceEnabled()) {
logger.trace("No view rendering, null ModelAndView returned.");
}
}
if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
// Concurrent handling started during a forward
return;
}
if (mappedHandler != null) {
// Exception (if any) is already handled..
mappedHandler.triggerAfterCompletion(request, response, null);
}
}
protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
// Determine locale for request and apply it to the response.
Locale locale =
(this.localeResolver != null ? this.localeResolver.resolveLocale(request) : request.getLocale());
response.setLocale(locale);
View view;
String viewName = mv.getViewName();
if (viewName != null) {
// 6. 뷰 리졸버를 통해서 뷰 찾기, 7. View 반환
view = resolveViewName(viewName, mv.getModelInternal(), locale, request);
if (view == null) {
throw new ServletException("Could not resolve view with name '" + mv.getViewName() +
"' in servlet with name '" + getServletName() + "'");
}
}
else {
// No need to lookup: the ModelAndView object contains the actual View object.
view = mv.getView();
if (view == null) {
throw new ServletException("ModelAndView [" + mv + "] neither contains a view name nor a " +
"View object in servlet with name '" + getServletName() + "'");
}
}
// Delegate to the View object for rendering.
if (logger.isTraceEnabled()) {
logger.trace("Rendering view [" + view + "] ");
}
try {
if (mv.getStatus() != null) {
response.setStatus(mv.getStatus().value());
}
//8. 뷰 렌더링
view.render(mv.getModelInternal(), request, response);
}
catch (Exception ex) {
if (logger.isDebugEnabled()) {
logger.debug("Error rendering view [" + view + "]", ex);
}
throw ex;
}
}
SpringMVC 구조
동작순서
- 핸들러 조회 : 핸들러 매핑을 통해 요청 URL에 매핑된 핸들러 (컨트롤러) 조회
- 핸들러 어댑터 조회 : 핸들러를 실행할 수 있는 핸들러 어댑터 조회
- 핸들러 어댑터 실행 : 핸들러 어댑터 실행
- 핸들러 실행 : 핸들러 어댑터가 실제 핸들러 실행
- ModelAndView 반환 : 핸들러 어댑터는 핸틀러가 반환하는 정보를 ModelAndView로 변환해서 반환
- viewResolver 호출 : 뷰 리졸버를 찾고 실행한다. ( JSP의 경우 InternalResourceViewResolver 가 자동등록 , 사용됨)
- View 반환 : 뷰 리졸버는 뷰의 논리 이름을 물리 이름으로 바꾸고 렌더링 역할을 담당하는 뷰 객체 반환
- 뷰 렌더링 : 뷰를 통해 뷰를 렌더링 한다.
스프링 MVC의 큰 장점은 DispatcherServlet 의 코드의 변경 없이 원하는 기능을 변경하거나 확장할 수 있다.
주요 인터페이스 목록
핸들러 매핑 : org.springframework.web.servlet.HandlerMapping
핸들러 어댑터 : org.springframework.web.servlet.HandlerAdapter
뷰 리졸버 : org.springframework.web.servlet.ViewResolver
뷰 : org.springframework.web.servlet.View
스프링 MVC는 전세꼐 개발자들의 요구사항에 맞추어 기능을 계속 확장했다.
대부분의 기능이 이미 다 구현되어있다.
핵심 동작방식을 알아야, 문제 발생 시 어떤 부분에서 문제가 발생했는지 쉽게 파악하고 문제해결이 가능하다.
핸들러 매핑과 핸들러 어댑터
핸들러 매핑과 핸들러 어댑터 어떻게 사용되는가.
과거 스프링 컨트롤러
@FunctionalInterface
public interface Controller {
@Nullable
ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception;
}
간단히 구현해본다면.
@Component("/springmvc/old-controller")
public class OldController implements Controller {
@Override
public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
System.out.println("OldController.handleRequest");
return null;
}
}
해당 컨트롤러는 /springmvc/old-controller 라는 이름의 스프링 빈으로 등록된다.
빈의 이름으로 URL을 매핑한다.
해당 컨트롤러가 호출되려면 다음 2가지가 필요하다.
HandlerMapping ( 핸들러 매핑 )
핸들러 매핑에서 이 컨트롤러를 찾을 수 있어야한다.
스프링 빈의 이름으로 핸들러를 찾을 수 있는 핸들러 매핑이 필요하다.
HandlerAdapter ( 핸들러 어댑터 )
핸들러 매핑으로 찾은 핸들러를 실행 할 수 있는 핸들러 어댑터가 필요하다.
Controller 인터페이스를 실행할 수 있는 핸들러 어댑터를 찾고 실행해야한다.
스프링 부트가 자동 등록하는 핸들러 매핑과 핸들러 어댑터
(일부 생략됨)
HandlerMapping
0 = RequestMappingHandlerMapping : 어노테이션 기반의 컨트롤러인 @RequestMapping에서 사용
1 = BeanNameUrlHandlerMapping : 스프링 빈의 이름으로 핸들러를 가진다.
HandlerAdapter
0 = RequestMappingHandlerAdapter : 어노테이션 기반의 컨트롤러인 @RequestMapping에서 사용
1 = HttpRequestHandlerAdapter : HttpRequestHandler 처리
2 = SimpleControllerHandlerAdapter : Controller 인터페이스 ( 어노테이션 아닌, 과거에 사용하던 형태 )
핸들러 매핑, 핸들러 어댑터도 모두 순서대로 찾고 만약 없으면 다음순서로 넘어간다.
1. 핸들러 매핑으로 핸들러 조회
- HandlerMapping을 순서대로 실행해서 핸들러를 찾는다.
- 이 경우 빈의 이름으로 핸들러를 찾아야되기 때문에 이름 그대로 빈 핸들러를 찾아주는 BeanNameUrlHandlerMapping이 실행에 성공하고 핸들러인 OldController 반환
2. 핸들러 어댑터 조회
- HandlerAdapter의 supports() 를 순서대로 호출
- SimpleControllerHandlerAdapter 가 Controller 인터페이스를 지원하므로 대상이 된다.
3. 핸들러 어댑터 실행
- 디스패처 서블릿이 조회한 SimpleControllerHandlerAdapter를 실행하면서 핸들러 정보도 함께 넘겨준다.
- SimpleControllerHandlerAdapter는 핸들러인 OldController를 내부에서 실행하고 , 그 결과 반환
OldController - 핸들러 매핑, 어댑터 사용된 객체 정리
HandlerMapping - BeanNameUrlHandlerMapping
HandlerAdapter - SimpleControllerHandlerAdapter
HttpRequestHandler는 서블릿과 가장 유사한 형태의 핸들러
@FunctionalInterface
public interface HttpRequestHandler {
void handleRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException;
}
구현해보면
@Component("/springmvc/request-handler")
public class MyHttpRequestHandler implements HttpRequestHandler {
@Override
public void handleRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("MyHttpRequestHandler.handleRequest");
}
}
1. 핸들러 매핑으로 핸들러 조회
- HandlerMapping 을 순서대로 실행하여 핸들러를 찾는다.
- 이 경우 빈이름으로 핸들러를 찾아야하기 때문에 BeanNameUrlHandlerMapping 이 실행에 성공하고
핸들러인 MyHttpRequestHandler을 반환한다.
2. 핸들러 어댑터 조회
- HandlerAdapter의 supports()를 순서대로 호출
- HttpRequestHandlerAdapter 가 HttpRequsetHandler를 지원하므로 대상이 된다.
3. 핸들러 어댑터 실행
- 디스패처 서블릿이 조회한 HttpRequestHandlerAdapter를 실행하면서 핸들러 정보도 함께 넘긴다.
- HttpRequestHandlerAdapter는 핸들러인 MyHttpRequsetHandler를 내부에서 실행하고 결과 반환
MyHttpRequestHandler - 핸들러 매핑, 어댑터 사용된 객체
HandlerMapping = BeanNameHandlerMapping
HandlerAdapter = HttpRequestHandlerAdapter
뷰 리졸버
@Component("/springmvc/old-controller")
public class OldController implements Controller {
@Override
public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
System.out.println("OldController.handleRequest");
return new ModelAndView("new-form");
}
}
view 를 사용할 수 있도록 new ModelAndView("new-form")을 추가했다.
application.properties 에 다음 코드를 추가한다.
spring.mvc.view.prefix=/WEB-INF/views/
spring.mvc.view.suffix=.jsp
뷰 리졸버 동작 방식
스프링 부트가 자동 등록하는 뷰 리졸버
1 = BeanNameViewResolver : 빈 이름으로 뷰를 찾아서 반환한다.
2 = InternalResourceViewResolver : JSP를 처리할 수 있는 뷰를 반환한다.
1. 핸들러 어댑터 호출
핸들러 어댑터를 통해 new-from이라는 논리 뷰 이름 획득
2. ViewResolver 호출
new-form이라는 뷰 이름으로 viewResolver를 순서대로호출한다.
BeanNameViewResolver는 new-form이라는 이름으로 스프링 빈으로 등록된 뷰를 찾아야되는데 없다.
InternalResourceViewResolver가 호출된다.
3. InternalResourceViewResolver
해당 뷰 리졸버는 InternalResourceView를 반환한다.
4. 뷰 - InternalResourceView
이 뷰 리졸버는 InternalResourceView를 반환한다.
5. view.render()
view.render()가 호출되고, InternalResourceView 는 foward()를 사용해서 jsp 실행
SpringMVC @RequestMapping
가장 우선순위가 높은 핸들러 매핑과 어댑터는 RequestMapping, RequestMappingAdapter이다.
회원등록 폼
@Controller
public class SpringMemberFormControllerV1 {
@RequestMapping("/springmvc/v1/members/new-form")
public ModelAndView process() {
return new ModelAndView("new-form");
}
}
회원저장
@Controller
public class SpringMemberSaveControllerV1 {
private MemberRepository memberRepository = MemberRepository.getInstance();
@RequestMapping("/springmvc/v1/members/save")
public ModelAndView process(HttpServletRequest request, HttpServletResponse response) {
String username = request.getParameter("username");
int age = Integer.parseInt(request.getParameter("age"));
Member member = new Member(username, age);
memberRepository.save(member);
ModelAndView mv = new ModelAndView("save-result");
mv.addObject("member", member);
return mv;
}
}
회원목록
@Controller
public class SpringMemberSaveControllerV1 {
private MemberRepository memberRepository = MemberRepository.getInstance();
@RequestMapping("/springmvc/v1/members/save")
public ModelAndView process(HttpServletRequest request, HttpServletResponse response) {
String username = request.getParameter("username");
int age = Integer.parseInt(request.getParameter("age"));
Member member = new Member(username, age);
memberRepository.save(member);
ModelAndView mv = new ModelAndView("save-result");
mv.addObject("member", member);
return mv;
}
}
스프링 MVC - 컨트롤러 통합
@Controller
@RequestMapping("/springmvc/v2/members")
public class SpringMemberControllerV2 {
private MemberRepository memberRepository = MemberRepository.getInstance();
@RequestMapping("/new-form")
public ModelAndView newForm() {
return new ModelAndView("new-form");
}
@RequestMapping("/save")
public ModelAndView save(HttpServletRequest request, HttpServletResponse response) {
String username = request.getParameter("username");
int age = Integer.parseInt(request.getParameter("age"));
Member member = new Member(username, age);
memberRepository.save(member);
ModelAndView mv = new ModelAndView("save-result");
mv.addObject("member", member);
return mv;
}
@RequestMapping
public ModelAndView members() {
List<Member> members = memberRepository.findAll();
ModelAndView mv = new ModelAndView("members");
mv.addObject("members", members);
return mv;
}
}
조합결과
클래스 레벨 @RequestMapping("/springmvc/v2/members")
메서드레벨 @RequsetMapping("/new-form") -> /springmvc/v2/members/new-form
메서드레벨 @RequsetMapping("/save") -> /springmvc/v2/members/save
메서드레벨 @RequsetMapping -> /springmvc/v2/members
스프링 MVC - 실용적인 방식
@Controller
@RequestMapping("/springmvc/v3/members")
public class SpringMemberControllerV3 {
private MemberRepository memberRepository = MemberRepository.getInstance();
@GetMapping("/new-form")
public String newForm() {
return "new-form";
}
@PostMapping("/save")
public String save(
@RequestParam("username") String username,
@RequestParam("age") int age,
Model model) {
Member member = new Member(username, age);
memberRepository.save(member);
model.addAttribute("member", member);
return "save-result";
}
@GetMapping
public String members(Model model) {
List<Member> members = memberRepository.findAll();
model.addAttribute("members", members);
return "members";
}
}
스프링 MVC - 기본기능
로깅
로그선언
private Logger log = LoggerFactory.getLogger(getClass());
private static final Logger log = LoggerFactory.getLogger(Xxx.class)
//@Slf4j : 롬복 사용 가능
//로그 호출
log.info("hello")
System.out.println("hello")
//시스템 콘솔로 직접 출력하는 것 보다 로그를 사용하면 다음과 같은 장점이 있다. 실무에서는 항상 로그를 사용해야 한다.
//@Slf4j
@RestController
public class LogTestController {
private final Logger log = LoggerFactory.getLogger(getClass());
@RequestMapping("/log-test")
public String logTest() {
String name = "Spring";
System.out.println("name = " + name);
log.trace(" trace my log="+ name);
log.trace("trace log={}", name);
log.debug("debug log={}", name);
log.info(" info log={}", name);
log.warn(" warn log={}", name);
log.error("error log={}", name);
return "ok";
}
}
@RestController
@Controller는 반환 값이 String이면 뷰 이름으로 인식된다.
@RestController는 반환 값으로 뷰를 가지는 게 아니라 HTTP메세지 바디에 바로 입력한다.
따라서 실행 결과로 ok 메세지 획득 가능.
로그 사용시 장점.
쓰레드 정보, 클래스 이름같은 부가정보 함께 볼 수 있음
파일이나 네트워크 등 로그를 별도의 위치에 남길 수 있다.
System.out 보다 성능이 좋다.
요청 매핑
@RestController
public class MappingController {
private Logger log = LoggerFactory.getLogger(getClass());
@RequestMapping(value = "/hello-basic")
public String helloBasic() {
log.info("helloBasic");
return "ok";
}
@RequestMapping(value = "/mapping-get-v1", method = RequestMethod.GET)
public String mappingGetV1() {
log.info("mappingGetV1");
return "ok";
}
/**
* 편리한 축약 애노테이션 (코드보기)
* @GetMapping
* @PostMapping
* @PutMapping
* @DeleteMapping
* @PatchMapping
*/
@GetMapping(value = "/mapping-get-v2")
public String mappingGetV2() {
log.info("mapping-get-v2");
return "ok";
}
/**
* PathVariable 사용
* 변수명이 같으면 생략 가능
*
* @PathVariable("userId") String userId -> @PathVariable userId
* /mapping/userA
*/
@GetMapping("/mapping/{userId}")
public String mappingPath(@PathVariable("userId") String data) {
log.info("mappingPath userId={}", data);
return "ok";
}
/**
* PathVariable 사용 다중
*/
@GetMapping("/mapping/users/{userId}/orders/{orderId}")
public String mappingPath(@PathVariable String userId, @PathVariable Long orderId) {
log.info("mappingPath userId={}, orderId={}", userId, orderId);
return "ok";
}
/**
* 파라미터로 추가 매핑
* params="mode",
* params="!mode"
* params="mode=debug"
* params="mode!=debug" (! = )
* params = {"mode=debug","data=good"}
*/
@GetMapping(value = "/mapping-param", params = "mode=debug")
public String mappingParam() {
log.info("mappingParam");
return "ok";
}
/**
* 특정 헤더로 추가 매핑
* headers="mode",
* headers="!mode"
* headers="mode=debug"
* headers="mode!=debug" (! = )
*/
@GetMapping(value = "/mapping-header", headers = "mode=debug")
public String mappingHeader() {
log.info("mappingHeader");
return "ok";
}
/**
* Content-Type 헤더 기반 추가 매핑 Media Type
* 만약 맞지 않으면 HTTP 415 상태코드(Unsupported Media Type)을 반환한다.
* consumes="application/json"
* consumes="!application/json"
* consumes="application/*"
* consumes="*\/*"
* MediaType.APPLICATION_JSON_VALUE
*/
@PostMapping(value = "/mapping-consume", consumes = MediaType.APPLICATION_JSON_VALUE)
public String mappingConsumes() {
log.info("mappingConsumes");
return "ok";
}
/**
* Accept 헤더 기반 Media Type
* 만약 맞지 않으면 HTTP 406 상태코드(Not Acceptable)을 반환한다.
* produces = "text/html"
* produces = "!text/html"
* produces = "text/*"
* produces = "*\/*"
*/
@PostMapping(value = "/mapping-produce", produces = MediaType.TEXT_HTML_VALUE)
public String mappingProduces() {
log.info("mappingProduces");
return "ok";
}
}
요청 매핑 API 예시
회원관리 API
회원목록조회 : GET ( /users )
회원 등록 : POST ( /users )
회원 조회 : GET ( /users/{userId} )
회원 수정 : PATCH ( /users/{userId} )
회원 삭제 : DELETE ( /users/{userId} )
@RestController
@RequestMapping("/mapping/users")
public class MappingClassController {
@GetMapping
public String user() {
return "get users";
}
@PostMapping
public String addUser() {
return "post user";
}
@GetMapping("/{userId}")
public String findUser(@PathVariable String userId) {
return "get userId=" + userId;
}
@PatchMapping("/{userId}")
public String updateUser(@PathVariable String userId) {
return "update userId=" + userId;
}
@DeleteMapping("/{userId}")
public String deleteUser(@PathVariable String userId) {
return "delete userId=" + userId;
}
}
HTTP요청 - 기본, 헤더 조회
@Slf4j
@RestController
public class RequestHeaderController {
@RequestMapping("/headers")
public String headers(HttpServletRequest request,
HttpServletRequest response,
HttpMethod httpMethod,
Locale locale,
@RequestHeader MultiValueMap<String, String> headerMap,
@RequestHeader("host") String host,
@CookieValue(value = "myCookie", required = false) String cookie
) {
log.info("request={}", request);
log.info("response={}", response);
log.info("httpMethod={}", httpMethod);
log.info("locale={}", locale);
log.info("headerMap={}", headerMap);
log.info("header host={}", host);
log.info("myCookie={}", cookie);
return "ok";
}
}
결과
2023-07-31 17:02:28.119 INFO 22544 --- [nio-8080-exec-3] h.s.b.request.RequestHeaderController : request=org.apache.catalina.connector.RequestFacade@45ee7561
2023-07-31 17:02:28.119 INFO 22544 --- [nio-8080-exec-3] h.s.b.request.RequestHeaderController : response=org.apache.catalina.connector.RequestFacade@45ee7561
2023-07-31 17:02:28.120 INFO 22544 --- [nio-8080-exec-3] h.s.b.request.RequestHeaderController : httpMethod=GET
2023-07-31 17:02:28.120 INFO 22544 --- [nio-8080-exec-3] h.s.b.request.RequestHeaderController : locale=ko
2023-07-31 17:02:28.120 INFO 22544 --- [nio-8080-exec-3] h.s.b.request.RequestHeaderController : headerMap={host=[localhost:8080], connection=[keep-alive], sec-ch-ua=["Whale";v="3", "Not-A.Brand";v="8", "Chromium";v="114"], sec-ch-ua-mobile=[?0], sec-ch-ua-platform=["Windows"], upgrade-insecure-requests=[1], user-agent=[Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Whale/3.21.192.18 Safari/537.36], accept=[text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7], sec-fetch-site=[none], sec-fetch-mode=[navigate], sec-fetch-user=[?1], sec-fetch-dest=[document], accept-encoding=[gzip, deflate, br], accept-language=[ko,en-US;q=0.9,en;q=0.8,ko-KR;q=0.7,ja;q=0.6,de;q=0.5]}
2023-07-31 17:02:28.120 INFO 22544 --- [nio-8080-exec-3] h.s.b.request.RequestHeaderController : header host=localhost:8080
2023-07-31 17:02:28.120 INFO 22544 --- [nio-8080-exec-3] h.s.b.request.RequestHeaderController : myCookie=null
HttpMethod : HTTP 메서드를 조회한다.
Locale : Locale 정보를 조회한다
@RequestHeader("host") : 특정 HTTP 헤더를 조회한다.
@CookieValue(value="myCookie", required = false ) : 특정 쿠키를 조회한다.
MultiValueMap : 맵과 유사한데 하나에 키에 여러 값을 받을 수 있다.
HTTP header, HTTP 쿼리 파라미터와 같이 하나의 키에 여러 값을 받을 때 사용한다.
HTTP 요청 파라미터 - 쿼리 파라미터, HTML Form
클라이언트에서 서버로 요청 데이터를 전달할 때에는 주로 3가지 방법을 사용한다
GET -쿼리 파라미터
/url?username=hello&age=20
메세지 바디 없이 URL의 쿼리 파라미터에 데이터를 포함하여 전달
ex) 검색 필터, 페이징
POST - HTML Form
content-type : application/x-www-form-urlencoded
메세지 바디에 쿼리 파라미터 형식으로 전달 username=hello&age=20
ex) 회원 가입, 상품 주문, HTML Form 사용
HTTP message body에 데이터를 직접 담아 요청
HTTP API에서 주로 사용, JSON, XML , TEXT
데이터 형식은 주로 JSON 사용
POST, PUT, PATCH
POST,HTML FORM 전송
@Slf4j
@Controller
public class RequestParamController {
//HttpServletRequest 제공하는 방식
@RequestMapping("/request-param-v1")
public void requestParamV1(HttpServletRequest request, HttpServletResponse response) throws IOException {
String username = request.getParameter("username");
int age = Integer.parseInt(request.getParameter("age"));
response.getWriter().write("ok");
}
//스프링이 제공하는 @RequestParam으로 요청 파라미터 사용
@ResponseBody // View조회 무시, HTTP message Body 직접 내용 입력
@RequestMapping("/request-param-v2")
public String requestParamV2(
@RequestParam("username") String memberName, // 파라미터 이름으로 바인딩
@RequestParam("age") int memberAge) {
log.info("username={}, age={}", memberName, memberAge);
return "ok";
}
@ResponseBody
@RequestMapping("/request-param-v3")
public String requestParamV3(
@RequestParam String username, // http파라미터 이름이 변수명과 같으면 name 속성 생략가능
@RequestParam int age) {
log.info("username={}, age={}", username, age);
return "ok";
}
/**
* String, int, Integer 등의 단순 타입이면 @RequestParam도 생략 가능
*/
@ResponseBody
@RequestMapping("/request-param-v4")
public String requestParamV4(String username, int age) {
log.info("username={}, age={}", username, age);
return "ok";
}
/**
* @RequestParam.required
* /request-param-required -> username이 없으므로 예외
*
* 주의!
* /request-param-required?username= -> 빈문자로 통과
*
* 주의!
* /request-param-required
* int age -> null을 int에 입력하는 것은 불가능, 따라서 Integer 변경해야 함(또는 다음에 나오는 defaultValue 사용)
*/
@ResponseBody
@RequestMapping("/request-param-required")
public String requestParamRequired(
@RequestParam(required = true) String username,
@RequestParam(required = false) Integer age) {
log.info("username={}, age={}", username, age);
return "ok";
}
/**
* @RequestParam
* - defaultValue 사용
*
* 참고: defaultValue는 빈 문자의 경우에도 적용
* /request-param-default?username=
*/
@ResponseBody
@RequestMapping("/request-param-default")
public String requestParamDefault(
@RequestParam(required = true, defaultValue = "guest") String username,
@RequestParam(required = false, defaultValue = "-1") int age) {
log.info("username={}, age={}", username, age);
return "ok";
}
/**
* @RequestParam Map, MultiValueMap
* Map(key=value)
* MultiValueMap(key=[value1, value2, ...]) ex) (key=userIds, value=[id1, id2])
*/
@ResponseBody
@RequestMapping("/request-param-map")
public String requestParamMap(@RequestParam Map<String, Object> paramMap) {
log.info("username={}, age={}", paramMap.get("username"), paramMap.get("age"));
return "ok";
}
/**
* @ModelAttribute 사용
* 참고: model.addAttribute(helloData) 코드도 함께 자동 적용
*/
@ResponseBody
@RequestMapping("/model-attribute-v1")
public String modelAttributeV1(@ModelAttribute HelloData helloData) {
log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
return "ok";
}
/**
* @ModelAttribute 생략 가능
* String, int 같은 단순 타입 = @RequestParam
* argument resolver 로 지정해둔 타입 외 = @ModelAttribute
*/
@ResponseBody
@RequestMapping("/model-attribute-v2")
public String modelAttributeV2(HelloData helloData) {
log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
return "ok";
}
}
HelloData 객체
@Data
public class HelloData {
private String username;
private int age;
}
HTTP요청 메세지 - 단순 텍스트
@Slf4j
@Controller
public class RequestBodyStringController {
@PostMapping("/request-body-string-v1")
public void requestBodyString(HttpServletRequest request, HttpServletResponse response) throws IOException {
ServletInputStream inputStream = request.getInputStream();
String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
log.info("messageBody={}", messageBody);
response.getWriter().write("ok");
}
// InputStream(Reader): HTTP 요청 메시지 바디의 내용을 직접 조회
// OutputStream(Writer): HTTP 응답 메시지의 바디에 직접 결과 출력
@PostMapping("/request-body-string-v2")
public void requestBodyStringV2(InputStream inputStream, Writer responseWriter) throws IOException {
String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
log.info("messageBody={}", messageBody);
responseWriter.write("ok");
}
/**
* HttpEntity: HTTP header, body 정보를 편리하게 조회
* - 메시지 바디 정보를 직접 조회(@RequestParam X, @ModelAttribute X)
* - HttpMessageConverter 사용 -> StringHttpMessageConverter 적용
*
* 응답에서도 HttpEntity 사용 가능
* - 메시지 바디 정보 직접 반환(view 조회X)
* - HttpMessageConverter 사용 -> StringHttpMessageConverter 적용
*/
@PostMapping("/request-body-string-v3")
public HttpEntity<String> requestBodyStringV3(HttpEntity<String> httpEntity) throws IOException {
String messageBody = httpEntity.getBody();
log.info("messageBody={}", messageBody);
return new HttpEntity<>("ok");
}
/**
* @RequestBody
* - 메시지 바디 정보를 직접 조회(@RequestParam X, @ModelAttribute X)
* - HttpMessageConverter 사용 -> StringHttpMessageConverter 적용
*
* @ResponseBody
* - 메시지 바디 정보 직접 반환(view 조회X)
* - HttpMessageConverter 사용 -> StringHttpMessageConverter 적용
*/
@ResponseBody
@PostMapping("/request-body-string-v4")
public String requestBodyStringV4(@RequestBody String messageBody) {
log.info("messageBody={}", messageBody);
return "ok";
}
}
HTTP 요청 메세지 - JSON
/**
* {"username":"hello", "age":20}
* content-type: application/json
*/
@Slf4j
@Controller
public class RequestBodyJsonController {
//Json 데이터 ObjectMapper 사용 변환
private ObjectMapper objectMapper = new ObjectMapper();
@PostMapping("/request-body-json-v1")
public void requestBodyJsonV1(HttpServletRequest request, HttpServletResponse response) throws IOException {
ServletInputStream inputStream = request.getInputStream();
String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
log.info("messageBody={}", messageBody);
HelloData helloData = objectMapper.readValue(messageBody, HelloData.class);
log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
response.getWriter().write("ok");
}
/**
* @RequestBody
* HttpMessageConverter 사용 -> StringHttpMessageConverter 적용
*
* @ResponseBody
* - 모든 메서드에 @ResponseBody 적용
* - 메시지 바디 정보 직접 반환(view 조회X)
* - HttpMessageConverter 사용 -> StringHttpMessageConverter 적용
*/
@ResponseBody
@PostMapping("/request-body-json-v2")
public String requestBodyJsonV2(@RequestBody String messageBody) throws IOException {
log.info("messageBody={}", messageBody);
HelloData helloData = objectMapper.readValue(messageBody, HelloData.class);
log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
return "ok";
}
/**
* @RequestBody 생략 불가능(@ModelAttribute 가 적용되어 버림)
* HttpMessageConverter 사용 -> MappingJackson2HttpMessageConverter (contenttype: application/json)
*
*/
@ResponseBody
@PostMapping("/request-body-json-v3")
public String requestBodyJsonV3(@RequestBody HelloData data) {
log.info("username={}, age={}", data.getUsername(), data.getAge());
return "ok";
}
@ResponseBody
@PostMapping("/request-body-json-v4")
public String requestBodyJsonV4(HttpEntity<HelloData> httpEntity) {
HelloData data = httpEntity.getBody();
log.info("username={}, age={}", data.getUsername(), data.getAge());
return "ok";
}
/**
* @RequestBody 생략 불가능(@ModelAttribute 가 적용되어 버림)
* HttpMessageConverter 사용 -> MappingJackson2HttpMessageConverter (contenttype:application/json)
*
* @ResponseBody 적용
* - 메시지 바디 정보 직접 반환(view 조회X)
* - HttpMessageConverter 사용 -> MappingJackson2HttpMessageConverter 적용(Accept: application/json)
*/
@ResponseBody
@PostMapping("/request-body-json-v5")
public HelloData requestBodyJsonV5(@RequestBody HelloData data) {
log.info("username={}, age={}", data.getUsername(), data.getAge());
return data;
}
}
@RequestBody 객체 파라미터
@RequestBody HelloData data
@RequestBody 에 직접 만든 객체를 지정할 수 있다.
HTTP 메세지의 본문에 존재하는 JSON 메세지를
객체 파라미터로 받아서 사용하는 경우 @RequestBody는 생략 불가능하다.
생략하면 @ModelAttribute가 사용되기 때문에
HTTP 메시지 바디가 아니라 요청 파라미터를 처리하게 된다.
HTTP 응답 - HTTP API, 메세지 바디에 직접 입력
@Slf4j
@Controller
public class ResponseBodyController {
// 서블릿을 직접 다룰때처럼 HttpServletResponse를 사용하여 메세지 바디에 ok 응답메세지 전달
@GetMapping("/response-body-string-v1")
public void responseBodyV1(HttpServletResponse response) throws IOException {
response.getWriter().write("ok");
}
// HttpEntity를 상속받았는데 ResponsseEntity는 여기에 더해 HTTP응답콛 설정이 가능하다.
@GetMapping("/response-body-string-v2")
public ResponseEntity<String> responseBodyV2() {
return new ResponseEntity<>("ok", HttpStatus.OK);
}
// @ResponseBody를 사용하면 view 사용하지 않고 HTTP메세지 컨버터 통해 직접 메세지 입력 가능
@ResponseBody
@GetMapping("/response-body-string-v3")
public String responseBodyV3() {
return "ok";
}
// ResponseEntity 반환, HTTP 메세지 컨버터를 통해 JSON 형식 반환
@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);
}
@ResponseStatus(HttpStatus.OK)
@ResponseBody
@GetMapping("/response-body-json-v2")
public HelloData responseBodyJsonV2() {
HelloData helloData = new HelloData();
helloData.setUsername("userA");
helloData.setAge(20);
return helloData;
}
}
@RestController 어노테이션을 사용하면
해당 컨트롤러 모두 @ResponseBody가 적용되는 효과가 있다.
따라서 뷰 템플릿을 사용하는 것이 아니라 , HTTP 메세지 바디에 직접 데이터를 입력한다.
@ResponseBody 는 클래스 레벨에 두면 전체 메서드에 적용되는데
@RestController 어노테이션 안에 @ResponseBody 가 적용되어 있다.
HTTP 메세지 컨버터
뷰 템플릿으로 HTML을 생성해서 응답하는 것이 아니라, HTTP API 처럼 JSON 데이터를 HTTP 메세지 바디에서 직접 읽거나 쓰는 경우 HTTP 메세지 컨버터를 사용하면 편리하다.
@ResponseBody 를 사용
- HTTP의 BODY에 문자 내용을 직접 반환
- viewResolver 대신에 HttpMessageConverter 가 동작
- 기본 문자처리: StringHttpMessageConverter
- 기본 객체처리: MappingJackson2HttpMessageConverter
- byte 처리 등등 기타 여러 HttpMessageConverter가 기본으로 등록되어 있음
스프링 MVC 의 경우
HTTP 요청 : @RequestBody , HttpEntity(RequestEntity)
HTTP 응답: @ResponseBody, HttpEntity(ResponseEntity)
public interface HttpMessageConverter<T> {
/**
* Indicates whether the given class can be read by this converter.
* @param clazz the class to test for readability
* @param mediaType the media type to read (can be {@code null} if not specified);
* typically the value of a {@code Content-Type} header.
* @return {@code true} if readable; {@code false} otherwise
*/
boolean canRead(Class<?> clazz, @Nullable MediaType mediaType);
/**
* Indicates whether the given class can be written by this converter.
* @param clazz the class to test for writability
* @param mediaType the media type to write (can be {@code null} if not specified);
* typically the value of an {@code Accept} header.
* @return {@code true} if writable; {@code false} otherwise
*/
boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType);
/**
* Return the list of media types supported by this converter. The list may
* not apply to every possible target element type and calls to this method
* should typically be guarded via {@link #canWrite(Class, MediaType)
* canWrite(clazz, null}. The list may also exclude MIME types supported
* only for a specific class. Alternatively, use
* {@link #getSupportedMediaTypes(Class)} for a more precise list.
* @return the list of supported media types
*/
List<MediaType> getSupportedMediaTypes();
/**
* Return the list of media types supported by this converter for the given
* class. The list may differ from {@link #getSupportedMediaTypes()} if the
* converter does not support the given Class or if it supports it only for
* a subset of media types.
* @param clazz the type of class to check
* @return the list of media types supported for the given class
* @since 5.3.4
*/
default List<MediaType> getSupportedMediaTypes(Class<?> clazz) {
return (canRead(clazz, null) || canWrite(clazz, null) ?
getSupportedMediaTypes() : Collections.emptyList());
}
/**
* Read an object of the given type from the given input message, and returns it.
* @param clazz the type of object to return. This type must have previously been passed to the
* {@link #canRead canRead} method of this interface, which must have returned {@code true}.
* @param inputMessage the HTTP input message to read from
* @return the converted object
* @throws IOException in case of I/O errors
* @throws HttpMessageNotReadableException in case of conversion errors
*/
T read(Class<? extends T> clazz, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException;
/**
* Write an given object to the given output message.
* @param t the object to write to the output message. The type of this object must have previously been
* passed to the {@link #canWrite canWrite} method of this interface, which must have returned {@code true}.
* @param contentType the content type to use when writing. May be {@code null} to indicate that the
* default content type of the converter must be used. If not {@code null}, this media type must have
* previously been passed to the {@link #canWrite canWrite} method of this interface, which must have
* returned {@code true}.
* @param outputMessage the message to write to
* @throws IOException in case of I/O errors
* @throws HttpMessageNotWritableException in case of conversion errors
*/
void write(T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException;
}
canRead(), canWrite() : 메세지 컨버터가 해당 클래스, 미디어 타입 지원 여부 체크
read(), write() : 메세지 컨버터를 통해서 메세지를 읽고 쓰는 기능
스프링 부트 기본 메세지 컨버터
0 = ByteArrayHttpMessageConverter
1 = StringHttpMessageConverter
2 = MappingJackson2HttpMessageConverter
요청 매핑 핸들러 구조
HTTP메시지 컨버터는 스프링 MVC에 어느 지점에서 사용되냐면
어노테이션 기반의 컨트롤러 @RequestMapping 을 처리하는 핸들러 어댑터인
RequestMappingHandlerAdapter( 요청 매핑 핸들러 어댑터 ) 에 있다.
ArgumentResolver
어노테이션 기반의 컨트롤러는 매우 다양한 파라미터 처리가 가능하다.
HttpServletRequest , Model 클래스 및 @RequestParam, @ModelAttribute 어노테이션 그리고 HTTP 메세지인
@RequestBody, HttpEntity 도 처리하는 유연함을 보여준다.
어노테이션 기반 컨트롤러를 처리하는 RequestMappingHandlerAdapter는 바로 이
ArgumentResolver 을 호출하여 컨트롤러 ( 핸들러 ) 가 필요로 하는 다양한 파라미터의 값을 생성한다.
/**
* Strategy interface for resolving method parameters into argument values in
* the context of a given request.
*
* @author Arjen Poutsma
* @since 3.1
* @see HandlerMethodReturnValueHandler
*/
public interface HandlerMethodArgumentResolver {
/**
* Whether the given {@linkplain MethodParameter method parameter} is
* supported by this resolver.
* @param parameter the method parameter to check
* @return {@code true} if this resolver supports the supplied parameter;
* {@code false} otherwise
*/
boolean supportsParameter(MethodParameter parameter);
/**
* Resolves a method parameter into an argument value from a given request.
* A {@link ModelAndViewContainer} provides access to the model for the
* request. A {@link WebDataBinderFactory} provides a way to create
* a {@link WebDataBinder} instance when needed for data binding and
* type conversion purposes.
* @param parameter the method parameter to resolve. This parameter must
* have previously been passed to {@link #supportsParameter} which must
* have returned {@code true}.
* @param mavContainer the ModelAndViewContainer for the current request
* @param webRequest the current request
* @param binderFactory a factory for creating {@link WebDataBinder} instances
* @return the resolved argument value, or {@code null} if not resolvable
* @throws Exception in case of errors with the preparation of argument values
*/
@Nullable
Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;
}
동작 방식
ArgumentResolver의 supportsParameter() 를 호출해서 해당 파라미터를 지원하는지 체크하고
지원하면 resolvArgumnet() 를 호출해서 실제 객체를 생성한다.
생성된 객체는 컨트롤러 호출 시 넘어간다.
ReturnValueHandler
HandlerMethodReturnValueHandler를 줄여 ReturnValue 핸들러라 부른다.
응답값을 변환하고 처리한다.
HTTP 메세지 컨버터 위치
요청의 경우
@RequestBody를 처리하는 ArgumentResolver 가 있고, HttpEntity를 처리하는 ArgumentResolver가 있다.
이 ArgumentResolver 들이 각각 HTTP 메세지 컨버터를 사용해서 필요한 객체를 생성한다.
응답의 경우
@ResponseBody 와 HttpEntity를 처리하는 ReturnValueHandler 가 있다.
스프링 MVC는 @RequestBody @ResponseBody 가 있으면 RequestResponseBodyMethodProcessor (ArgumentResolver)
HttpEntity가 있으면
HttpEntityMethodProcessor (ArgumentResolver)를 사용한다.
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-1/dashboard
'스터디 > 2023_스프링부트' 카테고리의 다른 글
[study] 스프링MVC2 - 2. 타임리프 스프링 통합과 폼 (0) | 2023.08.08 |
---|---|
[study] 스프링MVC2 - 1. 타임리프 (0) | 2023.08.08 |
[study] 스프링MVC 정리 1 (0) | 2023.07.24 |
[study] HTTP 웹 기본 지식 (1) | 2023.07.23 |
[study]자바의정석 11~14 chapter (0) | 2023.07.18 |