참고자료:
MVC 패턴이란건 왜 나왔는가? 이전 포스팅까지 그 이유를 알아봤다.
서블릿, JSP를 사용해보니 여러 불편한 점이 많았고 그 중 JSP는 서블릿보단 HTML을 만들어내기가 쉽지만 담당하고 있는게 너무 많아져버린다. 화면과 비즈니스 로직을 전부 담당하고 나니 지저분해지고 보기가 힘들어진다. 이는 곧 유지보수가 어려워진다.
그래서 화면은 딱 화면을 담당하는 쪽에서만, 비즈니스 로직은 비즈니스 로직을 담당하는 쪽에서만 관리하고 처리하게 하고 싶은것이다.
그리고 또 하나는 둘 간의 변경 사이클이 다를 확률이 높다. 무슨 말이냐면 화면에 보이는 버튼의 위치를 바꾸고 싶다는 요구사항이 생길 때 비즈니스 로직을 건들 필요가 없다. 반대로 비즈니스 로직을 해결한 기술을 바꾸고 싶을 때 화면을 구성하는 어떤 부분도 변경할 필요가 없다. 근데 두 코드가 같은 파일에 있다는 것은 유지보수하기 좋지 않다.
그래서 MVC 패턴이 등장한다. Model, View, Controller로 영역을 나눈것이다.
- 컨트롤러: HTTP 요청을 받아서 파라미터를 검증하고, 비즈니스 로직을 실행한다(호출한다). 그리고 뷰에 전달할 결과 데이터를 조회해서 모델에 담는다.
- 모델: 뷰에 출력할 데이터를 담아둔다. 뷰가 필요한 데이터를 모두 모델에 담아서 전달해주는 덕분에 뷰는 비즈니스 로직이나 데이터 접근을 몰라도 되고, 화면을 렌더링 하는 일에 집중할 수 있다.
- 뷰: 모델에 담겨있는 데이터를 사용해서 화면을 그리는 일에 집중한다. 여기서는 HTML을 생성하는 부분을 말한다.
참고: 컨트롤러가 비즈니스 로직을 실행할 수도 있다. 근데 사이즈가 조금만 크다면 실행하지 말고 호출해라. 비즈니스 로직을 수행하는 부분을 컨트롤러로부터 떼어내는 것이다. 일반적으로 잘 알려진 '서비스'라는 레이어로 말이다.
그래서, 지금부터 할 내용은 작성한 JSP 파일에서 비즈니스 로직과 뷰 부분을 떼어낼 것이다. 서블릿을 컨트롤러로 사용하고 JSP를 뷰로 사용해서 MVC 패턴을 적용해보자.
서블릿을 컨트롤러로, JSP를 뷰로
우선, 서블릿 하나를 만들자. 멤버를 생성하는 폼에 대한 컨트롤러이다.
경로는 /path/your/package/web/servletmvc로 만들었다.
MvcMemberFormServlet
package org.example.servlet.web.servletmvc;
import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.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 req, HttpServletResponse resp) throws ServletException, IOException {
String viewPath = "/WEB-INF/views/new-form.jsp";
RequestDispatcher dispatcher = req.getRequestDispatcher(viewPath);
dispatcher.forward(req, resp);
}
}
여기서 보면, 화면으로 뿌려줄 JSP파일의 경로를 작성해서 RequestDispatcher 객체로 만들었다. 이 객체는 서버로 이 요청이 들어오면 forward()를 통해 JSP파일로 요청을 전달해버리는 기능을 가진다. 리다이렉트와 유사한것 같지만 리다이렉트는 클라이언트와 통신을 두번한다.
최초의 요청 -> 서버는 리다이렉트 경로를 알려주는 정보를 가지고 응답 -> 응답 데이터에 있는 리다이렉트 확인 -> 다시 해당 정보로 서버에 요청 -> 서버가 응답.
이게 리다이렉트라면 이 forward()는 요청이 들어와서 서버 내부에서 호출을 해서 최종 결과를 클라이언트에게 전달해준다. 그래서 클라이언트와 서버 간 통신은 한번뿐이다.
그리고 WEB-INF는 뭐냐면 기존에는 webapp안에 jsp/members/new-form.jsp 이렇게 경로를 지정해서 JSP 파일을 만들었다. 그리고 이 경로 그대로 URL에 입력하면 JSP 파일이 딱 브라우저에 뜬다. 근데 그걸 못하게 하는 것이다. WEB-INF 내부에 있는 자원들은 외부에서 직접적으로 접근하지 못하고 항상 컨트롤러를 통해 호출된다.
src/main/resources/WEB-INF/views/new-form.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<form action="save" method="post">
username: <input type="text" name="username" />
age: <input type="text" name="age" />
<button type="submit">전송</button>
</form>
</body>
</html>
여기서는 action의 경로를 유심히 봐야한다. 단순히 "save"가 끝이다. 즉, 상대경로로 POST 요청이 날라가게 설정했다. 이렇게 해두면 현재 URL에 뒤에 "/save"가 붙은 경로로 보낸다.
예를 들어 이 new-form.jsp를 보여주기 위한 URL은 "http://localhost:8080/servlet-mvc/members/new-form"이다. 그럼 이제 전송 버튼을 클릭하면 경로가 "http://localhost:8080/servlet-mvc/members/save"인 곳으로 POST 요청이 날라간다.
그럼 이제 멤버를 저장할 서블릿과 JSP를 만들어야한다.
MvcMemberSaveServlet
package org.example.servlet.web.servletmvc;
import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.example.servlet.domain.member.Member;
import org.example.servlet.domain.member.MemberRepository;
import java.io.IOException;
@WebServlet(name = "mvcMemberSaveServlet", urlPatterns = "/servlet-mvc/members/save")
public class MvcMemberSaveServlet extends HttpServlet {
private final 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.setAttribute("member", member);
String viewPath = "/WEB-INF/views/save-result.jsp";
RequestDispatcher dispatcher = req.getRequestDispatcher(viewPath);
dispatcher.forward(req, resp);
}
}
여기서 중요한 건 모델을 만들어서 뷰에게 전달해줘야한다. 그 부분이 바로 req.setAttribute("member", member);이다.
그 다음은 똑같이 forward()를 통해 JSP 파일을 호출한다.
src/main/resources/WEB-INF/views/save-result.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</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 파일을 작성하자.
MvcMemberListServlet
package org.example.servlet.web.servletmvc;
import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.example.servlet.domain.member.Member;
import org.example.servlet.domain.member.MemberRepository;
import java.io.IOException;
import java.util.List;
@WebServlet(name = "mvcMemberListServlet", urlPatterns = "/servlet-mvc/members")
public class MvcMemberListServlet extends HttpServlet {
private final MemberRepository memberRepository = MemberRepository.getInstance();
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
List<Member> members = memberRepository.findAll();
// Model에 보관
req.setAttribute("members", members);
String viewPath = "/WEB-INF/views/members.jsp";
RequestDispatcher dispatcher = req.getRequestDispatcher(viewPath);
dispatcher.forward(req, resp);
}
}
src/main/resources/WEB-INF/views/members.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<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>
JSP에서는 여러 데이터를 하나씩 뽑아서 뿌려주는 방법이 있는데 그 중 하나가 이 녀석을 사용하는 것이다.
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
근데 이거 진짜 그냥 이런게 있구나하고 넘어가도 된다. 필요하면 찾아보면 되는거고 JSP 쓸 일 거의 없다. 이렇게 하고 리스트를 보면 잘 나온다.
결론
이렇게 서블릿과 JSP를 사용해서 한 곳에서 작성되던 뷰와 비즈니스 로직을 쪼개서 각자가 자기것만 잘 담당할 수 있도록 해봤다. 그러다보니 코드가 이전보다 깔끔해졌다. 그러나 여전히 100% 만족스럽지 않다. 왜냐하면 서블릿에서 지금 중복 코드가 계속 반복되고 있기 때문이다. 다음 코드를 보자.
String viewPath = "/WEB-INF/views/new-form.jsp";
RequestDispatcher dispatcher = req.getRequestDispatcher(viewPath);
dispatcher.forward(req, resp);
String viewPath = "/WEB-INF/views/members.jsp";
RequestDispatcher dispatcher = req.getRequestDispatcher(viewPath);
dispatcher.forward(req, resp);
String viewPath = "/WEB-INF/views/save-result.jsp";
RequestDispatcher dispatcher = req.getRequestDispatcher(viewPath);
dispatcher.forward(req, resp);
viewPath의 실제 경로 말고 모든게 똑같다. 이런 공통적으로 처리될 부분들이 계속해서 중복되고 있다. 이걸 처리하는 유틸성 메서드를 만들면 조금 더 나아지겠지만 그것을 매번 호출하는 것 역시 중복이다. 그래서 이것을 더 개선하고 싶어진다.
공통된 부분들은 앞에서 미리 다 처리하고 들어오는 방식으로 말이다. 수문장 하나가 맨 앞에서 모든 요청을 받고 그 요청마다 공통적으로 처리되는 부분들을 거기서 전부 해결하고난 후 각각의 컨트롤러로 요청을 전달해주는 것이다. 이런 패턴을 Front Controller 패턴이라고 하고 스프링 MVC도 이 패턴을 잘 구현한 방식이다.
한번 직접 이 Front Controller를 만들고 MVC 패턴을 구현해보자.
'Spring MVC' 카테고리의 다른 글
Spring MVC 구조 이해하기 2 - 스프링 MVC 전체구조 (0) | 2024.07.07 |
---|---|
Spring MVC 구조 이해하기 1 - Front Controller (0) | 2024.05.07 |
Spring MVC없이 Servlet, JSP로 MVC 구현해보기 - 2 (0) | 2024.04.26 |
Spring MVC없이 Servlet, JSP로 MVC 구현해보기 - 1 (0) | 2024.04.25 |
자바의 백엔드 웹 기술 역사 (0) | 2024.04.25 |