이 글은 강의 : 김영한님의 - "[스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술]"을 듣고 정리한 내용입니다. 😁😁
이번 시간에는 서블릿, JSP, MVC 패턴에 대해서 공부할 것이다.
🎈 간단한 회원관리 웹 애플리케이션을 만들면서 학습을 해보자.
회원은 이름(username), 나이(asge)정도의 정보를 가진 도메인 객체를 만들고 회원 정보를 저장하고 조회하는 기능을 구현해보자.
🎃 회원 정보(도메인)
🎃 기능 요구사항
package hello.servlet.domain.member;
import lombok.Getter;
import lombok.Setter;
@Getter @Setter
public class Member {
private Long id;
private String username;
private int age;
public Member() {
}
public Member(String username, int age) {
this.username = username;
this.age = age;
}
}
package hello.servlet.domain.member;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 동시성 문제가 고려되지 않고 있으며, 실무에서는 ConcurrentHashMap, AtomicLong 사용을 고려하자.
*/
public class MemberRepository {
private static Map<Long, Member> store = new HashMap<>();
private static long sequence = 0L;
private static final MemberRepository instance = new MemberRepository();
public static MemberRepository getInstance() {
return instance;
}
//Singleton 에서 생성자를 호출하지 못하도록 private 으로 제한
private MemberRepository() {}
public Member save(Member member) {
member.setId(++sequence);
store.put(member.getId(), member);
return member;
}
public Member findById(Long id) {
return store.get(id);
}
public List<Member> findAll() {
return new ArrayList<>(store.values());
}
public void clearStore() {
store.clear();
}
}
참고 : 도메인 테스트 케이스를 작성하고자 할 때 CTRL + SHIFT + T를 통해 단축키로 빠르게 테스트 클래스를 생성할 수 있다.
package hello.servlet.domain.member;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
class MemberRepositoryTest {
private MemberRepository memberRepository = MemberRepository.getInstance();
@AfterEach
void afterEach() {
memberRepository.clearStore();
}
@Test
void save() {
//given
Member member = new Member("catsbi", 20);
//when
Member savedMember = memberRepository.save(member);
//then
Member foundMember = memberRepository.findById(savedMember.getId());
assertThat(savedMember).isEqualTo(foundMember);
}
@Test
void findById() {
//given
Member savedMember = memberRepository.save(new Member("catsbi", 20));
//when
Member foundMember = memberRepository.findById(savedMember.getId());
//then
assertThat(foundMember).isEqualTo(savedMember);
}
@Test
void findAll() {
//given
Member savedMember1 = memberRepository.save(new Member("catsbi", 20));
Member savedMember2 = memberRepository.save(new Member("crong", 30));
//when
List<Member> members = memberRepository.findAll();
//then
assertThat(members).hasSize(2);
assertThat(members).contains(savedMember1, savedMember2);
}
}
먼저 서블릿으로 만들어보자.
🎈 회원을 새로 등록하고자 할 때 이름과 나이를 받는 HTML Form 양식을 반환해 주는 서블릿이다.
package hello.servlet.web.servlet;
import hello.servlet.domain.member.MemberRepository;
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;
import java.io.PrintWriter;
@WebServlet(name = "memberFormServlet", urlPatterns = "/servlet/members/new-form")
public class MemberFormServlet 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");
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/members/save
경로로 POST HTML Form방식으로 전달한다.POST Form 방식으로 전달된 회원 정보를 받아서 회원 객체(Member)를 만들어 저장소에 저장 후 응답 페이지(HTML)를 작성해 반환해주느 서블릿
package hello.servlet.web.servlet;
import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
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;
import java.io.PrintWriter;
@WebServlet(name = "memberSaveServlet", urlPatterns = "/servlet/members/save")
public class MemberSaveServlet 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);
Member savedMember = memberRepository.save(member);
response.setContentType("text/html");
response.setCharacterEncoding("utf-8");
PrintWriter w = response.getWriter();
w.write("<html>\n" +
"<head>\n" +
" <meta charset=\"UTF-8\">\n" + "</head>\n" +
"<body>\n" +
"성공\n" +
"<ul>\n" +
" <li>id="+member.getId()+"</li>\n" +
" <li>username="+member.getUsername()+"</li>\n" +
" <li>age="+member.getAge()+"</li>\n" + "</ul>\n" +
"<a href=\"/index.html\">메인</a>\n" + "</body>\n" +
"</html>");
}
}
🎈 저장되어 있는 회원 목록을 모두 보여주는 HTML 응답 메세지를 작성해 반환해주는 서블릿
package hello.servlet.web.servlet;
import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
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;
import java.io.PrintWriter;
import java.util.List;
@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 {
List<Member> members = memberRepository.findAll();
response.setContentType("text/html");
response.setCharacterEncoding("utf-8");
PrintWriter w = response.getWriter();
w.write("<!DOCTYPE html>");
w.write("<html lang=\"en\">");
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("<ul>");
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(" ");
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("</ul>");
w.write("</body>");
w.write("</html>");
}
}
🧨 실행
http://localhost:8080/servlet/members
지금 간단한 예제를 진행하면서도 HTML 코드를 서블릿과 자바 코드만으로 작성을 해줬다.
자바코드 내에서 작성되기 때문에 자바 문법을 통한 동적 생성도 가능하다.
하지만, 이 실습을 실제로 복사 붙혀넣기가 아니라 작성해보면서 따라왔다면 엄청나게 불편하고 오타가나서 에러가 발생할 확률이 높다는 점을 알 수 있다.
그래서 자바코드에서 HTML 코드를 작성하는 것이 아닌 HTML코드에서 자바를 사용하는게 훨씬 편리할 수 있다.
그래서 나온게 템플릿 엔진 이다. 이 템플릿 엔진을 사용하면 HTML 필요한 곳만 코드를 적용해 동적으로 변경이 가능하다.
대표적으로 JSP, Thymeleaf, Freemarker, Velocity 와 같은 템플릿 엔진이 있는데, 우선 JSP으로 지금까지 한 로직을 똑같이 구현해볼 것이다.
지금까지 진행했고, 앞으로 진행할 예제들을 빠르게 찾아갈 수 있게 모든 경로를 작성한 해당 페이지로 변경하자.
🎈 index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<ul>
<li><a href="basic.html">서블릿 basic</a></li>
<li>서블릿
<ul>
<li><a href="/servlet/members/new-form">회원가입</a></li>
<li><a href="/servlet/members">회원목록</a></li>
</ul>
</li>
<li>JSP
<ul>
<li><a href="/jsp/members/new-form.jsp">회원가입</a></li>
<li><a href="/jsp/members.jsp">회원목록</a></li>
</ul>
</li>
<li>서블릿 MVC
<ul>
<li><a href="/servlet-mvc/members/new-form">회원가입</a></li>
<li><a href="/servlet-mvc/members">회원목록</a></li>
</ul>
</li>
<li>FrontController - v1
<ul>
<li><a href="/front-controller/v1/members/new-form">회원가입</a></li>
<li><a href="/front-controller/v1/members">회원목록</a></li>
</ul>
</li>
<li>FrontController - v2
<ul>
<li><a href="/front-controller/v2/members/new-form">회원가입</a></li>
<li><a href="/front-controller/v2/members">회원목록</a></li>
</ul>
</li>
<li>FrontController - v3
<ul>
<li><a href="/front-controller/v3/members/new-form">회원가입</a></li>
<li><a href="/front-controller/v3/members">회원목록</a></li>
</ul>
</li>
<li>FrontController - v4
<ul>
<li><a href="/front-controller/v4/members/new-form">회원가입</a></li>
<li><a href="/front-controller/v4/members">회원목록</a></li>
</ul>
</li>
<li>FrontController - v5 - v3
<ul>
<li><a href="/front-controller/v5/v3/members/new-form">회원가입</a></li>
<li><a href="/front-controller/v5/v3/members">회원목록</a></li>
</ul>
</li>
<li>FrontController - v5 - v4
<ul>
<li><a href="/front-controller/v5/v4/members/new-form">회원가입</a></li>
<li><a href="/front-controller/v5/v4/members">회원목록</a></li>
</ul>
</li>
<li>SpringMVC - v1
<ul>
<li><a href="/springmvc/v1/members/new-form">회원가입</a></li>
<li><a href="/springmvc/v1/members">회원목록</a></li>
</ul>
</li>
<li>SpringMVC - v2
<ul>
<li><a href="/springmvc/v2/members/new-form">회원가입</a></li>
<li><a href="/springmvc/v2/members">회원목록</a></li>
</ul>
</li>
<li>SpringMVC - v3
<ul>
<li><a href="/springmvc/v3/members/new-form">회원가입</a></li>
<li><a href="/springmvc/v3/members">회원목록</a></li>
</ul>
</li>
</ul>
</body>
</html>
위에서 서블릿과 자바만 가지고 만들었던 회원 관리 웹 애플리케이션에서 HTML 코드를 자바로 작성하기 때문에 생긴 불편함을 해결하기 위해 템플릿 엔진을 사용한다고 했다. 이번에는 템플릿 엔진 중에서 JSP를 사용해서 지금까지 구현한 회원 관리 웹 애플리케이션을 구현해보자.
JSP를 사용하기 위해선 우선 build.gradle
에 라이브러리를 추가해야 한다.
//JSP 추가 시작
implementation 'org.apache.tomcat.embed:tomcat-embed-jasper'
implementation 'javax.servlet:jstl'
//JSP 추가 끝
생성 경로 : main/webapp/jsp/members/new-form.jsp
<%@ 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>
⇒ <%@ page contentType="text/html;charset=UTF-8" language="java" %>
JSP 사용시 첫 줄에 추가해야하는 코드로 이 문서가 JSP라는 의미다.
🎃 실행
http://localhost:8080/jsp/members/new-form.jsp
실행시 .jsp까지 함께 적어 주어야 한다.
생성 경로 : main/webapp/jsp/members/save.jsp
<%@ page import="hello.servlet.domain.member.Member" %>
<%@ page import="hello.servlet.domain.member.MemberRepository" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
//request, response 사용 가능
MemberRepository memberRepository = MemberRepository.getInstance();
String username = request.getParameter("username");
int age = Integer.parseInt(request.getParameter("age"));
Member member = new Member(username, age);
Member savedMember = memberRepository.save(member);
response.setContentType("text/html");
response.setCharacterEncoding("utf-8");
%>
<html>
<head>
<title>Title</title>
</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>
<%@ page import ...%>
: 자바의 import 문 <% ~~ %>
: 자바 코드를 입력하는 블럭<%= ~~ %>
: 자바 코드 출력 블럭생성 경로 : main/webapp/jsp/members.jsp
<%@ page import="hello.servlet.domain.member.MemberRepository" %>
<%@ page import="hello.servlet.domain.member.Member" %>
<%@ page import="java.util.List" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
MemberRepository memberRepository = MemberRepository.getInstance();
List<Member> members = memberRepository.findAll();
%>
<html>
<head>
<title>Title</title>
</head>
<body>
<table>
<thead>
<th>id</th>
<th>username</th>
<th>age</th>
</thead>
<tbody>
<%
for (Member member : members) {
out.write("<tr>");
out.write("<td>" + member.getId() + "</td>");
out.write("<td>" + member.getUsername() + "</td>");
out.write("<td>" + member.getAge() + "</td>");
out.write("</tr>");
}
%>
</tbody>
</table>
</body>
</html>
🎉 서블릿과 자바코드만으로 HTML 코드를 작성할 때보다는 확실히 오류 발생 확률이나 작성 난이도가 낮아졌다.
하지만 다음과 같은 문제를 직면하게 된다.
드디어 MVC에 대해서 공부해볼 것이다.
도메인 영역, 뷰 영역, 컨트롤러 영역 등 각각의 영역에서 자신의 역할만 담당하도록 분리하는 방법
🎈 비즈니스 로직은 서블릿 처럼 다른곳에서 처리하고, JSP는 목적에 맞게 HTML로 화면(View)을 그리는 일에 집중하도록 하자. 과거 개발자들도 모두 비슷한 고민이 있었고, 그래서 MVC 패턴이 등장했다. 우리도 직접 MVC 패턴을 적용해서 프로젝트를 리팩터링 해보자
이미 얘기했지만 하나의 서블릿이나 JSP만으로 비즈니스 로직과 뷰 렌더링까지 모두 처리하게 되면 하나의 영역에서 너무 많은 역할을 부담지게 된다. 이는 필연적으로 유지보수가 어려워지는 결과를 낳는다.
비즈니스 로직에 문제가 생겨도, UI를 변경해야해도 두 로직이 같이 있는 파일을 수정해야 하는데, 매번 수정할때마다 소스코드가 수백 수천줄이 있는데 Java코드를 수정해야하는데 그 사이에 HTML 코드가 수천줄이 있거나 반대의 경우가 있다고 하면 수정해야 할 코드를 찾는것만해도 비용소모가 커질 것이다.
사실 이게 정말 중요한데, 진짜 문제는 둘 사이에 변경의 라이프 사이클이 다르다는 점이다. 예를 들어서 UI 를 일부 수정하는 일과 비즈니스 로직을 수정하는 일은 각각 다르게 발생할 가능성이 매우 높고 대부분 서로에게 영향을 주지 않는다. 이렇게 변경의 라이프 사이클이 다른 부분을 하나의 코드로 관리하는 것은 유지보수하기 좋지 않다. (물론 UI가 많이 변하면 함께 변경될 가능성도 있다.)
🎈 특히 JSP 같은 뷰 템플릿은 화면을 렌더링 하는데 최적화 되어 있기 때문에 이 부분의 업무만 담당하는 것이 가장 효과적이다.
MVC 패턴은 지금까지 학습한 서블릿, JSP 영역에서 처리되던 것을 컨트롤러(Controller)와 뷰(View)라는 영역으로 서로 영역을 나눈 것을 말한다. 웹 애플리케이션은 보통 이 MVC 패턴을 사용한다.
🎃 HTTP 요청을 받아 파라미터를 검증하고 비즈니스 로직을 실행한다. 그리고 뷰에 전달할 결과 데이터를 조회에 모델에 담는다.
🎃 뷰에 출력할 데이터를 담아둔다. 뷰가 필요한 데이터를 모두 모델에 담아 전달해주기에 뷰는 비즈니스 로직이나 데이터 접근에 대해 알 필요가 없고 화면 렌더링에만 집중하면 된다.
🎃 모델에 담겨있는 데이터를 사용해 화면을 그리는 일에 집중한다. 여기서는 HTML을 생성하는 부분을 말한다.
지금까지 이러던게
MVC 패턴1
MVC 패턴2
MVC 패턴에 대해서 개념을 알아봤다. 하나의 영역(이전 시간에는 JSP)에 여러 영역의 코드들을 모아놨었는데 이를 MVC 패턴을 사용해서 분리를 해 볼 것이다.
우선, 서블릿은 컨트롤러로 사용하고, JSP는 뷰만 담당하도록 MVC패턴을 적용해보자.
그리고 Model은 HttpServletRequest에서 attribute를 이용해 데이터를 보관 및 조회할 것이다.
회원 등록 페이지 서블릿 구현
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가 있으면 외부(웹 브라우저...)에서 직접 JSP를 호출할 수 없다. 즉 위에서 처럼 경로를 직접 지정하며 JSP를 호출해도 호출할 수 없다는 의미. 이 경로 내에 있는 VIEW 파일은 컨트롤러를 통해서만 호출 가능하다.
리다이렉트는 실제 클라이언트(웹)에 응답이 나갔다가, 클라이언트가 redirect경로로 다시 요청한다. 그렇기에 클라이언트가 인지할 수 있고, URL 경로도 실제로 변경된다.
반면, forward의 경우 서버 내부에서 일어나는 호출이기 때문에 클라이언트는 인지할 수 없다.
회원 등록 JSP 페이지 구현
생성 경로 : main/webapp/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"
폼에서 이렇게 써놨는데, 지금까지 학습했을 때 경로 앞에 / 슬래쉬를 붙인 것과 다르게 슬래쉬를 쓰지 않았는데, 이것은 절대경로가 아닌 상대경로로 시작한다는 것을 의미한다.
이렇게 상대 경로로 폼을 전송하면 현재 URL이 속한 계층 경로 + 작성한 상대경로로 호출된다.
현재 계층 경로 : /servlet-mvc/members/
결과 : /servlet-mvc/members/save
회원 저장 - 컨트롤러 구현
package hello.servlet.web.servletmvc;
import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
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 = "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);
Member savedMember = memberRepository.save(member);
//Model에 데이터 보관
request.setAttribute("member", member);
String viewPath = "/WEB-INF/views/save-result.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
}
}
🎈 request.setAttribute("member", member)
⇒ HttpServletRequest를 Model로 사용한다.
⇒ request가 제공하는 setAttribute() 메서드를 사용해 request객체에 데이터를 보관해서 뷰에 전달할 수 있다.
⇒ 뷰에서는 request.getAttribute()를 사용해 저장한 데이터를 꺼내 조회할 수 있다.
생성 경로 : main/webapp/WEB-INF/views/save-result.jsp
<%@ page import="hello.servlet.domain.member.Member" %>
<%@ 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>
🎃 ${member.id}
⇒ 원래 <%=request.getAttribute("member")%>를 사용해서 모델에 저장한 member 객체를 꺼낼 수있지만 이를 실제 사용하려면 형변환을 한 뒤 그 객체에서 필요한정보를 getter를 통해 꺼내는 작업을 해야해서 많이 번거로워진다.
⇒ 그래서 JSP는 ${} 문법을 제공하는데, 이를 사용하면 request의 attribute에 담긴 데이터를 프로퍼티조회할 수 있다. (member.getId() - X)(member.id - O)
MVC 덕분에 컨트롤러 로직과 뷰 로직을 확실하게 분리한 것을 확인할 수 있다. 향후 화면에 수정이 발생하면 뷰 로직만 변경하면 된다
회원 목록을 조회
package hello.servlet.web.servletmvc;
import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
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;
import java.util.List;
@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 dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
}
}
생성 경로 : main/webapp/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를 학습하는 것이 이 강의의 주 목적이 아니다.
MVC패턴을 적용한 덕분에 컨트롤러의 역할과 뷰를 렌더링하는 역할을 명확하게 구분할 수 있었다.
이처럼 MVC패턴을 사용함으로써 JSP에 부과되는 수많은 영역들의 코드들을 분리할 수 있었기에, 컨트롤러, 모델, 뷰가 명확하게 구분되었다. 그럼으로써 JSP에는 화면을 그리는 코드를 제외하고는 다 걷어낼 수 있었다. 하지만 이런 MVC 패턴을 적용함에도 문제는 남아있다.
String viewPath = "viewPath";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
🎈 View로 이동하는 코드가 항상 중복 호출되고 있다. 그럼 이 코드를 모듈화해서 공통화하면 해결될까 생각하지만, 그 모듈화된 메서드도 항상 직접 호출을 해야하면서 중복이 발생한다.
String viewPath = "/WEB-INF/views/new-form.jsp";
회원 관리 웹 애플리케이션을 MVC 패턴을 적용해서 구현하면서 작성한 ViewPath만 최소 3번인데, 중복되는 부분을 봤을 것이다. 내가 호출하고자 하는 jsp파일이 위치한 상위 디렉토리 경로와 .jsp라는 접미사다.
prefix : /WEB-INF/views
suffix : .jsp
그리고 만약 jsp가 아닌 thymeleaf 같은 다른 뷰로 변경한다면 전체 코드를 다 변경해야 한다.
모든 서블릿에서 공통적으로 받는 매개변수와 forward를 통해 전달하는 인자값으로 request, response가 있다. 근데 이 중에서 response는 이번 예제에서는 사용하지 않는 경우가 많았다.
HttpServletRequest request, HttpServletResponse response
그리고 이런 HttpServletRequest request, HttpServletResponse response
를 사용하는 코드는 테스트 케이스 작성하기도 어렵다.
기능이 복잡할수록 컨트롤러에서 공통으로 처리해야하는 부분이 증가할 것인데, 이를 모듈화 하는 비용도 있고, 이 모듈을 항상 호출하는데 신경써야하며 호출을 하지 않았을때 생기는 문제도 있다.
여러 문제들이 있지만, 결국 이 문제들 또한 종합하자면 공통 처리에 대해서 어려움이 있다는 부분이다. ViewPath부터 request, response객체 forward코드까지 중복되는게 많다.
그래서 이러한 문제를 해결하기 위해서는 컨트롤러가 호출되기 전에 공통적으로 처리될 수 있는 부분은 처리되어야 한다. 그리고 이 문제의 해결책은 프론트 컨트롤러(Front Controller) 패턴을 사용해서 해결할 수 있는데, 이 스프링 MVC의 핵심이 프론트 컨트롤러에 있다.