UL :)
UL의 개발 블로그
UL :)
전체 방문자
오늘
어제
  • 분류 전체보기 (220)
    • 일상 (1)
    • 회고록 (7)
    • ChatGPT 아카이빙 (0)
    • PS(Java) (114)
      • 백준 (37)
      • 인프런 강의 문제 (77)
    • Web (69)
      • Spring (18)
      • JPA (7)
      • JSP (9)
      • HTML5 (12)
      • CSS (19)
      • HTTP (0)
      • 보안 (2)
    • Language (5)
      • Java (3)
      • JS (1)
      • Python (1)
    • Git, GitHub (4)
    • Settings (18)
      • IntelliJ (7)
      • Eclipse (2)
      • VSCode (3)
      • Android Studio (1)
      • VMware (2)
      • Mac (0)
    • Etc (1)

블로그 메뉴

  • 홈
  • 태그

공지사항

인기 글

태그

  • 요청헤더
  • 영속성
  • consumes
  • @GetMapping
  • ORM
  • @JoinColumn
  • IDENTITY 전략
  • argumentresolver
  • HandlerMethodArgumentResolver
  • 정렬
  • @ManyToOne
  • 백준
  • ReturnValueHandler
  • 동일성보장
  • @Column
  • @RequestParam
  • 1차 캐시
  • ViewName반환
  • @Id
  • TABLE 전략
  • 엔티티 매핑
  • @Table
  • JPA
  • 영속성컨텍스트
  • SEQUENCE 전략
  • produces
  • HttpMessageConverter
  • @PostMapping
  • BOJ
  • EntityManagerFactory

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
UL :)

UL의 개발 블로그

[Inflearn] 스프링 MVC (1) MVC 프레임 워크 만들기
Web/Spring

[Inflearn] 스프링 MVC (1) MVC 프레임 워크 만들기

2022. 1. 26. 18:31

김영한 강사님의 '스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술' 강의 정리

강의를 듣고 개인적으로 정리한 글입니다. 코드와 그림 출처는 김영한 강사님께 있습니다. 문제 있을 시 알려주세요.

템플릿 엔진이란?

  • HTML에서 필요한 곳만 코드를 적용해서 동적 변경이 가능하게 해주는 소프트웨어
  • JSP, Thymeleaf 등이 있다.
  • JSP는 너무 많은 역할을 떠안아 유지보수가 힘든 단점이 있다. 그래서 역할 분리를 위해 나온 것이 MVC 패턴이다.
  • 요즘에는 Thymeleaf를 많이 사용한다.

 

MVC 패턴이 필요한 이유

  1. JSP와 같이 너무 많은 역할을 담당하면 한 곳에 많은 코드가 집적되어 유지보수가 어렵다.
  2. UI와 비즈니스 로직 등 변경의 사이클이 다른 부분을 하나로 관리하면 유지보수가 어렵다.
  3. 뷰 템플릿은 화면 렌더링에 최적화 돼있어 이 부분만 담당하는 것이 가장 효과적이다.

즉 유지보수와 개발을 쉽게하기 위해서는 역할분리를 하는 MVC 패턴이 필요하다.

 

MVC 패턴

MVC 패턴은 JSP가 처리하던 것을 Model, View, Controller 라는 영역으로 역할을 분리하는 것을 말한다. 웹 애플리케이션은 MVC 패턴을 사용하는 것이 일반적이다.

  • Controller : HTTP 요청을 받아서 파라미터를 검증하고, 비즈니스 로직을 실행한다. 그리고 뷰에 전달할 결과 데이터를 조회해서 모델에 담는다.
  • Model : 뷰에 출력할 데이터를 담아둔다.
  • View : 모델에 담겨있는 데이터를 사용해서 화면을 그린다.

가장 기본적인 MVC 패턴 구조는 아래 그림과 같다.

  • Controller - 서블릿
  • Model - HttpServletRequest 객체
  • View - JSP

간단하게 회원가입 시스템을 구현한 예제는 다음과 같다.
​

  1. 아이디와 패스워드를 입력하는 회원가입 페이지를 처리하는 코드
@WebServlet(name = "mvcMemberFormServlet", urlPatterns = "/servlet-mvc/members/new-form")
public class MvcMemberFormServlet extends HttpServlet {

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String viewPath = "/WEB-INF/views/new-form.jsp";
        RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
        dispatcher.forward(request,response);
    }
}
  1. 회원가입을 정보를 받아 저장한 후 완료 페이지를 처리하는 코드
@WebServlet(name = "mvcMemberSaveServlet", urlPatterns = "/servlet-mvc/members/save")
public class MvcMemberSaveServlet extends HttpServlet {

    private MemberRepository memberRepository = MemberRepository.getInstance();

    @Override
    protected void service(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);
        System.out.println("member = " + member);
        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);
    }
}
  1. 저장된 회원 정보 리스트를 보여주는 페이지를 처리하는 코드
@WebServlet(name = "mvcMemberListServlet", urlPatterns = "/servlet-mvc/members")
public class MvcMemberListServlet extends HttpServlet {

    private MemberRepository memberRepository = MemberRepository.getInstance();

    @Override
    protected void service(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);
    }
}

​
​

/WEB-INF/views/에 저장된 JSP 코드.
상대경로를 사용해서 재활용할 수 있게 구현한다.

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<!-- 상대경로 사용, [현재 URL이 속한 계층 경로 + /save] -->
<form action="save" method="post">
    username: <input type="text" name="username" />
    age: <input type="text" name="age" />
    <button type="submit">전송</button>
</form>
</body>
</html>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <meta charset="UTF-8">
</head>
<body>
성공
<ul>
    <li>id=${member.id}</li>
    <li>username=${member.username}</li>
    <li>age=${member.age}</li>
</ul>
<a href="/index.html">메인</a>
</body>
</html>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<html>
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<a href="/index.html">메인</a>
<table>
    <thead>
    <th>id</th>
    <th>username</th>
    <th>age</th>
    </thead>
    <tbody>
    <c:forEach var="item" items="${members}">
        <tr>
            <td>${item.id}</td>
            <td>${item.username}</td>
            <td>${item.age}</td>
        </tr>
    </c:forEach>
    </tbody>
</table>
</body>
</html>

 

MVC 컨트롤러의 단점을 개선해보자

[1] 공통처리가 어렵다

  • 기능이 복잡해질 수록 컨트롤러에서 공통으로 처리해야 하는 부분이 더 많아진다.
  • 메서드로 분리하기 보다는(호출 중복 및 호출하지 않는 실수 등의 문제로) 컨트롤러 호출 전에 먼저 공통 기능을 처리하는 편이 좋다.

해결: FrontController 도입

  • Map 데이터에 URL과 컨트롤러를 매핑해둔다.
  • 프론트 컨트롤러가 요청에 맞는 컨트롤러를 찾아서 호출한다
  • 프론트 컨트롤러를 제외한 나머지 컨트롤러는 서블릿을 사용하지 않아도 된다(직접 호출 해줄거니까)
@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);
    }
}
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 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);
    }
}

 

[2] 뷰로 이동하는 코드가 중복된다

JSP의 경우 실제 뷰가 동작 되도록 랜더링 하는 foward 코드가 중복된다.

해결: MyView 클래스로 분리하자

프론트 컨트롤러에서 MyView 클래스에 viewPath 를 저장하고, render함수를 호출하면

실제 뷰가 동작되도록 랜더링하도록 하자. (JSP의 경우 foward작동)

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);//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));
    }
}
@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);
    }
}
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");
    }
}

그러면 이런 구조가 된다.

 

[3] viewPath가 중복된다

viewPath가 중복되면 다른 뷰로 변경 시 Controller 마다 전체 코드를 다 바꿔야 한다.

해결: viewResolver 메서드로 논리뷰를 받아 절대뷰로 바꾸자

  • 논리뷰 :"members"
  • 절대뷰 : "/WEB-INF/views/members.jsp"
/* FrontController.java */
    private MyView viewResolver(String viewName) {
        return new MyView("/WEB-INF/views/" + viewName + ".jsp");
    }
}

vieResolver 함수는FrontController에서 컨트롤러가 반환한 뷰의 논리 이름 을 실제 물리 뷰 경로로 변경 한 후, MyView 객체에 담아서 반환한다.

이렇게 해두면 향후 뷰의 폴더 위치가 함께 이동해도 FrontController만 고치면 된다.

 

[4] 컨트롤러가 서블릿에 종속되어 있다

  • HttpServletRequest,HttpServletResponse를 사용하지 않을 때에도 작성
  • HttpServletRequest,HttpServletResponse를 사용하는 코드는 테스트 케이스 작성하기도 어렵다.

해결 : request 객체 말고, 별도 Model 객체를 반환하자

FrontController에서 요청파라미터를 request, response 대신 Map으로 Controller에 넘긴다.

@WebServlet(name = "frontControllerServletV3", urlPatterns = "/front-controller/v3/*")
public class FrontControllerServletV3 extends HttpServlet {
    ...

    @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);//컨트롤러에 파라미터 전달 후 ModelView 받아옴

        //논리 이름 -> 물리 이름 : viewResolver
        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;
    }
}

그러면 Controller는 ModelView에 논리뷰와 파라미터를 담아 FrontController에 반환한다.

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;
    }
}
@Getter @Setter //Lombok
public class ModelView {
    private String viewName; //논리뷰
    private Map<String, Object> model = new HashMap<>();

    public ModelView(String viewName) {
        this.viewName = viewName;
    }
}

여기 까지 바꿨을 때 구조는 다음과 같다.

 

[5] 한 가지 방식의 컨트롤러 인터페이스만 사용할 수 있다

ControllerV3, ControllerV4 등 완전히 다른 인터페이스를 상속하는 컨트롤러가 있다면 어떻게 처리해야하는가? 프론트 컨트롤러가 다양한 방식의 컨트롤러를 처리할 수 있도록 변경해보자

해결 : 어댑터 패턴을 사용해서 여러 개 호환되게 해보자

  • 핸들러 어댑터: 어떤 한 종류의 핸들러를 처리할 수 있는지를 핸들러 어댑터로 구분한다.
    • 예를 들어ControllerV3HandlerAdpater는 ControllerV3 인터페이스를 상속하는 컨트롤러만 처리할 수 있다.
  • 핸들러(=컨트롤러): 어댑터가 있으면 컨트롤러 뿐만 아니라 어떤 것이든 다 처리할 수 있으므로 앞으로 더넓은 의미의 뜻인 핸들러로 얘기하자.

​

어댑터 패턴을 적용하면 다음과 같은 구조가 된다.

​

먼저 FrontController의 Map 데이터에 URL과 핸들러를 매핑시켜놓고 List 데이터에 핸들러 어댑터 목록을 추가한다.

  1. HTTP request가 들어오면 FrontController는 핸들러매핑정보에서 URL로 사용할 핸들러를 찾아온다.
  2. 사용할 핸들러를 처리할 수 있는 핸들러 어댑터를 찾아온다.
  3. 핸들러어댑터의 handle 메서드를 호출해 파라미터와 핸들러를 전달한다.
  4. 핸들러를 작동시키고 (process 메서드 호출) ModelView를 받아 FrontController에 반환한다.
@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); //핸들러(컨트롤러)에 파라미터 전달 후 ModelView 받아옴

        //논리 이름 -> 물리 이름 : viewResolver
        String viewName = mv.getViewName();
        MyView view = viewResolver(viewName);

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

    private MyHandlerAdapter getHandlerAdapter(Object handler) {
        MyHandlerAdapter a;
        for (MyHandlerAdapter adapter : handlerAdapters) {
            if(adapter.supports(handler)){
                return adapter; //지원하면 선택
            }
        }
        throw new IllegalArgumentException("handler adapter를 찾을 수 없습니다. handler=" + handler);
    }

    private Object getHandler(HttpServletRequest request) {
        String requestURI = request.getRequestURI();
        return handlerMappingMap.get(requestURI);
    }

    private MyView viewResolver(String viewName) {
        return new MyView("/WEB-INF/views/" + viewName + ".jsp");
    }
}
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 {
        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;
    }
}

 

사실 이렇게 MVC를 개선한 결과는 스프링 MVC의 구조이다

우리가 개선한 것  → 스프링 MVC
FrontController DispatcherServlet
handlerMappingMap HandlerMapping
MyHandlerAdapter HandlerAdapter
ModelView ModelAndView
viewResolver (메서드) ViewResolver (인터페이스)
MyView View(인터페이스)
저작자표시 비영리 변경금지 (새창열림)

'Web > Spring' 카테고리의 다른 글

[Inflearn] 스프링 MVC (5) HTTP 요청  (0) 2022.01.28
[Inflearn] 스프링 MVC (4) 요청 매핑  (0) 2022.01.28
[Inflearn] 스프링 MVC (3) 로깅  (0) 2022.01.27
[Inflearn] 스프링 MVC (2) 구조 이해  (0) 2022.01.27
[Inflearn] 스프링 설정 & 생성  (0) 2022.01.03
    'Web/Spring' 카테고리의 다른 글
    • [Inflearn] 스프링 MVC (4) 요청 매핑
    • [Inflearn] 스프링 MVC (3) 로깅
    • [Inflearn] 스프링 MVC (2) 구조 이해
    • [Inflearn] 스프링 설정 & 생성
    UL :)
    UL :)
    백엔드 개발자를 목표로 달리고 있습니다🔥

    티스토리툴바