출처 : https://inf.run/Ewvr
서블릿, JSP를 토대로 어떻게 MVC 패턴이 만들어졌는지 그 과정을 이해해보자.
코드 자체를 암기하기 보다는 흐름이 중요하다.
@WebServlet(name = "memberFormServlet", urlPatterns = "/servlet/members/newform")
public class MemberFormServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest request, HttpServletResponse
response)
throws ServletException, IOException {
response.setContentType("text/html");
response.setCharacterEncoding("utf-8");
PrintWriter w = response.getWriter();
w.write("<!DOCTYPE html>\n" +
"<html>\n" +
"<head>\n" +
" <meta charset=\"UTF-8\">\n" +
" <title>Title</title>\n" +
"</head>\n" +
"<body>\n" +
"<form action=\"/servlet/members/save\" method=\"post\">\n" +
" username: <input type=\"text\" name=\"username\" />\n" +
" age: <input type=\"text\" name=\"age\" />\n" +
" <button type=\"submit\">전송</button>\n" +
"</form>\n" +
"</body>\n" +
"</html>\n");
}
}
Servlet은 단순하게 회원정보를 입력할 수 있는 HTML FORM을 만들어서 응답한다.
자바 코드로 HTML을 제공해야하므로 매우 불편하다라는 생각이 든다.
@WebServlet(name = "memberListServlet", urlPatterns = "/servlet/members")
public class MemberListServlet extends HttpServlet {
private MemberRepository memberRepository = MemberRepository.getInstance();
@Override
protected void service(HttpServletRequest request, HttpServletResponse
response)
throws ServletException, IOException {
response.setContentType("text/html");
response.setCharacterEncoding("utf-8");
List<Member> members = memberRepository.findAll();
PrintWriter w = response.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>");
/*
w.write(" <tr>");
w.write(" <td>1</td>");
w.write(" <td>userA</td>");
w.write(" <td>10</td>");
w.write(" </tr>");
*/
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 안에 for문으로 동적으로 생성하고 응답하고 있다.
단순히 정적인 html 문서라면 화면이 게속 달라지는 회원의 저장 결과라던가 회원 목록 같은 동적 html을 만드는 것은 불가하다.
하지만 이러한 방식은 매우 복잡하고 비효율적이다.
자바 코드로 html을 만들어서 하는 것보다
차라리 HTML 문서에 동적인 부분만 자바코드로 넣을 수 있다면 더 편리하다라는 생각이 든다.
이것이 템플릿 엔진(JSP, Thymeleaf, Freemaker, Velocity ...)이 나온 이유다.
tmi) JSP는 성능과 기능 측면에서 다른 템플릿에 밀려 사장되어가는 추세이다.
스프링과 잘 통합되는 thymeleaf를 추후에 사용예정.
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<form action="/jsp/members/save.jsp" method="post">
username: <input type="text" name="username" />
age: <input type="text" name="age" />
<button type="submit">전송</button>
</form>
</body>
</html>
JSP문서에 경우 맨 첫 줄에
<%@ page contentType="text/html;charset=UTF-8" language="java" %> 가 있어야한다.
<%@ 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);
%>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
성공
<ul>
<li>id=<%=member.getId()%></li>
<li>username=<%=member.getUsername()%></li>
<li>age=<%=member.getAge()%></li>
</ul>
<a href="/index.html">메인</a>
</body>
</html>
JSP는 자바 코드를 그대로 사용할 수 있다.
<% ~ %> 부분에 자바코드를 입력할 수 있고
<%= ~ %> 해당 부분에 자바 코드를 출력할 수 있다.
JSP는 보다시피 HTML을 중심으로 하고, 자바 코드를 부분적으로 입력했다.
*) 서블릿과 JSP의 한계
서블릿을 활용할 때는 View 화면을 위한 HTML 작업이 자바 코드에 섞여서 지저분하고 복잡했다.
JSP를 사용해서 View를 생성하는 HTML 작업을 깔끔하게 가져가고, 동적이 필요한 부분만 자바 코드를 적용시켰다.
하지만 JSP 역시 단점이 있다.
자바 코드, 데이터 조회를 위한 Repository 등 다양한 코드가 모두 JSP 안에 있다.
JSP가 너무 많은 역할을 하다보니, 유지 보수에 큰 어려움이 있다.
그렇기에, 비즈니스 로직은 서블릿에서 처리하고, JSP는 목적에 맞게 HTML로 화면을 보이게 하는 역할에 집중하도록 하기로 했다. 이것이 MVC 패턴의 등장 배경이다.
MVC 패턴의 개요)
하나의 서블릿이나 JSP만으로도 비즈니스 로직과 뷰 렌더링까지 모두 처리하게 되면 너무 많은 역할을 하게 되고
유지 보수에 어려움을 겪게 된다.
또한 둘 사이에 변경의 라이프 사이클이 다르다. UI를 수정하는 일과 비즈니스 로직을 수정하는 일은
각자 다른 시기에 발생할 가능성이 매우 높고 대부분 서로 영향을 주지 않는다.
이런 상황에서 변경의 라이프 사이클이 다른 부분을 하나의 코드로 관리하는 것은 유지보수에 좋지 않다.
Model, View, Controller
MVC패턴은 하나의 서블릿이나 JSP로 처리하던 것을 컨트롤러(Controller)와 뷰(View)라는 영역으로 서로 역할을 나눈다
Controller: HTTP 요청을 받아서 파라미터를 검증하고, 비즈니스 로직을 실행한다.
그리고 뷰에 전달할 결과 데이터를 조회해서 모델에 담는다.
Model: 뷰에 출력할 데이터를 담아둔다.
뷰가 필요한 데이터를 모두 모델에 담아서 전달해주는 덕분에 뷰는 비즈니스 로직이나 데이터 접근을 몰라도 되고,
화면을 렌더링 하는 일에 집중할 수 있다.
View: 모델에 담겨있는 데이터를 사용해서 화면을 그리는 일에 집중한다. 여기서는 HTML을 생성하는 부분을 말한다
Controller에 비즈니스 로직을 넣을 순 있지만, 이렇게 되면 Controller가 너무 많은 역할을 한다.
그래서 일반적으로 비즈니스 로직은 서비스(Service)라는 계층을 별도로 만들어서 처리한다.
그리고 Controller는 비즈니스 로직이 있는 서비스를 호출한다.
해당 예시에선
서블릿을 Controller로, JSP를 View로, Model은 HTTPServletRequest 객체를 사용했다.
package hello.servlet.web.servletmvc;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@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);
}
}
dispatcher.forward() : 다른 서블릿이나 JSP로 이동할 수 있는 기능이다.
클라이언트에서 하는 게 아닌, 서버 내부에서 다시 호출이 발생하는 것이다.
/WEB-INF의 경로안에 있다면 외부에서 직접 JSP를 호출할 수 없다. Controller를 활용해서 JSP를 호출하는 것이다.
* redirect vs forward
리다이렉트는 실제 클라이언트에 응답이 갔다가 클라이언트가 redirect 경로로 다시 요청한다.
그렇기에 클라이언트가 인지할 수 있고, URL 경로도 변경이 된다.
하지만 forward의 경우 서버 내부에서 일어나기 때문에, 클라이언트가 전혀 인지할 수 없다.
하지만 MVC 패턴에도 한계가 명확하다.
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
View로 이동하는 코드가 항상 중복 호출이 되어야한다. 이 부분을 메소드로 묶어서 활용할 수도 있지만
이것도 결국엔 항상 직접 호출해야한다.
String viewPath = "/WEB-INF/views/new-form.jsp"
/WEB-INF , .jsp 부분이 계속 중복이 된다.
그리고 만약 다른 뷰 모델(thymeleaf 등)로 변경해야한다면 전체 코드를 변경해야한다.
기능이 복잡해질수록 컨트롤러에서 공통으로 처리해야하는 부분이 많이 증가한다.
이를 해결하기 위해, 컨트롤러 호출 이전에 먼저 공통 기능을 처리하게 한다.
이러한 역할을 흔히 수문장 역할로 비유하고, 이를 프론트 컨트롤러(Front Controller) 패턴을 도입해 해결할 수 있다.
스프링 MVC의 핵심도 바로 이 Front Controller에 있다.
'스프링 강의 필기 > 스프링 MVC 1편' 카테고리의 다른 글
7) 스프링 MVC - 웹 페이지 만들기 (0) | 2023.01.27 |
---|---|
6) 스프링 mvc 기본 기능 (0) | 2022.08.03 |
5) 스프링 MVC - 구조 이해 (0) | 2022.07.30 |
4) MVC 프레임워크 만들기 (0) | 2022.07.30 |
1) 웹 애플리케이션 이해 (0) | 2022.07.22 |