[Spring] 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 - 강의 정리 - 4

JJAM·2022년 9월 16일
0
post-thumbnail

📖 MVC 프레임워크 만들기

📒 프론트 컨트롤러 패턴 소개

FrontController 패턴

  • 프론트 컨트롤러 서블릿 하나로 클라이언트의 요청을 받음
  • 요청에 맞는 컨트롤러를 찾아서 호출
  • 공통 처리 가능
  • 나머지 컨트롤러는 서블릿을 사용하지 않아도 됨

📒 프론트 컨트롤러 도입 - v1

프론트 컨트롤러 기능을 도입할 것이다.

✏️ 컨트롤러 인터페이스 생성

프론트 컨트롤러가 인터페이스를 호출해서 구현과 관계없이 로직의 일관성을 가져가기 위해,
서블릿과 비슷한 모양의 컨트롤러 인터페이스를 생성한다.

src/main/java/hello/servlet/web/frontcontroller/v1/ControllerV1

public interface ControllerV1 {
    void process(HttpServletRequest request, HttpServletResponse response) 
            throws ServletException, IOException;
}

✏️ 컨트롤러 생성

인터페이스를 구현한 회원 등록 폼, 회원 저장, 회원 목록 조회 컨트롤러를 만든다.

src/main/java/hello/servlet/web/frontcontroller/v1/controller/MemberFormControllerV1

public class MemberFormControllerV1 implements ControllerV1 {

    @Override
    public void process(HttpServletRequest request, HttpServletResponse response) 
            throws ServletException, IOException {

        // MvcMemberFormServlet과 동일한 내용
    }
}

src/main/java/hello/servlet/web/frontcontroller/v1/controller/MemberSaveControllerV1

public class MemberSaveControllerV1 implements ControllerV1 {

    @Override
    public void process(HttpServletRequest request, HttpServletResponse response) 
            throws ServletException, IOException {

        // MvcMemberSaveServlet과 동일한 내용
    }
}

src/main/java/hello/servlet/web/frontcontroller/v1/controller/MemberListControllerV1

public class MemberListControllerV1 implements ControllerV1 {

    @Override
    public void process(HttpServletRequest request, HttpServletResponse response) 
            throws ServletException, IOException {

        // MvcMemberListServlet과 동일한 내용
    }
}

✏️ 프론트 컨트롤러 생성

클라이언트의 요청을 받고, 매핑 정보로 컨트롤러를 호출해주는 프론트 컨트롤러를 생성한다.

src/main/java/hello/servlet/web/frontcontroller/v1/FrontControllerServletV1

프론트 컨트롤러는 서블릿이므로 서블릿 어노테이션을 추가한다.

@WebServlet(name = "frontControllerServletV1", urlPatterns = "/front-controller/v1/*")

매핑 정보를 담을 HashMap을 만들어 준다.

private Map<String, ControllerV1> controllerMap = new HashMap<>();

// mapping 정보
public FrontControllerServletV1() {
	controllerMap.put("/front-controller/v1/members/new-form", new MemberFormControllerV1());
		...
}

URI를 조회해 호출할 컨트롤러를 Map에서 찾고, 만약 없으면 404 상태 코드를 반환한다.

컨트롤러를 찾으면 controller.process(request, response); 을 호출해서 해당 컨트롤러를 실행한다.

// ex) /front-controller/v1/members/new-form
String requestURI = request.getRequestURI();

// URI로 controllerMap에서 호출할 컨트롤러 찾음
ControllerV1 controller = controllerMap.get(requestURI);

// 해당 controller에 대한 mapping 정보가 없으면
if(controller == null) { 
	response.setStatus(HttpServletResponse.SC_NOT_FOUND); // 404
	return;
}

// 컨트롤러 호출
controller.process(request, response);

http://localhost:8080/front-controller/v1/members/new-form에 들어가면 정상적으로 실행되는 것을 확인할 수 있다.

📒 View 분리 - v2

모든 컨트롤러에서 로 이동하는 부분에 중복이 있고, 깔끔하지 않아 별도로 를 처리하는 객체를 만들 것이다.

✏️ MyView 생성

src/main/java/hello/servlet/web/frontcontroller/MyView

public class MyView {

    // 뷰 경로
    private String viewPath;

    public MyView(String viewPath) {
        this.viewPath = viewPath;
    }

    // rendering, 뷰로 이동
    public void render(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
        dispatcher.forward(request,response);
    }

}

✏️ 컨트롤러 인터페이스 생성

src/main/java/hello/servlet/web/frontcontroller/v2/ControllerV2

v1에서 만든 컨트롤러 인터페이스와 다르게, v2에서는 MyView를 반환해준다.

왜냐하면 v1에서는 컨트롤러에서 알아서 view를 호출했기 때문이다.

// MyView 반환
MyView process(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException;

✏️ 컨트롤러 생성

MyView로 인해 로 이동하는 3줄의 코드를 1줄로 줄일 수 있게 되었다.

src/main/java/hello/servlet/web/frontcontroller/v2/controller/MemberFormControllerV2

// 기존 코드
String viewPath = "/WEB-INF/views/new-form.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);// 변경 코드
return new MyView("/WEB-INF/views/new-form.jsp");

똑같이 MemberSaveControllerV2, MemberListControllerV2 컨트롤러를 만들어,
MyView 객체를 생성하고, 이름을 넣어 반환해준다.

✏️ 프론트 컨트롤러 생성

기존 v1 코드의 내용과 똑같으며 대신 v1에서 컨트롤러를 호출 했던 부분을
MyView를 호출하여, 뷰(JSP) 로 이동하는 코드로 변경한다.

src/main/java/hello/servlet/web/frontcontroller/v2/FrontControllerServletV2

// 기존 코드
controller.process(request, response);// 변경 코드              
MyView view = controller.process(request, response); // view: return new MyView("...");
view.render(request, response); // rendering, 뷰로 이동

http://localhost:8080/front-controller/v2/members/new-form에 들어가면 정상적으로 실행되는 것을 확인할 수 있다.

📒 Model 추가 - v3

서블릿 종속성 제거

  • HttpServletRequest, HttpServletResponse가 필요하지 않은 컨트롤러 있음
  • 요청 파라미터 정보를 자바의 Map으로 대신 넘김

뷰 이름 중복 제거

  • 컨트롤러에서 지정하는 뷰 이름 중복
  • 컨트롤러는 뷰의 논리 이름을 반환, 실제 물리 위치의 이름은 프론트 컨트롤러에서 처리

ModelView

  • 서블릿의 종속성을 제거하기 위해 Model을 직접 만들고, 추가로 View 이름까지 전달하는 객체
  • 컨트롤러에 HttpServletRequest를 사용할 수 없어 request.setAttribute()도 호출할 수 없다.

✏️ ModelView 생성

src/main/java/hello/servlet/web/frontcontroller/ModelView

ModelView에는 view의 논리 이름과, Model이 있다.

private String viewName; // view 논리 이름
private Map<String, Object> model = new HashMap<>(); // model

그리고 viewName에 대한 생성자를 만들고, viewName과 model의 getter와 setter를 만들어 준다.

✏️ 컨트롤러 인터페이스 생성

src/main/java/hello/servlet/web/frontcontroller/v3/ControllerV3

v3의 컨트롤러 인터페이스는 방금 위에서 만든 ModelView를 반환해준다.

// modelview 반환
ModelView process(Map<String, String> paramMap);

✏️ 컨트롤러 생성

src/main/java/hello/servlet/web/frontcontroller/v3/controller/MemberSaveControllerV3

파라미터 정보는 map에 담겨있어, map에서 필요한 요청 파라미터를 조회한다.

// 기존 코드
String username = request.getParameter("username");// 변경 코드  
String username = paramMap.get("username");

view의 논리적 이름을 넣어, ModelView를 생성하고,
모델은 단순한 map이므로 모델에 뷰에서 필요한 member 객체를 넣고 반환한다.

// 기존 코드
request.setAttribute("member",member);
return new MyView("/WEB-INF/views/save-result.jsp");// 변경 코드
ModelView mv = new ModelView("save-result");
mv.getModel().put("member",member);
return mv;

ModelView로 인해 서블릿 종속성을 제거하였고, 뷰 이름 중복도 제거하였다.

이를 활용해 MemberFormControllerV3, MemberListControllerV3 컨트롤러를 만들어, ModelView 객체를 생성해 반환해준다.

✏️ 프론트 컨트롤러 생성

v3의 프론트 컨트롤러에서는 paramMap을 만들어 주고, 논리 이름으로 실제 경로를 찾을 수 있게 만들어야 한다.

src/main/java/hello/servlet/web/frontcontroller/v3/FrontControllerServletV3

요청한 request에 있는 모든 정보를 꺼내, paramMap에 넣어주고
controller를 호출해, 논리 이름이 들어간 ModelView를 반환해준다.

    // paramMap
    Map<String, String> paramMap = createParamMap(request);
    ModelView mv = controller.process(paramMap);
}

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;
}

ModelView에 있는 논리 이름을 viewResolver를 통해 실제 경로 이름을 반환한다.

    // 논리 이름 -> 실제 경로 이름
    String viewName = mv.getViewName();
    MyView view = viewResolver(viewName);
}

// 실제 경로
private MyView viewResolver(String viewName) {
	return new MyView("/WEB-INF/views/" + viewName + ".jsp");
}

model을 함께 넣어 viewrendering 한다.

view.render(mv.getModel(), request, response);

그러나 render를 할 때, 파라미터 3개에 대한 메서드가 없어 새로 추가해준다.

src/main/java/hello/servlet/web/frontcontroller/v3/MyView

JSP는 request.getAttribute() 로 데이터를 조회하기 때문에, model에 있는 데이터를 꺼내 request.setAttribute()requestmodel 데이터를 담는다.

// v3 rendering
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));
}

그러면 http://localhost:8080/front-controller/v3/members/new-form 에 들어가면 정상적으로 작동하는 것을 확인할 수 있다.

📒 단순하고 실용적인 컨트롤러 - v4

위에서 만든 v3는 ModelView 객체를 생성하고 반환해야 하는 부분이 조금은 번거로워, 좀 더 편리하게 v4를 만들 것이다.

v4는 v3와 다르게 컨트롤러가 ModelView가 아닌 ViewName을 반환한다.

✏️ 컨트롤러 인터페이스 생성

src/main/java/hello/servlet/web/frontcontroller/v4/ControllerV4

param로 paramMapmodel을 받아 컨트롤러를 호출하고, ViewName을 반환받는다.

String process(Map<String, String> paramMap, Map<String, Object> model);

✏️ 컨트롤러 생성

src/main/java/hello/servlet/web/frontcontroller/v4/controller/MemberSaveControllerV4

ModelView를 생성하고, model에 데이터를 보관할 필요없이
param으로 받은 model에 바로 데이터를 보관하고 viewName을 반환해준다.

// 기존 코드
ModelView mv = new ModelView("save-result");
mv.getModel().put("member",member);
return mv;// 변경 코드
model.put("member", member);
return "save-result"; 

이를 활용해 MemberFormControllerV4, MemberListControllerV4 컨트롤러를 만들어, viewName 반환해준다.

✏️ 프론트 컨트롤러 생성

src/main/java/hello/servlet/web/frontcontroller/v4/FrontControllerServletV4

v4에서는 프론트 컨트롤러에서 컨트롤러를 호출할 때, param으로 model을 넘겨주기 때문에
model을 생성하여 컨트롤러를 호출하고, viewName을 반환 받는다.

// 기존 코드
ModelView mv = controller.process(paramMap);// 변경 코드
Map<String, Object> model = new HashMap<>();
String viewName = controller.process(paramMap, model);

그래서 반환받은 viewName으로 바로 실제 경로 이름을 찾고, rendering 한다.

MyView view = viewResolver(viewName);

view.render(model, request, response);

그러면 http://localhost:8080/front-controller/v4/members/new-form에 들어가면 정상적으로 작동하는 것을 볼 수 있다.

📒 유연한 컨트롤러1 - v5

만약 ControllerV3, ControllerV4 방식 둘 다 사용하고 싶을 때,
어탭터 패턴을 사용하여 프론트 컨트롤러가 다양한 방식의 컨트롤러를 처리할 수 있도록 변경할 수 있다.

핸들러 어댑터

중간에 어댑터 역할을 하는 어댑터
다양한 종류의 컨트롤러를 호출해줌

핸들러

컨트롤러의 이름을 더 넓은 범위
해당하는 종류의 어댑터만 있으면 다 처리

✏️ 어댑터 인터페이스 생성

src/main/java/hello/servlet/web/frontcontroller/v5/MyHandlerAdapter

어댑터가 해당 컨트롤러를 처리할 수 있는지 boolean 타입으로 판단해주고,
어댑터가 실제 컨트롤러를 호출하고, 그 결과로 ModelView를 반환하는 인터페이스를 만들어 준다.

boolean supports(Object handler);
ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) 
            throws ServletException, IOException;

✏️ 어댑터 생성

핸들러를 호출하여, ModelView를 반환하는 핸들러 어댑터를 생성한다.

src/main/java/hello/servlet/web/frontcontroller/v5/adapter/ControllerV3HandlerAdapter

핸들러(컨트롤러)가 ControllerV3를 상속받는지 확인하는 메서드를 만든다.

@Override
public boolean supports(Object handler) {
    return (handler instanceof ControllerV3);
}

핸들러(컨트롤러)ControllerV3에 맞게 변환하고,

ControllerV3는 paramMap을 받아 ModelView를 반환하기 때문에, 형식에 맞게 핸들러를 호출하여 ModelView를 반환한다.

@Override
public ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
		throws ServletException, IOException {

    ControllerV3 controller = (ControllerV3) handler;

    Map<String, String> paramMap = createParamMap(request);
    ModelView mv = controller.process(paramMap);

    return mv;
}

✏️ 프론트 컨트롤러 생성

src/main/java/hello/servlet/web/frontcontroller/v5/FrontControllerServletV5

모든 핸들러(컨트롤러) 가 들어갈 수 있는 map과 핸들러 어탭터가 들어있는 List 생성한다.

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());
    	...
}

// 핸들러 어댑터 등록
private void initHandlerAdapters() {
	handlerAdapters.add(new ControllerV3HandlerAdapter());
}

그러면 핸들러 매핑 정보를 통해 핸들러(컨트롤러) 를 조회하고,
핸들러 어댑터 목록에서 핸들러(컨트롤러)를 처리할 수 있는 핸들러 어탭터를 조회한다.

    Object handler = getHandler(request);
    MyHandlerAdapter adapter = getHandlerAdapter(handler);
}

// URI에 대한 handler(controller)를 handlerMappingMap에서 찾아서 반환
private Object getHandler(HttpServletRequest request) {
    String requestURI = request.getRequestURI();
    return handlerMappingMap.get(requestURI);
}

// handler를 처리할 수 있는 핸들러 어댑터 조회하여 반환
private MyHandlerAdapter getHandlerAdapter(Object handler) {
    for (MyHandlerAdapter adapter : handlerAdapters) {
        if (adapter.supports(handler)) { // 어댑터가 handler를 지원하는지
            return adapter;
        }
    }
    throw new IllegalArgumentException("handler adapter를 찾을 수 없습니다. handler=" + handler);
}

그리고 핸들러 어댑터로 핸들러(컨트롤러) 를 호출하고, modelView 반환한다.

ModelView mv = adapter.handle(request, response, handler);

프론트 컨트롤러는 반환받은 modelView로 viewResolver를 호출해 Myview를 반환하고, render(model) 호출한다.

    MyView view = viewResolver(mv.getViewName());
    view.render(mv.getModel(), request, response);
}

// 논리 이름 -> 실제 경로 이름
private MyView viewResolver(String viewName) {
	return new MyView("/WEB-INF/views/" + viewName + ".jsp");
}

http://localhost:8080/front-controller/v5/v3/members/new-form에 들어가면 정상적으로 작동되는 것을 확인할 수 있다.

📒 유연한 컨트롤러2 - v5

ControllerV4도 사용할 수 있도록 기능을 추가할 것이다.

✏️ 어댑터 생성

src/main/java/hello/servlet/web/frontcontroller/v5/adapter/ControllerV4HandlerAdapter

ControllerV4를 지원하는 어댑터를 만들어 준다.

ControllerV3HandlerAdapter와 다른 점은
ControllerV4는 param으로 paramMap, model을 받아 논리 이름 viewName 반환한다는 점이다.

Map<String, String> paramMap = createParamMap(request);
HashMap<String, Object> model = new HashMap<>();

String viewName = controller.process(paramMap, model);

그러나 어댑터ModelView로 반환해야 하므로, 어댑터ModelView 형식으로 맞추어 반환해준다.

ModelView mv = new ModelView(viewName);
mv.setModel(model);
return mv;

✏️ 프론트 컨트롤러 수정

src/main/java/hello/servlet/web/frontcontroller/v5/FrontControllerServletV5

핸들러 매핑에 ControllerV4를 사용하는 핸들러(컨트롤러)를 추가하고,
해당 컨트롤러를 처리하는 어댑터인 ControllerV4HandlerAdapter도 추가한다.

private void initHandlerMappingMap() {
		...
	handlerMappingMap.put("/front-controller/v5/v4/members/new-form", new MemberFormControllerV4());
		...
}
private void initHandlerAdapters() {
		...
	handlerAdapters.add(new ControllerV4HandlerAdapter());
}

그러면 http://localhost:8080/front-controller/v5/v4/members/new-form에 들어가면 정상적으로 작동하는 것을 확인할 수 있다.


지금까지 김영한 - 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술(유료강의) 강의를 참고하여 MVC 프레임워크 만들기 에 대해 공부하였다.

profile
☘️

0개의 댓글