본문 바로가기

스프링 부트

📋 Spring MVC - 3. 서블릿, JSP, MVC 패턴

2021-05-03글

서블릿, JSP, MVC 패턴

서블릿으로 만들어보고, JSP로 만들어보고 마지막으로 MVC로 만들어 볼 것이다!

서블릿으로 만들었을 때 일부 예제

@WebServlet(name = "memberListServlet", urlPatterns = "/servlet/members")
public class MemberListServlet extends HttpServlet {
    private MemberRepository memberRepository = MemberRepository.getInstance();

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        List<Member> members = memberRepository.findAll();
        PrintWriter w = resp.getWriter();
        w.write("<html>");
        w.write("<head>");
        w.write("    <meta charset=\"UTF-8\">");
        w.write("    <title>Title</title>");
        w.write("</head>");
        w.write("<body>");
        w.write("<a href=\"/index.html\">메인</a>");
        w.write("<table>");
        w.write("    <thead>");
        w.write("    <th>id</th>");
        w.write("    <th>username</th>");
        w.write("    <th>age</th>");
        w.write("    </thead>");
        w.write("    <tbody>");
        for (Member member : members) {
            w.write("    <tr>");
            w.write("<td>" + member.getId() + "</td>");
            w.write("<td>" + member.getUserName() + "</td>");
            w.write("<td>" + member.getAge() + "</td>");
            w.write("    </tr>");
        }
        w.write("    </tbody>");
        w.write("</table>");
        w.write("</body>");
        w.write("</html>");
    }
}
  • 복잡하지만... 동적으로 원하는 HTML을 만들 수 있다.
  • 각 요청마다 Servlet을 만들어야 한다.
  • 응답할 HTML을 자바 코드 내에 작성해야한다.

JSP

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
import문 작성
<%@ page import="hello.servlet.domain.member.MemberRepository" %>
<%@ page import="hello.servlet.domain.member.Member" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
    // request, response 사용 가능
    MemberRepository memberRepository = MemberRepository.getInstance();
    System.out.println("save.jsp");
    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);

%>

<% %> 안에 자바코드를 작성할 수 있다.

  • 비즈니스 로직과 뷰가 연결되어 있다.
  • 요구사항이 변경되면 뷰를 수정하다가 비즈니스 로직도 같이 수정될 수 있다.
  • 유지보수성이 떨어진다.

MVC

Servlet, JSP의 문제점

너무 많은 역할

하나의 서블릿이나 JSP만으로 비즈니스 로직과 뷰 렌더링까지 모두 처리하게 되면, 너무 많은 역할을 하고, 또한 유지보수가 어려워진다.

변경의 라이프 사이클

비즈니스 로직과 뷰의 변경의 라이프 사이클이 다르다.
예를 들어서 UI 를 일부 수정하는 일과 비즈니스 로직을 수정하는 일은 각각 다르게 발생할 가능성이 매우 높고 대부분 서로에게 영향을 주지 않는다.
이렇게 변경의 라이프 사이클이 다른 부분을 하나의 코드로 관리하는 것은 유지보수하기 좋지 않다.
레이어를 분리하는 기준은 변경 주기가 다를 때라고 생각하면 편하다.

기능 특화

뷰 템플릿은 화면을 렌더링 하는데 최적화 되어 있기 때문에 이 부분의 업무만 담당하는 것이 가장 효과적이다.

Model View Controller

MVC 패턴은 서블릿이나, JSP로 처리하던 것을 컨트롤러(Controller)와 뷰(View)라는 영역으로 서로 역할을 나눈 것을 말한다.
웹 애플리케이션은 보통 이 MVC 패턴을 사용한다.

Controller

HTTP 요청을 받아서 파라미터를 검증하고, 비즈니스 로직을 실행한다. 그리고 뷰에 전달할 결과 데이터를 조회해서 모델에 담는다.
중앙에서 조종하는 역할. (비즈니스 로직을 담을 수 있지만, 역할이 너무 많아지기 때문에 비즈니스 로직을 호출한다고 하자.)

Model

뷰에 출력할 데이터를 담아둔다.
뷰가 필요한 데이터를 모두 모델에 담아서 전달해주는 덕분에 뷰는 비즈니스 로직이나 데이터 접근을 몰라도 되고, 화면을 렌더링 하는 일에 집중할 수 있다.

View

모델에 담겨있는 데이터를 사용해서 화면을 그리는 일에 집중한다. 여기서는 HTML을 생성하는 부분을 말한다.

MVC 적용하기

  • 서블릿을 컨트롤러로 사용
  • JSP를 뷰로 사용
  • HttpServletRequest 객체를 모델로 사용 (request.setAttribute() , request.getAttribute()사용)

MvcMemberFormServlet

@WebServlet(name = "mvcMemberFormServlet", urlPatterns = "/servlet-mvc/members/new-form")
public class MvcMemberFormServlet extends HttpServlet {
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String viewPath = "/WEB-INF/views/new-form.jsp";
        RequestDispatcher requestDispatcher = req.getRequestDispatcher(viewPath); // 컨트롤러에서 뷰로 이동
        requestDispatcher.forward(req, resp); // 서블릿에서 JSP를 호출한다.
    }
}
  • dispatcher.forward() : 다른 서블릿이나 JSP로 이동할 수 있는 기능이다. 서버 내부에서 다시 호출이 발생한다.
    클라이언트에게 갔다가 오는게 아닌(리다이렉트가 아닌), 서버 내부에서 호출한 것이다.
  • /WEB-INF : 이 경로안에 JSP가 있으면 외부에서 직접 JSP를 호출할 수 없어 컨트롤러를 통해서만 접근이 가능하도록 한다.

redirect vs forward

  • redirect : 실제 클라이언트(웹 브라우저)에 응답이 나갔다가, 클라이언트가 redirect 경로로 다시 요청한다.
    클라이언트가 인지할 수 있고, URL 경로도 실제로 변경된다.
  • forward : 서버 내부에서 일어나는 호출이기 때문에 클라이언트가 인지하지 못한다.

MvcMemberSaveServlet

@WebServlet(name = "mvcMemberSaveServlet", urlPatterns = "/servlet-mvc/members/save")
public class MvcMemberSaveServlet extends HttpServlet {
    MemberRepository memberRepository = MemberRepository.getInstance();

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String username = req.getParameter("username");
        int age = Integer.parseInt(req.getParameter("age"));

        // 비즈니스 로직
        Member member = new Member(username, age);
        memberRepository.save(member);

        // Model에 데이터를 보관한다. - req 객체에 Map이 있는데 여기에 저장한다.
        req.setAttribute("member", member);

        String viewPath = "/WEB-INF/views/save-result.jsp";
        RequestDispatcher requestDispatcher = req.getRequestDispatcher(viewPath);
        requestDispatcher.forward(req, resp);
    }
}

save-result.jsp

<%@ 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>
  • ${} : jsp가 제공하는 프로퍼티 접근법으로, req에 담겨있는 attribute에 있는 데이터를 가져올 수 있다.

MvcMemberListServlet

@WebServlet(name = "mvcMemberListServlet", urlPatterns = "/servlet-mvc/members")
public class MvcMemberListServlet extends HttpServlet {
    private MemberRepository memberRepository = MemberRepository.getInstance();

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        List<Member> members = memberRepository.findAll();

        req.setAttribute("members", members);

        String viewPath = "/WEB-INF/views/members.jsp";
        RequestDispatcher dispatcher = req.getRequestDispatcher(viewPath);
        dispatcher.forward(req, resp);
    }
}

members.jsp 일부

<c:forEach var="item" items="${members}">
    <tr>
        <td>${item.id}</td>
        <td>${item.username}</td>
        <td>${item.age}</td>
    </tr>
</c:forEach>
  • <c:forEach> : <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> 를 선언해야한다.

MVC 패턴의 한계

forward의 중복

메서드를 추출해도 되지만, 메서드 호출마저 중복으로 일어난다.

ViewPath에 중복

prefix인 /WEB-INF/views/ 와 suffix인 .jsp 가 중복으로 발생한다.
만약 jsp가 아닌 thymeleaf 같은 다른 뷰로 변경한다면 전체 코드를 다 변경해야 한다.

사용하지 않는 코드

HttpServletRequest request, HttpServletResponse response

위 객체는 사용할 수도 있고, 안할수도 있다.

공통 처리가 어렵다.

컨트롤러에서 공통으로 처리해야 하는 부분이 점점 증가하는 경우를 생각해보자.
단순히 공통 기능을 메서드로 뽑으면 될 것 같지만, 결과적으로 해당 메서드를 항상 호출해야 하고, 실수로 호출하지 않으면 문제가 된다.
또한 호출하는 것 자체도 중복이다.

프론트 컨트롤러(Front Controller) 패턴을 도입하면 이런 문제를 깔끔하게 해결할 수 있다.


✍️ 김영님의 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 강의 노트 ✍️

'스프링 부트' 카테고리의 다른 글

📋 Spring MVC - 5. 스프링 MVC  (0) 2021.08.06
📋 Spring MVC - 4. MVC 프레임워크 만들기  (0) 2021.08.06
📋 Spring MVC - 2. 서블릿  (0) 2021.08.06
@Valid와 @Validated  (0) 2021.08.06
@JsonProperty, @JsonNaming  (0) 2021.08.06