Search
Duplicate

서블릿,JSP, MVC패턴

목차

회원 관리 웹 애플리케이션 요구사항

간단한 회원관리 웹 애플리케이션을 만들면서 학습을 해보자.
회원은 이름(username)과 나이(age) 정도의 정보를 가진 도메인객체를 만들고 회원 정보를 저장하고 조회하는 기능을 구현해보자.

요구사항

회원 정보(도메인)

이름: username
나이: age

기능 요구사항

회원 저장
회원 목록 조회

코드

1. Member - 회원 도메인 모델

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; } }
Java
복사

2. MemberRepository - 회원 도메인 저장소

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 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(); } }
Java
복사
싱글톤 패턴을 적용해여 서블릿 컨테이너에서 하나의 저장소만 동작한다.
싱글톤은 기본생성자의 접근제어자를 private로 하여 추가 생성을 막아준다.
시퀀스도 클래스 변수로 선언해 서블릿 컨테이너 내에서 하나만 관리되도록 한다.
저장소는 HashMap을 사용하며 sequance를 key로하여 회원 도메인을 value로 사용한다.

테스트 해보기 (TDD)

참고: 도메인 테스트 케이스를 작성하고자 할 때 cmd + shift + t 단축키로 빠르게 테스트 클래스를 생성할 수 있다. (윈도우는 ctrl + shfit + t)

1. MemberRepositoryTest

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); } }
Java
복사
회원을 저장하고 목록을 조회하는 테스트를 작성했다.
매번 테스트 종료시 저장소를 초기화 해서 다른 테스트 케이스에 영향을 주지않도록 @AfterEach를 만들어줬다.

서블릿으로 회원 관리 웹 애플리케이션 만들기

이제 서블릿으로 회원 관리 웹 애플리케이션을 만들어보자.

코드

1. MemberFromServlet - 회원 가입 폼

회원을 새로 등록하고자 할때 이름과 나이를 받는 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"); } }
Java
복사
회원 가입을 위한 폼을 동적으로 생성하여 반환하는 서블릿이다.
폼 양식을 살펴보면 /servlet/members/save 경로로 POST HTML Form 방식으로 전달한다.

2. MemberSaveServlet - 회원 저장

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>"); } }
Java
복사
HTML POST Form 방식이니 getParameter로 전송받은 Form의 파라미터를 꺼낼 수 있다.
username, age를 getParameter로 꺼낸다.
저장소에 회원을 저장한 뒤 저장 결과를 HTML 타입으로 응답 메세지로 작성해 반환해준다.
서블릿에서 응답 HTML을 작성하다보니 동적으로 자바로직을 추가해서 동적으로 HTML을 생성할수도 있다.

3. MemberListServlet - 회원 목록

저장되있는 회원 목록을 모두 보여주는 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>"); } }
Java
복사
memberRepository.findAll()을 통해 조회한 모든 회원 정보를 토대로 이터레이터 순회(향상된 for문)를 통해 HTML 태그를 작성한다.
회원 목록이 정상적으로 나오는지 확인해보자.
1.
http://localhost:8080/servlet/members/new-form 에서 회원을 1회 이상 등록한다.
⇒ 필자는 catsbi/20, crong/20 두 번 등록을 했다.
2.
http://localhost:8080/servlet/members 경로로 접근해서 회원리스트가 정상적으로 나오는지 확인한다.
/servlet/members 결과 페이지

불편한 HTML 응답 메세지 작성

지금 간단한 예제를 진행하면서도 HTML 코드를 서블릿과 자바 코드만으로 작성을 해줬다.
자바코드 내에서 작성되기 때문에 자바 문법을 통한 동적 생성도 가능하다.
하지만, 이 실습을 실제로 복사 붙혀넣기가아니라 작성해보면서 따라왔다면 엄청나게 불편하고 오타가나서 에러가 발생할 확률이 높다는 점을 알 수 있다.
그래서 자바코드에서 HTML 코드를 작성하는 것이 아닌 HTML코드에서 자바를 사용하는게 훨씬 편리할 수 있다.
그래서 나온게 템플릿 엔진 이다. 이 템플릿 엔진을 사용하면 HTML 필요한 곳만 코드를 적용해 동적으로 변경이 가능하다.
대표적으로 JSP, Thymeleaf, Freemarker, Velocity 와 같은 템플릿 엔진이 있는데, 우선 JSP으로 지금까지 한 로직을 똑같이 구현해볼 것이다.
참고: Wellcom Page Change
지금까지 진행했고, 앞으로 진행할 예제들을 떠 빠르게 찾아갈 수 있게 모든 경로를 작성한 해당 페이지로 변경하자.
index.html

JSP로 회원 관리 웹 애플리케이션 만들기

위에서 서블릿과 자바만 가지고 만들었던 회원관리 웹 애플리케이션에서 HTML 코드를 자바로 작성하기 때문에 생긴 불편함을 해결하기위해 템플릿 엔진을 사용한다고 했다. 그래서 이번엔 이 템플릿 엔진중에서 JSP 를 사용해서 지금까지 구현한 회원 관리 웹 애플리케이션을 구현해보자.

JSP로 회원 관리 웹 애플리케이션 구현

1. JSP 라이브러리 추가

JSP를 사용하기위해서는 우선 build.gradle에 라이브러리를 추가해야한다.
//JSP 추가 시작 implementation 'org.apache.tomcat.embed:tomcat-embed-jasper' implementation 'javax.servlet:jstl' //JSP 추가 끝
Groovy
복사
dependency부분에 해당 라이브러리 추가를 해줬다면 Gradle refresh를 해주자.
두 빨간 박스중 어느것을 선택해도 된다.

2. 회원등록 폼 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>
HTML
복사
<%@ page contentType="text/html;charset=UTF-8" language="java" %> JSP 사용시 첫 줄에 추가해야하는 코드로 이 문서가 JSP라는 의미다.

2-1. 회원등록 폼 확인

접속 URL: http://localhost:8080/jsp/members/new-form.jsp

3. 회원 저장 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>
HTML
복사
JSP에서는 자바 코드를 그대로 다 사용할 수 있다.
<%@ page import ...%> : 자바의 import 문
<% ~~ %> : 자바 코드를 입력하는 블럭
<%= ~~ %> : 자바 코드 출력 블럭

4. 회원 목록 JSP

생성 경로: 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
복사
코드 중간에 자바 코드(향상된 for문)를 작성해서 동적으로 회원 목록을 HTML의 table tag로 만들어준다.

서블릿과 JSP의 한계

서블릿과 자바코드만으로 HTML 코드를 작성할때보다는 확실히 오류 발생 확률이나 작성 난이도가 낮아졌다.
하지만, 다음과 같은 문제에 직면하게 된다.
화면을 보여주는 HTML 코드와 자바 코드가 한 소스에 섞여있어 가독성이 떨어진다.
데이터 조회(MemberRepository), Java 로직, Request, Response 조작등 각 계층의 모든 코드가 JSP에 노출되어있다.

MVC 패턴

도메인영역, 뷰영역, 컨트롤러 영역등 각각의 영역에서 자신의 역할만 담당하도록 분리하는 방법
비즈니스 로직은 위에서 했던 서블릿과 같이 다른 부분에서 처리하고 JSP에서는 HTML로 화면단을 구현하는데 집중한다. 다음 챕터부터는 이런 MVC 패턴을 사용해서 리팩토링을 해보자.

MVC패턴 - 개요

서블릿과 JSP만 가지고 처리를 하나보면 하나의 영역에서 여러 영역의 코드들이 작성되는 문제가 있다고 했고, 그래서 MVC 패턴이 나왔다는 얘기를 했는데, 이 MVC 패턴의 등장 배경부터 어떤 구조를 가지고 있는지 학습해보자.

등장 배경

너무 많은 역할

이미 얘기했지만 하나의 서블릿이나 JSP만으로 비즈니스 로직과 뷰 렌더링까지 모두 처리하게 되면 하나의 영역에서 너무 많은 역할을 부담지게 된다. 이는 필연적으로 유지보수가 어려워지는 결과를 낳는다.
비즈니스 로직에 문제가 생겨도, UI를 변경해야해도 두 로직이 같이 있는 파일을 수정해야 하는데, 매번 수정할때마다 소스코드가 수백 수천줄이 있는데 Java코드를 수정해야하는데 그 사이에 HTML 코드가 수천줄이 있거나 반대의 경우가 있다고 하면 수정해야 할 코드를 찾는것만해도 비용소모가 커질 것이다.

변경의 라이프 사이클

가장 큰 문제인데 변경의 라이프사이클이 서로 다르다는 점이다. 정말 큰 변화가 아니고서는
대부분의 변경은 HTML에서 UI변경이 주가 될 것이기에 UI 변경과 비즈니스 로직의 발생시기는 대부분 다를 것이고, 서로에게 영향을 주지도 않는다.
그런데도 이렇게 라이프사이클이 다른 영역을 하나의 코드로 관리하는 것은 유지보수에 좋지 않다.

기능특화

JSP와 같은 뷰 템플릿은 뷰를 그리는데 최적화 되있기 때문에 이 역할만 담당하는것이 제일 좋다.

Model View Controller

MVC 패턴은 지금까지 학습한 서블릿, JSP영역에서 처리되던 것을 컨트롤러(Controller)와 뷰(view)라는 영역으로 서로 영역을 나눈 것을 말한다. 웹 애플리케이션은 보통 이 MVC 패턴을 사용한다.
컨트롤러: HTTP 요청을 받아 파라미터를 검증하고 비즈니스로직을 실행한다. 그리고 뷰에 전달할 결과 데이터를 조회해 모델에 담는다.
모델: 뷰에 출력할 데이터를 담아둔다. 뷰가 필요한 데이터를 모두 모델에 담아 전달해주기에 뷰는 비즈니스 로직이나 데이터 접근에 대해 알 필요가 없고 화면 렌더링에만 집중하면 된다.
: 모델에 담겨있는 데이터를 사용해 화면을 그리는일에 집중한다. 여기서는 HTML 을 생성하는 부분을 말한다.

참고

MVC만 보면 비즈니스 로직을 컨트롤러에 두는 것 같지만 이렇게 되면 이번엔 컨트롤러에서 너무 많은 책임이 부가된다. 그렇기에 일반적으로 비즈니스 로직은 서비스 계층(Service Layer)을 따로 만들어 처리한다.
MVC Pattern 1
MVC Pattern 2

MVC패턴 - 적용

MVC패턴에 대해서 개념을 알아봤다. 하나의 영역(이전시간에는 JSP)에 여러 영역의 코드들을 모아놨었는데 이를 MVC 패턴을 사용해서 분리를 해 볼 것이다.
우선, 서블릿은 컨트롤러로 사용하고, JSP는 뷰만 담당하도록 MVC 패턴을 적용해보자.
그리고, Model은 HttpServletRequest에서 attribute를 이용해 데이터를 보관 및 조회할 것이다.

회원 등록 구현

1. MvcMemberFormServlet

회원 등록 페이지 서블릿 구현
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); } }
Java
복사
dispatcher.forward()
⇒ 다른 서블릿이나 JSP로 이동할 수 있는 메서드, 서버 내부에서 다시 호출이 발생한다.
참고 : /WEB-INF
: 이 경로안에 JSP가 있으면 외부에서 직접 JSP를 호출할 수 없다. 즉, 위에서처럼 경로를 직접 지정하며 JSP를 호출해도 호출할 수 없다는 의미다. 이 경로내에 있는 VIEW 파일은 컨트롤러를 통해서만 호출 가능하다.
참고: redirect vs forward
:리다이렉트는 실제 클라이언트(웹)에 응답이 나갔다가, 클라이언트가 redirect경로로 다시 요청한다. 그렇기에 클라이언트가 인지 할 수 있고, URL경로도 실제로 변경된다. 반면 포워드의 경우 서버 내부에서 일어나는 호출이기 때문에 클라이언트가 인지할 수 없다.

2. new-form.jsp

회원 등록 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>
HTML
복사
action="save"
⇒ 지금까지 학습했을때 경로 앞에 / 슬래쉬를 붙힌것과 다르게 없는데, 이는 이게 절대경로가 아닌 상대경로로 시작한다는 것을 의미하는데 이렇게 상대경로로 폼을 전송하면
현재 URL이 속한 계층 경로 + 작성한 상대경로로 호출된다.
현재 계층 경로: /servlet-mvc/members/
결과: /servlet-mvc/members/save

회원 저장 구현

1. MvcMemberSaveServlet

회원 저장 - 컨트롤러 구현
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); } }
Java
복사
request.setAttribute("member", member)
HttpServletRequestModel로 사용한다.
request가 제공하는 setAttribute() 메서드를 사용해 request객체에 데이터를 보관해서 뷰에 전달할 수 있다.
⇒ 뷰에서는 request.getAttribute()를 사용해 저장한 데이터를 꺼내 조회할 수 있다.

2. save-result.jsp

회원 저장 - 뷰
생성 경로: 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>
HTML
복사
${member.id}
⇒ 원래 <%=request.getAttribute("member")%>를 사용해서 모델에 저장한 member 객체를 꺼낼 수있지만 이를 실제 사용하려면 형변환을 한 뒤 그 객체에서 필요한정보를 getter를 통해 꺼내는 작업을 해야해서 많이 번거로워진다.
⇒ 그래서 JSP는 ${} 문법을 제공하는데, 이를 사용하면 request의 attribute에 담긴 데이터를 프로퍼티조회할 수 있다. (member.getId() - X)(member.id - O)

3. 실행 및 동작 확인

실행 URL: http://localhost:8080/servlet-mvc/members/new-form
Form 작성후 전송 결과

회원 목록 조회

1. MvcMemberListServlet

회원 목록을 조회- 컨트롤러 구현
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); } }
Java
복사
Model(request)에 attribute에 조회한 멤버리스트 콜렉션을 저장한다.

2. members.jsp

회원 목록 조회 - 뷰
생성 경로 : 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>
HTML
복사
이전에 MVC Pattern을 사용하기 전의 JSP에서는 스크립트릿(<% ~ %>)을 이용해서 자바코드를 불러서 향상된 for문을 사용해서 동적으로 테이블 태그를 생성했었다. 하지만 코드가 지저분해지고, HTML영역에 자바코드가 뒤섞이는 문제가 있었다.
이를 해결하기위해 JSP가 제공하는 taglib 기능을 사용해 반복하면서 출력하도록 구현할 수 있다.
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<c:forEach>...</c:forEach> 문법을 사용할 수 있게 된다.

3. 실행결과 확인

실행 URL: http://localhost:8080/servlet-mvc/members

MVC패턴 - 한계

이처럼 MVC 패턴을 사용함으로써 JSP에 부과되는 수많은 영역들의 코드들을 분리할 수 있었기에
컨트롤러, 모델, 뷰가 명확하게 구분되었다. 그럼으로써 JSP에는 화면을 그리는 코드를 제외하고는 다 걷어낼 수 있었다. 하지만 이런 MVC패턴을 적용함에도 문제는 남아있다.

MVC 컨트롤러의 단점

1. forward 중복

String viewPath = "viewPath"; RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath); dispatcher.forward(request, response);
Java
복사
View로 이동하는 코드가 항상 중복 호출되고있다. 그럼 이 코드를 모듈화해서 공통화하면 해결될까 생각하지만, 그 모듈화된 메서드도 항상 직접호출을 해야하면서 중복이 발생한다.

2. ViewPath 중복

String viewPath = "/WEB-INF/views/new-form.jsp";
Java
복사
회원 관리 웹 애플리케이션을 MVC 패턴을 적용해서 구현하면서 작성한 viewPath만 최소 3번인데 중복되는 부분을 봤을 것이다. 내가 호출하고자하는 jsp 파일이 위치한 상위 디렉토리 경로와 .jsp 라는 접미사다.
prefix: /WEB-INF/views
suffix: .jsp

3. 사용하지 않는 코드

모든 서블릿에서 공통적으로 받는 매개변수와 forward를 통해 전달하는 인자값으로 request, response가 있다. 근데 이 중에서 response는 이번 예제에서는 사용하지 않는 경우가 더 많았다.
void service(HttpServletRequest request, HttpServletResponse response){ ... }
Java
복사
더하여 이런 서블릿 요청/응답 객체를 사용하는 코드는 테스트 케이스 작성이 쉽지 않다.

4. 공통 처리의 어려움

기능이 복잡할수록 컨트롤러에서 공통으로 처리해야하는 부분이 증가할 것인데, 이를 모듈화 하는 비용도 있고, 이 모듈을 항상 호출하는데 신경써야하며 호출을 하지 않았을때 생기는 문제도 있다.

결국....

여러 문제들이 있지만, 결국 이 문제들 또한 종합하자면 공통 처리에 대해서 어려움이 있다는 부분이다. ViewPath부터 request, response객체 forward코드까지 중복되는게 많다.
그래서 이러한 문제를 해결하기 위해서는 컨트롤러가 호출되기 전에 공통적으로 처리될 수 있는 부분은 처리되어야 한다. 그리고 이 문제의 해결책은 프론트 컨트롤러(Front Controller) 패턴을 사용해서 해결할 수 있는데, 이 스프링 MVC의 핵심이 프론트 컨트롤러에 있다.
이러한 프론트 컨트롤러 패턴에 대해 다음 챕터부터 알아보자.

이전 챕터로

다음 챕터로