📌 목차
회원 관리 웹 애플리케이션 요구사항
서블릿으로 회원 관리 웹 애플리케이션 만들기
JSP로 회원 관리 웹 애플리케이션 만들기
서블릿과 JSP 의 한계와 MVC 패턴의 등장
MVC 패턴 - 적용
MVC 패턴 - 한계
📌 회원 관리 웹 애플리케이션 요구사항
간단한 회원 관리 웹 애플리케이션을 만들어 보자.
먼저 서블릿을 통해 만들어 보고 , 서블릿의 한계점을 살펴본후에 더 나은 해결책인 JSP 를 사용한다.
하지만 JSP 를 사용함에도 불구하고 여전히 불편한 점이 생긴다. 그러한 이유로 인해 MVC 패턴이 발생하게 되는데 이번 포스팅을 통해 MVC 패턴이 발생한 배경을 알아본다.
마지막으로 MVC 패턴의 한계점을 살펴본다.
회원정보는 이름 username , 나이 age 로 구성되어있으며 기능은 회원저장과 회원목록 조회 2가지가 존재한다.
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;
}
}
lombok 을 통해 getter,setter 를 애노테이션으로 대체할 수 있다.
package hello.servlet.domain.member;
/**
* 동시성 문제가 고려되어 있지 않음 , 실무에서는 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(); // singleton
private MemberRepository() {
}
public static MemberRepository getInstance() {
return instance;
}
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();
}
}
회원을 저장하는 repository 는 singleton 형식이고 저장과 findById , 전체 찾기 , 전체 제거가 있다.
📌 서블릿으로 회원 관리 웹 애플리케이션 만들기
서블릿을 통해 회원등록 HTML 폼을 만들어 보자.
package hello.servlet.web.servlet;
// http://localhost:8080/servlet/members/new-form
@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();
/**
* 순수 servlet 을 사용하면 html 을 자바코드로 작성해야하므로 매우 불편하다.
*/
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");
}
}
서블릿을 사용하면 자바 코드 안에 HTML 코드를 쓸 수 있다.
순수한 서블릿은 매우 불편한다는 점을 알 수 있다.
HTML 화면은 위와 같다.
이제 동적으로 회원 저장 코드를 만들어 보자.
package hello.servlet.web.servlet;
// http://localhost:8080/servlet/members/new-form
@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 {
System.out.println("MemberSaveServlet.service");
String username = request.getParameter("username");
int age = Integer.parseInt(request.getParameter("age")); // getParameter 의 반환값은 항상 String이다.
Member member = new Member(username , age);
memberRepository.save(member);
response.setContentType("text/html");
response.setCharacterEncoding("utf-8");
PrintWriter w = response.getWriter();
/**
* 동적인 html 코드. 중간에 자바 코드를 통해 동적으로 html에 변화를 줄 수 있다.
*/
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>");
}
}
이전에 만든 MemberRepository 객체를 가져오고 request 를 통해 파라미터를 전달 받은 뒤에 MemberRepository 에 저장한다.
그리고 서블릿을 통해 자바 코드에 HTML 을 넣음으로써 중간에 member.getId() 와 같은 데이터 정보를 함께 출력하면서 동적인 웹 애플리케이션을 실행한다.
HTML form 에서 이름과 나이를 입력하면 위와같은 출력 결과가 나오게 된다.
이제 저장한 회원의 목록을 조회하는 코드를 만들어 보자.
package hello.servlet.web.servlet;
// http://localhost:8080/servlet/members
@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("<html>");
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("<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(" <tr>");
w.write(" <td>1</td>");
w.write(" <td>userA</td>");
w.write(" <td>10</td>");
w.write(" </tr>");
*/
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("</body>");
w.write("</html>");
}
}
순수하게 서블릿만을 사용해서 회원 저장 , 회원목록조회기능을 가진 동적 웹 애플리케이션을 만들었다.
하지만 코드를 보면 불편한 점이 아주 않다.
자바 코드로 HTML 코드를 짜는것 자체가 매우 번거롭다. 차라리 HTML 문서에 동적으로 변경해야 하는 부분만 자바 코드를 넣는 것이 어떨까?
이를 해결하는것이 바로 템플릿 엔진이다. 템플릿 엔진에는 JSP, Thymeleaf, Freemarker, Velocity 등이 있다.
이번에는 JSP 를 통해 좀 더 간결한 코드를 짜보자.
📌 JSP로 회원 관리 웹 애플리케이션 만들기
JSP 란 Java Server Pages 로써 자바를 웹서버에서 쉽게 쓰기 위한 기술이며 언어가 아니다. JSP 를 사용한다면 HTML 코드 안에 자바 코드를 넣을 수 있다.
자바 코드에 HTML 코드를 넣는 Servlet 과는 반대되는 개념이다.
// build.gradle
//JSP 추가 시작
implementation 'org.apache.tomcat.embed:tomcat-embed-jasper'
implementation 'javax.servlet:jstl'
//JSP 추가 끝
jsp 를 사용하기 위해선 build.gradle 경로에 위와 같은 코드를 추가해주어야 한다.
회원을 등록하는 HTML form 을 만들어 보자.
<%@ 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>
첫번째 줄에 있는 내용은 jsp 를 사용하기 위해서는 꼭 적어주어야 한다.
또한 HTML 화면 결과는 Servlet 에서 만든 HTML form 형식과 동일하다.
이제 회원을 저장하는 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();
System.out.println("MemberSaveServlet.service");
String username = request.getParameter("username");
int age = Integer.parseInt(request.getParameter("age")); // getParameter 의 반환값은 항상 String 이다.
Member member = new Member(username , age);
memberRepository.save(member);
%>
<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>
위 코드를 보면 Servlet 으로 작성한 코드보다 훨씬 더 명확하고 깔끔하다.
자바 코드는 상단에 배치하고 HTML 코드는 하단에 배치함으로써 분리가 가능해졌고
<li>id=<%=member.getId()%></li> 와 같은 JSP 문법을 통해 동적 웹 애플리케이션을 간단명료하게 만들 수 있다.
이제 회원 목록을 출력하는 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>
<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>
<%
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 코드의 분리가 명확하다.
하지만 아직까지도 코드에 불편한 점이 여전히 몇가지가 보인다.
📌 서블릿과 JSP의 한계와 MVC 패턴의 등장
서블릿으로 개발할때는 HTML 을 만드는 작업이 자바 코드때문에 지저분하고 복잡했다.
JSP 를 사용한 덕분에 자바 코드와 HTML 코드를 분리함으로써 어느정도 깔끔해졌다.
하지만 코드를 잘보면 JSP 파일 하나에 HTML 코드와 java 코드가 모두 노출되어있다.
만약에 수백 , 수천줄이 넘는 JSP 파일에 HTML 코드를 일부분만 수정해야한다면 어떨까?
그때 만약 java 코드때문에 예기치 못한 상황이 발생할 수 있지 않을까?
또한 아주 사소한 작업 ( 버튼의 위치변경 ) 을 하기 위해서 자바 코드와 HTML 코드가 섞인 상황에서 HTML 코드 뿐만 아니라 자바코드까지 같이 수정해야한다면 일이 복잡해지지않을까?
이를 해결하기 위해서 MVC 패턴이 등장했다.
현재 문제점을 정리하자면 JSP 에 너무많은 역할이 존재하는 것이고
비지니스 로직과 UI 수정같은 작업이 각각 다르게 발생할텐데 JSP 로 인해 비지니스 로직과 UI 수정작업이 동시에 발생한다.
마지막으로 JSP 는 화면을 렌더링 하는데 최적화 되어 있기 때문에 이 부분의 기능만 담당하는것이 가장 효과적이다.
MVC 는 model , view , controller 로 나뉜다.
HTTP 요청을 받아서 파라미터를 검증하고 비지니스 로직을 실행한다. 그리고 모델 , 뷰를 업데이트 한다.모델 : 뷰에 출력할 데이터를 담는다. 이로인해 뷰는 비지니스 로직이나 데이터에 대해 몰라도 되고 화면을 렌더링하는 일에만 집중할 수 있다.
뷰 : 모델에 있는 데이터를 사용해서 화면을 출력하는 일을 담당한다.
📌 MVC 패턴 - 적용
서블릿을 Controller 로 사용하고 jsp 를 view 로 사용해서 MVC 패턴을 사용해보자.
Model 은 HttpServletRequest 객체를 사용한다.
request.setAttribute() , request.getAttribute() 를 사용하면 데이터를 보관하고 , 저장할 수 있다.
회원 등록 폼을 만들어 보자.
package hello.servlet.web.servletmvc;
@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 로 이동할 수 있는 기능이다.
서버 내부에서 다시 호출이 발생하므로 redirection 과는 다른 기능이다.
이때 경로를 /WEB-INF/~ 로 지정해주었는데 이 경로에 있다면 외부에서 jsp 를 직접 호출할수 없다.
즉 localhost:8080/WEB-INF/~ 로 http 요청을 해도 jsp 파일을 볼 수 없다.
이로인해서 jsp 는 항상 컨트롤러에 의해서만 호출이 된다.
<%@ 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>
회원등록 폼 HTML 은 이전에 jsp 에서 사용했던 것을 그대로 사용했다.
이때 경로를 상대경로로 설정해주었다.
이제 회원저장 코드를 만들어보자.
package hello.servlet.web.servletmvc;
@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")); // getParameter 의 반환값은 항상 String이다.
Member member = new Member(username , age);
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);
}
}
회원 저장 코드도 경로 저장과 dispatcher 부분을 제외하고 동일하다.
<%@ 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 코드와 완벽하게 분리되었다.
이제 비지니스 로직변경이 필요할땐 HTML 코드를 수정할 필요가 없어지게 되었다.
또한 JSP 는 ${} 문법을 제공하는데, 이 문법을 사용하면 request 의 attribute 에 담긴 데이터를 편리하게 조회할 수 있다.
이제 회원 목록 조회 컨트롤러와 뷰를 만들어 보자.
package hello.servlet.web.servletmvc;
@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);
}
}
List<Member> members = memberRepository.findAll(); 로 회원 목록을 조회하고 request 에 setAttribute() 메서드를 통해 저장해주었다.
<%@ 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 문을 HTML 코드에서 써야할때는
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> 을 추가하면 forEach 문법을 사용할 수 있다.
지금까지 MVC 패턴을 적용해보았다.
컨트롤러 java 와 뷰 HTML 를 분리함으로써 jsp 에 비해 코드가 더 명확하고 깔끔해졌다. 더불어서 유지보수도 편리해졌다.
📌 MVC 패턴 - 한계
MVC 패턴을 적용한 덕분에 컨트롤러의 역할과 뷰를 렌더링 하는 역할을 명확하게 구분할 수 있다.
그런데 컨트롤러는 중복 코드가 많이 발생하고 필요하지 않는 코드가 몇몇 보인다.
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request , response);
view 로 이동하는 코드가 항상 중복되어 나타난다. 이를 메서드로 따로 만들순 있지만 매번 메서드를 호출해야하며 실수로 호출하지 않았을때 큰 오류가 생길 수 있다.
String viewPath = "/WEB-INF/views/members.jsp"
두번째로 경로에 중복이 발생한다. WEB-INF 경로는 항상 필수적이다.
또한 만약 jsp 가 아니라 thymeleaf 같은 다른 뷰로 변경한다면 코드를 전부 수정해야한다.
HttpServletRequest request, HttpServletResponse response
세번째로 사용하지 않은 코드가 발생한다. dispatcher 를 사용함으로써 response 는 사용하지 않게 되었다. 이로인해 HttpServletResponse 를 사용하는 코드는 테스트 케이스를 작성하기도 어렵다는 문제점이 생긴다.
결론적으로 공통적인 처리가 어려워진다.
기능이 복잡해져서 컨트롤러에 공통으로 처리해야하는 부분이 증가하게 된다면 그 부분을 메서드로뽑아서 쓸 수 있지만 여전히 중복코드가 발생하게 된다.
따라서 이를 해결하기 위해서는 컨트롤러 호출 전에 먼저 공통기능을 처리해야한다.
이를 프론트 컨트롤러 Front Controller 패턴이라고 한다.