스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 | 김영한 - 인프런
김영한 | 웹 애플리케이션을 개발할 때 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. 스프링 MVC의 핵심 원리와 구조를 이해하고, 더 깊이있는 백엔드 개발자로 성장할 수 있습
www.inflearn.com
1. 강의 흐름
이 강의는 스프링 MVC의 핵심 원리와 구조를 이해하기 위한 코스다.
아래 절차로 스프링 MVC가 왜 필요하고 어떻게 만들어진 프레임워크인지 이해할 수 있다.
1) 서블릿으로 회원관리 웹 애플리케이션을 만들어서 간단하게 회원가입, 회원등록, 회원목록 조회 기능을 만들음.
-> 템플릿 엔진의 필요성을 느낌
2) JSP로 웹 애플리케이션을 만들음.
-> 비즈니스 로직과 뷰가 함께 있으니 분리해보는 작업을 함.
3) MVC 패턴을 적용해봄
-> 공통 기능을 처리하는 부분이 없어서 중복이 발생하는 한계가 있음.
4) 프론트 컨트롤러를 도입해서 공통기능을 처리할 수 있도록 함.
-> v1 부터 v5까지 조금씩 버전 업그레이드를 하며 설계를 정돈해간다.
(서블릿 종속성을 제거하고 뷰 경로의 중복을 제거하는 등..)
5) 최종적으로 v5의 구조가 스프링MVC의 구조를 이해하는 기반이 된다.
2. V5 구조 이해
어댑터 패턴을 이용해서 다양한 방식의 컨트롤러 인터페이스를 처리할 수 있도록 하는 구조다.
1) 클라이언트가 HTTP요청을 하면 FrontController를 거쳐서 핸들러 매핑 정보에서 핸들러를 조회한다.
(* 핸들러란, HTTP 요청을 처리하고 적절한 응답을 생성하는 메서드나 컨트롤러 )
2) 핸들러 어댑터 목록에서 핸들러를 처리할 수 있는 핸들러 어댑터를 조회한다.
(마치, 일본여행을 갔을 때 내 제품을 사용하기 위해 필요한 어댑터가 110V인데 110V어댑터가 있는지 가방을 뒤지는 것과 같다.)
3) 핸들러 어댑터가 있으면, 어댑터가 핸들러를 대신 호출해준다.
어댑터는 핸들러에서 결과를 반환받아서 FrontController에 ModelView를 반환한다.
4) FrontController는 뷰 리졸버를 호출한다.
(* 뷰 리졸버는 핸들러가 반환한 논리 뷰 이름을 실제 물리 뷰 경로로 바꿔준다.)
5) 뷰 리졸버는 실제 물리 경로가 있는 MyView 객체를 반환한다.
6) 이제 뷰 객체(MyView)를 통해서 HTML 화면을 렌더링한다.
(render를 할 때, model정보를 함께 받아서 JSP 포워드를 하고 JSP를 렌더링 한다.)
3. 코드로 V5 구조 이해하기
1) 프론트 컨트롤러 - frontControllerServletV5 코드
@WebServlet(name = "frontControllerServletV5", urlPatterns = "/front-controller/v5/*")
public class FrontControllerServletV5 extends HttpServlet {
//기존에는 Map<String, ControllerV3>같이 특정 Controller만 받았는데, v5버전에서는 Object 객체로 받는다.
private final Map<String, Object> handlerMappingMap = new HashMap<>();
//여러개 어댑터중에 선택해야하니 List로 만든다.
private final List<MyHandlerAdapter> handlerAdapters = new ArrayList<>();
public FrontControllerServletV5() {
initHandlerMappingMap();
initHandlerAdapters();
}
private void initHandlerMappingMap() {
//V3
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); //요청정보에서 handler를 찾아온다.
if(handler == null){
response.setStatus(HttpServletResponse.SC_NOT_FOUND);//404
return;
}
//handlerAdapter를 찾아온다.
MyHandlerAdapter adapter = getHandlerAdapter(handler);
//adapter에 request,response, handler정보를 보내면 ModelView를 반환.
ModelView mv = adapter.handle(request, response, handler);
String viewName = mv.getViewName();//논리이름
//실제 물리 경로가 있는 Myview 객체를 반환
MyView view = viewResolver(viewName);
//view에 model을 함께 넘겨준다.
//request.setAttribute()를 통해 데이터를 저장하고 뷰에 전달.
view.render(mv.getModel(),request, response);
}
private MyHandlerAdapter getHandlerAdapter(Object handler) {
for (MyHandlerAdapter adepter : handlerAdapters) {
if(adepter.supports(handler)){
return adepter;
}
}
throw new IllegalArgumentException("handler adapter를 찾을 수 없습니다. handler = "+handler);
}
private Object getHandler(HttpServletRequest request) {
String requestURI = request.getRequestURI();
return handlerMappingMap.get(requestURI);
}
private static MyView viewResolver(String viewName) {
return new MyView("/WEB-INF/views/" + viewName + ".jsp");
}
}
✔️ handlerMappingMap
- URL 패턴을 핸들러 객체와 매핑하는 Map.
- 특정 URL 요청이 들어오면 이 Map을 통해 요청을 처리할 핸들러를 찾는다.
✔️ handlerAdapters
- 여러 핸들러 어댑터를 담고 있는 List.
- 각 어댑터는 특정 타입의 핸들러를 처리.
✔️ initHandlerMappingMap
- 특정 URL과 그에 대응하는 핸들러 객체를 매핑
- V5 이전에 만들었던, V3와 V4 컨트롤러 객체들을 등록했다.
✔️ initHandlerAdapters
- 다양한 핸들러 어댑터를 List에 추가
- 각 어댑터는 특정 타입 핸들러(컨트롤러)를 지원한다.
✔️ getHandler
- 요청 URI에 해당하는 핸들러 객체를 반환
✔️ getHandlerAdapter
- 핸들러를 처리할 수 있는 어댑터를 List에서 찾아 반환
- 각 어댑터에 있는 supports 메서드를 통해 어댑터가 해당 컨트롤러를 처리할 수 있는지 판단한다.
✅ControllerV3HandlerAdapter의 supports 메서드
@Override
public boolean supports(Object handler) { //어댑터가 해당 컨트롤러를 처리할 수 있는지 판단하는 메서드.
return (handler instanceof ControllerV3);//ControllerV3인지 확인하고 아니면 false 반환
}
✅ControllerV4HandlerAdapter의 supports 메서드
@Override
public boolean supports(Object handler) {
return (handler instanceof ControllerV4);
}
✔️ viewResolver
- 논리 뷰 이름을 실제 물리 뷰 경로로 변환하여 MyView 객체를 반환
✔️ service
- HTTP 요청을 처리하는 메서드
- 요청에 해당하는 핸들러를 찾는다.
- 이를 처리할 수 있는 핸들러 어댑터를 찾는다.
- 요청을 처리하여 ModelView 객체를 반환받는다.
- 뷰 리졸버를 통해 실제 뷰를 찾아 렌더링한다.
2) ModelView 코드
package hello.servlet.web.frontcontroller;
import lombok.Getter;
import lombok.Setter;
import java.util.HashMap;
import java.util.Map;
@Getter
@Setter
//이전 버전과 다른점
//request객체를 model로 사용하지 않고 ModelView를 만들어서 반환한다.
public class ModelView {
private String viewName;
private Map<String, Object> model = new HashMap<>();
public ModelView(String viewName) {
this.viewName = viewName;
}
}
✔️ModelView 클래스는 요청을 처리한 결과를 담는 모델과 뷰 이름을 함께 관리하는 역할!
✔️viewName에 논리적인 뷰 이름을 저장한다.이 이름을 통해 뷰 리졸버가 실제 물리적 뷰 경로를 찾는다.
3) MyView 코드
package hello.servlet.web.frontcontroller;
import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;
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 static void modelToRequestAttribute(Map<String, Object> model, HttpServletRequest request) {
model.forEach((key, value)-> request.setAttribute(key,value));
}
}
✔️viewPath
- 뷰의 경로를 저장한다.
✔️생성자
- 요청을 포워딩할 뷰의 경로(viewPath)로 필드를 초기화한다.
✔️ render(HttpServletRequest, HttpServletResponse)
- render 메서드에서는 요청을 다른 JSP로 포워딩하는 객체(RequestDispatcher)를 이용하여 요청과 응답을 지정된 viewPath로 포워딩한다.
✔️ render(Map<String, Object>, HttpServletRequest, HttpServletResponse)
- model 데이터를 HttpServletRequest 객체에 속성으로 추가하고 요청을 해당 viewPath로 포워딩한다.
✔️ modelToRequestAttribute
- model 데이터를 HttpServletRequest 객체 속성으로 설정한다.
3) MyHandlerAdapter 코드
package hello.servlet.web.frontcontroller.v5;
import hello.servlet.web.frontcontroller.ModelView;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
public interface MyHandlerAdapter {
boolean supports(Object handler); //어댑터가 해당 컨트롤러를 처리할 수 있는지 판단하는 메서드.
ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException;
}
각 핸들러 어댑터에서는 MyHandlerAdapter 인터페이스를 구체화한다.
4) ControllerV3HandlerAdapter 코드
package hello.servlet.web.frontcontroller.v5.adapter;
import hello.servlet.web.frontcontroller.ModelView;
import hello.servlet.web.frontcontroller.v3.ControllerV3;
import hello.servlet.web.frontcontroller.v5.MyHandlerAdapter;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
public class ControllerV3HandlerAdapter implements MyHandlerAdapter {
@Override
public boolean supports(Object handler) { //어댑터가 해당 컨트롤러를 처리할 수 있는지 판단하는 메서드.
return (handler instanceof ControllerV3);//ControllerV3인지 확인하고 아니면 false 반환
}
@Override
public ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException {
ControllerV3 controller = (ControllerV3) handler; //ControllerV3인지는 supports를 호출해서 확인하기때문에 캐스팅해도 된다.
//Map형식 paramMap이 필요함.
Map<String, String> paramMap = createParamMap(request);
ModelView mv = controller.process(paramMap);
return mv;
}
private static Map<String, String> createParamMap(HttpServletRequest request) {
//paramMap을 넘겨줘야된다.
Map<String, String> paramMap = new HashMap<>();
//모든 parameterName을 다 가져옴.
//paramMap에 데이터를 다 집어넣는다.
request.getParameterNames().asIterator()
.forEachRemaining(paramName -> paramMap.put(paramName, request.getParameter(paramName)));
return paramMap;
}
}
✅CotrollerV3 코드
public interface ControllerV3 {
//서블릿 기술에 종속적이지 않다.
ModelView process(Map<String, String> paramMap);
}
✔️ ControllerV3HandlerAdapter클래스는 MyHandlerAdapter 인터페이스를 구현한다.
이 클래스는 ControllerV3 타입의 핸들러를 처리한다.
✔️ handle (HttpServletRequest request, HttpServletResponse response, Object handler)
- 요청에서 파라미터를 추출하여 paramMap을 생성
- ControllerV3 핸들러의 process 메서드를 호출하여 ModelView 객체를 생성
- ModelView 객체를 반환
✔️ createParamMap(HttpServletRequest request)
- 요청 객체에서 모든 파라미터를 추출하여 paramMap을 생성
- request.getParameterNames().asIterator()를 사용하여 모든 파라미터 이름을 반복
- 각 파라미터 이름과 해당 값을 paramMap에 추가
- paramMap 반환
5) ControllerV4HandlerAdapter 코드
package hello.servlet.web.frontcontroller.v5.adapter;
import hello.servlet.web.frontcontroller.ModelView;
import hello.servlet.web.frontcontroller.v4.ControllerV4;
import hello.servlet.web.frontcontroller.v5.MyHandlerAdapter;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
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형식 paramMap이 필요함.
Map<String, String> paramMap = createParamMap(request);
HashMap<String, Object> model = new HashMap<>();
//viewName 반환
String viewName = controller.process(paramMap, model);
//viewName을 넣어 ModelView를 반환
ModelView mv = new ModelView(viewName);
//model 값을 ModelView에 세팅
mv.setModel(model);
return mv;
}
private static Map<String, String> createParamMap(HttpServletRequest request) {
//paramMap을 넘겨줘야된다.
Map<String, String> paramMap = new HashMap<>();
//모든 parameterName을 다 가져옴.
//paramMap에 데이터를 다 집어넣는다.
request.getParameterNames().asIterator()
.forEachRemaining(paramName -> paramMap.put(paramName, request.getParameter(paramName)));
return paramMap;
}
}
✅CotrollerV4 코드
public interface ControllerV4 {
/**
*
* @param paramMap
* @param model
* @return viewName
*/
String process(Map<String, String> paramMap, Map<String, Object> model);
}
✔️ ControllerV4HandlerAdapter클래스는 MyHandlerAdapter 인터페이스를 구현한다.
이 클래스는 ControllerV4 타입의 핸들러를 처리한다.
✔️ handle(HttpServletRequest request, HttpServletResponse response, Object handler)
- ControllerV4 핸들러의 process 메서드를 호출하여 논리 뷰 이름(viewName)을 반환받고, model 데이터를 갱신한다.
- viewName을 사용하여 ModelView 객체를 생성하고, 모델 데이터를 ModelView 객체에 설정
- ModelView 객체를 반환
4. 디렉토리 구조
5. 정리
V5 버전의 작동 구조를 먼저 파악하고 코드를 이해하는 시간을 가졌다.
이렇게 110V, 220V 어댑터를 끼워서 콘센트에 맞추듯 다양한 컨트롤러를 핸들러 어댑터를 통해 호출할 수 있다.
이 구조를 이해하면 스프링MVC 구조도 이해할 수 있다!
'인프런 김영한 강의 정리 > 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술' 카테고리의 다른 글
스프링 MVC - MultiValueMap | 자료구조 (0) | 2024.06.01 |
---|---|
스프링 MVC - 로깅 알아보기 | System.out.println()을 사용하면 안되는 이유 (0) | 2024.06.01 |