이전 JSP와 서블릿에서 구현하던 경우, 뷰와 컨트롤러가 하나의 코드, 서블릿 또는 JSP에 결합되어 있어
둘 중 하나를 수정하려 할때 다른 곳의 코드까지 변형하는 실수를 범하거나 한 서블릿 또는 JSP에 너무 많은 책임이 부여되어있다는 단점이 있었다.
스프링 MVC는 이런 책임의 가중을 분산(분리)를 통해 해결한다.
MVC패턴에서 MVC는 모델 / 뷰 / 컨트롤러 의 약자이며 웹 애플리케이션은 보통 이런 MVC 패턴을 이용한다
컨트롤러: 요청을 받아서 파라미터를 검증하고, 비즈니스 로직을 실행하는 역할을 맡는다
그리고 뷰에 전달할 결과 데이터를 조회해서 모델에 담는다.
-> 주소를 검증하고, 호출하는 것에 집중한다.
세부적으로 컨트롤러에 비즈니스 로직을 함께 구현하는 경우가 있으나, 이렇게 되면 컨트롤러에 책임이 가중되기 때문에
서비스 계층을 따로 만들어서 비즈니스 로직을 처리하도록 한다.
모델 : 뷰에 출력할 데이터를 담아둔다. / 뷰 로직의 의존관계를 다 끊어놓을 수 있다
-> 각자 책임에 충실하게 만들어줌
뷰: 모델에 담겨있는 데이터를 사용해서 화면을 렌더링하는 일에 집중함, 동적 HTML 생성하는 부분을 말한다.
뷰는 어떤 데이터를 찾던 전부 모델에서 찾는다
뷰 로직을 거쳐서 response 메세지가 나간다
MVC 패턴 - 적용
서블릿을 컨트롤러로 사용하고 JSP를 뷰로 사용해서 MVC 패턴을 적용해보겠다.
단, 이전에 JSP에서 자바 로직을 처리했고, 서블릿에서 getWriter를 통해 HTML 문서가 생성된 것과 달리
지금부터는 JSP를 명확하게 뷰로 사용하고 서블릿을 명확하게 컨트롤러로 사용해보겠다.
package hello.servlet.web.servletmvc;
import java.io.IOException;
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;
@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 {
// 1. MemberForm을 보여준다, 일단 요청이 들어와야 함.
// 얘는 할 일 없음 바로 JSP로 보내버릴거임
String viewPath = "/WEB-INF/views/new-form.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
// 컨트롤러에서 뷰로 이동할때 사용하는게 getRequestDispatcher
dispatcher.forward(request, response);
// forward -> 서블릿에서 JSP를 호출할 수 있다! JSP를 찾아서 넘어가서 호출됨
// 컨트롤러 호출시 -> viewPath로 제어권 넘겨주는 역할.
}
}
MemberForm의 컨트롤러 역할을 하는 MvcMemberFormServlet을 생성했다.
url로 /new-form 요청이 들어오게 되면 설정된 viewPath 경로에 존재하는 뷰로 dispatcher.forward에 의해 이동한다.
컨트롤러에서 뷰로 이동할때 사용하는 것이 getRequestDispatcher 메서드이다
.getRequestDispatcher의 forward() 메서드는 다른 서블릿이나 JSP로 이동할 수 있는 기능인데
이 다른 서블릿이나 JSP로의 이동은 HTTP 코드 302 즉 Redirection과는 거리가 있다.
Redirection의 경우 클라이언트 응답에 302 코드가 포함되어 Response가 나간 이후,
클라이언트에 의해 다시 리다이렉트 된 곳으로 요청이 들어온다.
그러나 지금 forward()의 경우 서버 내에서 일어나는 과정으로 서버 내부에서 다시 호출이 일어나기 때문에
응답 메세지가 클라이언트로 나가지 않는다, 따라서 클라이언트는 서버 내부에서 다른 뷰나 서블릿으로 이동이 일어났다는 사실을 인지하지 못한다.
P.S.) 추가로, webapp내에 /WEB-INF/ 폴더를 설정하는 경우 일반적인 요청으로는 해당 리소스에 접근하지 못한다.
오로지 컨트롤러에 의해 호출할 수 있다.
회원 등록 폼 - 뷰
<%--
Created by IntelliJ IDEA.
User: yunsik
Date: 2023/05/21
Time: 6:21 PM
To change this template use File | Settings | File Templates.
--%>
<%@ 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>
form의 action 속성을 보면 절대경로가 아니라 상대경로로 경로가 지정되어있음을 알 수 있다.
이렇게 상대 경로를 사용하면 폼 전송시 현재 url이 속한 계층경로 + save가 호출된다.
일반적으로 상대경로보다 절대경로를 많이 사용하나, 현재 실습에서는 이 파일들을 추후 MVC 패턴 개선하는 과정에서
컨트롤러들마다 재사용해야하기 때문에 실습 용이성을 위해 상대경로로 지정한다.
package hello.servlet.web.servletmvc;
import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
import java.io.IOException;
import java.io.PrintWriter;
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;
@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);
memberRepository.save(member);
// Model에 데이터를 보관한다.
request.setAttribute("member", member);
// setAttribute 내에 Map 존재하는데 거기에 들어가게 됨.
String viewPath = "/WEB-INF/views/save-result.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request,response);
}
}
멤버 저장 REST API /save를 호출할때 호출되는 MvcMemberSaveServlet의 구현은 다음과 같다.
먼저 이전에 작성한 것 처럼, POST FORM에 의해 전달된 Parameter의 조회를 위해 getParameter 메서드를 사용한다.
이후 MemberRepository 클래스의 save 메서드를 통해 멤버 정보를 저장한다.
그리고 model을 사용하기 위해 request가 제공하는 setAttribute() 메서드를 사용하게 되는데
이를 사용하면 request 객체 내에 데이터를 보관해서 뷰에 전달 가능하게 된다.
뷰는 request.getAttribute()를 통해 데이터를 꺼내서 동적 HTML을 생성할 수 있다.
회원 저장 - 뷰
<%--
Created by IntelliJ IDEA.
User: yunsik
Date: 2023/05/21
Time: 6:31 PM
To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
성공
<ul> <!-- 프로퍼티 접근법, getId, getUsername, getAge 자동호출 -->
<li>id=${member.id}</li>
<li>username=${member.username}</li>
<li>age=${member.age}</li>
</ul>
<a href="/index.html">메인</a>
</body>
</html>
이전에 자바 문법을 적용했던 것 처럼 request.getAttribute() 를 통해 member 객체를 꺼내 사용할 수 있지만
자바 코드가 혼용되어 복잡해지는 문제점이 있다.
따라서 JSP는 ${} 문법을 제공하는데 이 문법을 사용하면 request의 attribute에 담긴 데이터의 조회가 가능하다.
이를 통해 완벽하게 컨트롤러와 뷰 로직을 분리했다.
따라서 컨트롤러 로직이 수정되면 컨트롤러를, 뷰 내용의 수정이 필요하면 jsp 파일만 건드리면 해결할 수 있다.
회원 목록 조회 - 컨트롤러
package hello.servlet.web.servletmvc;
import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;
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;
@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 requestDispatcher = request.getRequestDispatcher(viewPath);
requestDispatcher.forward(request, response);
}
}
위 MvcMemberSaveServlet에서 했던것 처럼 request.setAttribute()를 이용하여 List<Member> members 객체를 모델에 보관했다.
<%--
Created by IntelliJ IDEA.
User: yunsik
Date: 2023/05/21
Time: 6:37 PM
To change this template use File | Settings | File Templates.
--%>
<%@ 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>
모델에 담아둔 members를 jsp가 제공하는 taglib 기능을 이용하여 반복하면서 출력했다.
taglib 기능은 상단에 다음과 같은 코드를 추가함으로써 사용할 수 있다.
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
'스프링 공부 (인프런 김영한 선생님) > 스프링 MVC 1편' 카테고리의 다른 글
[스프링 웹 MVC 1편] 11. MVC 프레임워크 제작해보기 - v1 (0) | 2023.05.22 |
---|---|
[스프링 웹 MVC 1편] 10. MVC 패턴 - 한계 (0) | 2023.05.22 |
[스프링 웹 MVC 1편] 8. JSP (0) | 2023.05.21 |
[스프링 웹 MVC 1편] 7. 서블릿 구현 (0) | 2023.05.21 |
[스프링 웹 MVC 1편] 6. HTTPServletResponse - 기본 사용법 (0) | 2023.05.20 |