개발자로 후회없는 삶 살기
spring PART.JSP MVC 패턴, 한계점 본문
서론
※ 이 포스트는 다음 강의의 학습이 목표임을 밝힙니다.
https://www.inflearn.com/roadmaps/373
본론
- mvc 패턴 적용
이제 실제 mvc 패턴을 적용해보자 서블릿을 컨트롤러로 사용하고 jsp를 뷰로 사용할 것입니다. 모델은 httpservletrequest는 내부에 데이터 저장소가 있다고 했습니다. 이 저장소에 set해서 값을 저장하고 get으로 데이터를 저장할 수 있습니다. httpservletrequest req 객체를 모델처럼 쓰는 것입니다. 컨트롤러에서 set으로 저장하고 뷰에서 get으로 값을 꺼내는 것입니다.
-> 회원 등록
@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);
}
}
회원 등록부터 mvc로 개발해보겠습니다. 서블릿 mvc 패키지를 만들고 mvc form 서블릿을 만듭니다. 얘가 컨트롤러가 되는 것입니다. url은 new-form으로 합니다.
> 얘는 컨트롤러로 mvc 패턴을 쓰면 항상 컨트롤러를 거쳐서 뷰로 들어가야합니다. 컨트롤러로 요청이 항상들어와야합니다. 맨 처음에는 form을 보여줘야합니다. 그래서 얘는 할일이 없습니다. 그냥 jsp로 가주면 됩니다.
> jsp 경로를 나중에 만들 경로로 초기화하고 디스패쳐라고 있는데 얘가 컨트롤러에서 뷰로 이동할 때 사용하는 것입니다. 이 경로로 이동할 거야라는 것입니다. forward()를 호출하면 서블릿에서 jsp를 호출할 수 있습니다. 즉 고객이 요청이 오면 이 서비스가 호출될 것이고 얘가 jsp를 호출하는 것입니다. 말 그대로 컨트롤러는 조종만 하므로 jsp를 보여주는 것을 하면 되는데 처음에 form 화면을 보여줄 것이니 jsp를 호출하도록 합니다.
> web-inf는 /web-inf니깐 그냥 index.html과 같은 계층에 두면 됩니다. 조심할게 action을 save로 할 건데 이렇게 한 이유는 다른 대서도 이를 호출 할 것이라서 그렇습니다.
> 실행해보면 서블릿을 호출하면 new-form.jsp가 나옵니다. 제출을 하면 당연히 save를 만들지 않았으니 오류가 납니다. 근데 url 변하는 것을 보자 지금 form의 액션에 save가 있습니다. 절대 경로로 하면 /로 시작해서 localhost:8080/save로 포트뒤에 절대 경로가 붙습니다.
근데 상대 경로로 하면 지금 내 http://localhost:8080/servlet-mvc/members/new-form에서 마지막 만 http://localhost:8080/servlet-mvc/members/save로 바뀝니다. 다른 곳에서 재활용을 하기 위하면 상대 경로를 씁니다. 뒤에 여러 프로젝트에서 이 뷰를 재사용할 것입니다.
=> 컨트롤러 코드 설명
-> redirect vs forward의 차이
dispatcher.forward()를 쓰면 다른 서블릿이나 jsp로 이동할 수 있습니다. 서버 내부에서 다시 호출이 발생합니다. 다시 클라에게 응답을 갔다가 서버에 다시 요청하는 리다이렉이 아니라 서버에서 바로 다른 요청을 호출하는 것입니다. 그러면 컨트롤러 서버, 서비스 서버, jsp 서버를 둘 수 있겠습니다.
-> web-inf
WEB-INF는 이전에는 webapp의 jsp를 썼었습니다. 이건 url 엔터로 가능합니다. 근데 지금 뷰는 그냥 webapp의 jsp로 불려지고 싶지 않습니다. "컨트롤러를 거쳐서 불려지고 싶다!" 그래서 외부에서 직접적으로 url 엔터로 누르기 싫을 때 web-inf에 넣습니다. 반드시 컨트롤러를 거쳐야 불려지는 것입니다.
- 회원 저장
이제 이 상대 경로에다가 실제 저장되는 코드를 작성할 것입니다. save 서블렛을 만듭니다. 생각을 해보라 저장을 하면 컨트롤러는 뭐를 하고 로직은 무엇이고 모델은 무엇이고 뷰는 뭐를 할까? 보겠습니다.
@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);
req.setAttribute("member", member);
String viewPath = "/WEB-INF/views/save-result.jsp";
RequestDispatcher requestDispatcher = req.getRequestDispatcher(viewPath);
requestDispatcher.forward(req, resp);
}
}
url은 save입니다. 근데 이때 /servlet-mvc/members/save라고 해야하는 이유를 생각해보겠습니다. 이유는 처음에 index에서 mvc를 적용해서 회원 등록을 하려고 할 때 /servlet-mvc/members/new-form으로 했습니다. 그냥 이렇게 정한 것이었습니다. 지금은 상대 경로로 /servlet-mvc/members/save가 될 것이니 이렇게 합니다. 이렇게 헷갈리니깐 uri 설계를 잘해야하는 것입니다.
<%@ 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>
action을 누르면 save 컨트롤러가 호출됩니다. 역시나 레포가 필요합니다. 저장이 일어날 것입니다. 이전 서블릿 코드를 가져옵니다. 왜냐면 서블릿이 비즈니스 로직을 가지고 있었으니 그대로 가져오면 됩니다. 근데 이전에는 서블릿에서 로직도 하고 그 아래 writer로 뷰도 작성했습니다. 이제는 그렇게 하지 않습니다.
모델에 데이터를 보관해야합니다. 왜냐면 jsp에 데이터를 줄 것이기 때문에 모델에 set을 합니다. request 객체 저장소에 set으로 member를 저장합니다. 그리고 이제 jsp 로 넘어갈 것입니다. 그래서 또 dis패쳐를 만들고 forward를 합니다. 이번에 이동할 곳은 web-inf의 save-result.jsp입니다.
<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는 이전에 만든 jsp 코드를 가져옵니다. jsp에서 request.get으로 하면 됩니다. 근데 이를 쉽게 하는게 있습니다. $로 멤버.id라고 하면 모델에 담긴 키 값의 필드를 가져올 수 있습니다. 저장할 때는 set을 해야하지만 조회할 때는 get을 자동으로 해주는 $가 있습니다.
-> 코드 설명
save 서블릿을 보면 파라미터 받고 모델에 넣고 뷰로 던지고 등 컨트롤러 역할을 충실하게 하고 있습니다. 뷰를 봐도 혼재했던 jsp가 뷰 출력만 하게 됩니다. 뷰에 특화된 깔끔한 로직이 놔왔습니다.
-> 목록 서블릿 작성
@WebServlet(name = "mvcMemberListServlet", urlPatterns = "/servlet-mvc/members")
public class MvcMemberListServlet extends HttpServlet {
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 requestDispatcher = req.getRequestDispatcher(viewPath);
requestDispatcher.forward(req, resp);
}
}
호출은 members로 호출하도록 url을 강사님께서 임의로 정하셨습니다. 역시 레포가 필요하니 레포를 가져오고 findAll로 가져오고 모델에 담아야합니다. req.set에 All로 가져온 members를 가져옵니다. 이제 jsp에 또 던져야합니다. > 느껴지는게 뭔가 이를 반복하고 있는 것 같습니다. 이를 다음에 개선하게 되고 다 개선한게 스프링입니다.
-> list jsp 작성
<%@ 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>
for문으로 해야하는데 for each문으로 jsp가 제공합니다. 결과를 보면 자바 코드가 하나도 없습니다. 뷰에 특화된 jsp가 됩니다.
결과를 보면 잘 나옵니다. 지금 하는 게 잘 나오는 건 아까도 잘 나왔습니다. 근데 지금은 컨트롤러, 모델, 뷰로 특화되게 나눈게 중요한 것입니다.
-> 정리
클라에게 요청이오면 항상 컨트롤러를 거치고 뷰로 갑니다. 뷰로 바로 절대 안 갑니다. 컨트롤러에서 서비스나 회원 가입을 하면 서비스는 레포를 접근하고 그 결과를 컨트롤러에서 모델에 담아서 (req.set) 뷰는 req.get으로 모델에서 값을 꺼내서 뷰를 뿌렸습니다.
회원 등록 폼은 로직이 없는데 컨트롤러를 거쳤습니다. 무조건 컨트롤러를 거치고 뷰로 가게 해야합니다.
- 한계
지금한 mvc 패턴의 한계를 보자 분명 뷰는 깔끔하기는 합니다. 하지만 컨트롤러가 중복이 너무 많습니다. 알아보겠습니다.
1. 디스패쳐 forward가 반복
2. WEB-INF도 반복
prefix, suffix가 반복되어서 나중에 suffix가 타임리프로 바뀌면 전체 코드를 다 바꿔야합니다.
3. 사용하지 않는 코드
response가 이제는 응답 메세지를 만들기 보다 jsp로 뿌리기 때문에 사용이 되지 않습니다.
4. 공통 처리가 어렵다.
기능이 복잡해질 수록 공통처리할 요소가 많아집니다. ex) 로그출력 이 문제가 제일 크고 중요합니다. 이 문제를 해결하기 위해서는 컨트롤러 호출 전에 서블릿이 호출되기 전에 먼저 공통 기능을 처리해야합니다. 소위 수문장 역할이 필요합니다. 지금은 아무대나 다 들어오는 것입니다. 그게 아니고 수문장이 앞에 있어서 그 객체를 통해서 컨트롤러를 호출해야하고 그 객체에서 공통처리를 다 해버려야합니다.
이게 패턴이 되어서 프론트 컨트롤러 패턴으로 컨트롤러 중에서 수문장 역할을 하는 컨트롤러를 넣는 것입니다. 어떤 http 요청도 이 수문장을 거쳐서 들어오게 해야합니다. 공통적인 일을 이 수문장이 대신하게 하면 됩니다. 입구를 하나로 만드는 것입니다.
'[백엔드] > [spring | 학습기록]' 카테고리의 다른 글
spring PART.유연한 컨트롤러 (0) | 2023.04.12 |
---|---|
spring PART.MVC 프레임워크 만들기 (0) | 2023.04.11 |
spring PART.서블릿 jsp로 웹 어플리케이션 개발 (0) | 2023.04.10 |
spring PART.HTTP의 모든 것 1 (0) | 2023.04.07 |
spring PART.서블릿 프로그래밍 개요 (0) | 2023.04.07 |