지난 포스팅에서 서블릿을 통해 HTTP 요청 메세지를 어떻게 처리하고 응답을 보내는지 알아봤다.
본 포스팅에서는 간단한? 회원 관리 웹 애플리케이션을 만들어볼 것이다!
서블릿, JSP, MVC 패턴을 이용해서 '동일한 '웹 애플리케이션을 총 3번 만들어 본다.
가장 오래된 기술인 서블릿을 사용하면 생기는 불편한 점과 비효율적인 면이 무엇인지 알아보고, JSP를 사용했음에도 여전히 남아있는 비효율적인 면들을 살펴볼 것이다.
마지막으로 현재로써 최고의 방식인 서블릿 + JSP를 활용한 MVC 패턴에 존재하는 한계점은 무엇인지, 차근차근 알아가보자.
- 회원 관리 웹 애플리케이션의 요구사항
회원 정보
- 이름 : username
- 나이 : age
기능 요구사항
- 회원 저장
- 전체 회원 목록 조회
설명
- 회원 리포지토리를 생성해 로컬 메모리에 List 형태로 회원 정보 저장
- HTML Form을 통해 회원 등록
- 성공적으로 저장 시, 저장 된 회원 정보를 보여주는 HTML 반환
- 전체 회원 목록을 조회할 수 있는 동적 HTML 화면 개발
- 서블릿으로 회원 관리 웹 애플리케이션 만들기
1. 회원 등록 폼
@WebServlet(name = "memberFormServlet", urlPatterns = "/servlet/members/new-form")
public class MemberFormServlet extends HttpServlet {
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");
}
}
- @WebServlet에 명시한 url로 접근하면 해당 서블릿에 접근하게 된다.
- 이 단계에서는, 처음 웹 브라우저에서 접근하는 과정이므로 HTTP 요청 메세지를 처리할 게 없다.
- 응답으로, HTML Form을 반환해주면 된다.
- 폼의 액션을 보면, 다음 단계에서 개발할 SaveServlet으로 POST 해주고 있는 것을 확인할 수 있다.
- 아래는 반환 받은 HTML Form이다.
- 이제 전송버튼을 눌러 데이터를 던져줬으니, SaveServlet을 개발하러 가보자.
2. 회원 저장
@WebServlet(name = "memberSaveServlet", urlPatterns = "/servlet/members/save")
public class MemberSaveServlet extends HttpServlet {
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);
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>");
}
}
- 이전 폼에서 받은 데이터는 서블릿 덕분에 'request' 객체를 통해서 편하게 조회할 수 있다.
- getParameter() 메소드로 회원 이름, 회원 나이를 꺼내어 변수로 담고 회원을 생성한다.
- 싱글톤인 MemberRepository 인스턴스를 불러와 회원을 저장한다.
- 성공적으로 저장되었다는 것을 동적인 HTML 화면으로 반환해주자! 방금 생성된 회원의 고유ID, 회원 이름, 회원 나이를 반환해준다.
- 참고로, MemberRepository의 save() 메소드는 반환타입이 Member 인데 저장된 회원으로 곧바로 추가적인 작업을 할거면 save()를 호출하면서 Member 객체에 담아줘도 되고, 필요없다면 마치 void 처럼 save() 만 호출해도 된다. 자바가 알아서 반환 타입을 담을 객체를 사용하지 않았다면, 리턴 값은 버려준다.
3. 전체 회원 목록 조회
@WebServlet(name = "memberListServlet", urlPatterns = "/servlet/members")
public class MemberListServlet extends HttpServlet {
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>");
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>");
}
}
- MemberRepository의 findAll() 메소드를 통해 전체 회원 목록을 리스트 형태로 받는다.
- HTML의 table을 만들고, table 내부에 for문을 통해 리스트의 모든 회원정보를 출력해준다.
- @WebServlet의 url로 접근하면 전체 회원 정보를 확인할 수 있다.
- JSP로 회원 관리 웹 애플리케이션 만들기
JSP가 뭐야?
- 지금 만들고 있는 회원 관리 웹 애플리케이션 처럼 동적인 HTML을 만들어 반환하는 과정을 생각해보자.
- 위에서 서블릿만으로 애플리케이션을 개발할 때, 자바 코드에 HTML을 추가해야 해서 굉장히 지저분한 코드가 완성됐다.
- 요구하는 상황에 따라 적절히 HTML을 동적으로 생성해 반환해야 할 때, 그 때 필요한 것이 뷰 템플릿이다.
- 뷰 템플릿을 사용하면 HTML을 기반으로 작성하고, 필요한 부분은 자바코드를 추가할 수 있다.
- 뷰 템플릿에는 JSP, 타임리프 등이 있는데 기능적인 면이나 속도적인 면으로 요즘은 '타임리프'를 많이 사용한다.
- 그래도 고전이 중요한 만큼! JSP 뷰 템플릿을 활용해 다시 회원 관리 웹 애플리케이션을 개발해보자.
1. 회원 등록 폼
<%@ 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 ... %> 은 JSP문서라는 뜻이다.
- 브라우저에서 해당 JSP파일의 절대 경로로 접근하게 되면 HTML Form이 보여지게 된다.
- 액션을 보면, 새로운 save.jsp 파일로 POST 방식으로 데이터를 전달하고 있다.
- 새로운 save.jsp파일을 개발하러 가보자!
2. 회원 저장
<%@ page import="hello.servlet.domain.MemberRepository" %>
<%@ page import="hello.servlet.domain.Member" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
MemberRepository memberRepository = MemberRepository.getInstance();
System.out.println("save.jsp");
String username = request.getParameter("username");
int age = Integer.parseInt(request.getParameter("age"));
Member member = new Member(username, age);
System.out.println("member = " + member);
memberRepository.save(member);
%>
<html>
<head>
<meta charset="UTF-8">
</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>
- 회원 저장부터는 기존의 서블릿 내의 service() 내부에서 getParameter() 메소드를 통해 원하는 데이터를 조회하고 회원을 저장하는 로직을 만들었다.
- JSP는 HTML 기반이다. 어떻게 자바 코드를 추가할까?
- 파일의 상단을 보면 <% ..... %> 부분이 보일 것이다.
- 해당 부분 안에 필요한 자바 코드를 넣으면 된다!
- 그런데 여기서 의문이 든다. 서블릿에서 사용하던 request, response 객체들과 getParameter() 메소드는 어떻게 사용하는걸까?
- JSP파일은 실행 될 때 내부적으로 서블릿으로 변환되기 때문에 사용이 가능하다!
- 실제로 service() 단을 서블릿 처럼 적어주지 않았지만, 마치 service() 가 있는 것처럼 개발하면 자동으로 인식된다.
- 결과적으로 보여주고 싶은 것은 성공적으로 저장된 회원 정보이다.
- JSP에서는 <%= ... %> 을 통해 보여주고 싶은 자바 코드 값을 직접 넣을 수 있다.
3. 전체 회원 목록 조회
<%@ page import="hello.servlet.domain.MemberRepository" %>
<%@ page import="hello.servlet.domain.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로 환경이 바뀐 것이다.
- 결과를 확인해보자!
- 조금 특이한 점이 있다.
- 이전에 서블릿으로 웹 애플리케이션을 개발 할 때 등록했던 회원들이 같이 조회되고 있다.
- MemberRepository를 싱글톤으로 만들었고, 내부의 저장소 store를 static 으로 만들었기 때문에 공유되는 것이다!
- 서블릿과 JSP의 한계
서블릿의 특징과 한계
- 서블릿을 통해 임의의 요청에 대한 응답으로 HTML을 반환할 수 있었다.
- 하지만 작성하다 보니 계속 공통적으로 반복되는 작업들이 존재한다.
- 또한 가장 중요한 응답으로 내보내주는 HTML을 자바 코드 위에서 작성해야 하기 때문에 너무 지저분하고 복잡하다.
- HTML을 간결하게 작성하기 위해 다음 단계로 뷰 템플릿인 JSP를 사용해봤다.
JSP의 특징과 한계
- JSP를 통해 기존 자바 코드 속 복잡한 HTML 코드에서, 진짜 HTML 기반으로 뷰를 만들 수 있게 됐다.
- 필요한 자바 코드들도 JSP가 지원하는 방식으로 같이 작성할 수 있었다.
- 하지만 이렇게 해도 해결되지 않는 몇 가지 고민이 남는다.
- JSP 파일을 보면 코드의 상위 절반은 회원을 저장하기 위한 비지니스 로직이고, 나머지 하위 절반만 결과를 HTML로 보여주기 위한 뷰 영역이다.
- 코드를 잘 보면, 자바 코드, 데이터를 조회하는 리포지토리 등 다양한 코드가 모두 JSP에 노출되어 있다.
- JSP 하나에서 너무 많은 역할을 수행한다.
- 프로젝트 규모가 커질수록 유지보수가 정말 어려울 것이다.
- 따라서 비지니스 로직의 역할과, 뷰 역할을 분리할 수 있는 방법을 고민해봐야 한다.
- MVC 패턴으로 회원 관리 웹 애플리케이션 만들기
MVC 패턴이 뭐야?
- MVC는 Mode, View, Controller를 의미한다.
- MVC 패턴은 지금까지 실습한 것 처럼 하나의 서블릿이나, JSP로 처리하던 것을 컨트롤러와 뷰 영역으로 서로 역할을 나눈 것을 말한다.
- 웹 애플리케이션은 보통 이 MVC 패턴을 사용한다.
- 컨트롤러 (Controller) : HTTP 요청을 받아서 파라미터를 검증하고, 비지니스 로직을 실행한다. 이후 뷰에 전달할 데이터를 모델에 담는다.
- 모델 (Model) : 뷰에 출력할 데이터를 담아둔다. 모델 덕분에 뷰는 비지니스 로직이나 데이터베이스에 대한 접근을 몰라도 되고, 화면을 렌더링 하는 일에만 집중할 수 있다.
- 뷰 (View) : 모델에 담겨있는 데이터를 사용해서 화면을 렌더링 하는 일에만 집중한다. 여기서는 HTML을 생성하는 부분을 의미한다.
- MVC 패턴을 그림으로 이해해보자!
- MVC 패턴은 크게 컨트롤러에서 비지니스 로직까지 담당하는 패턴과, 컨트롤러와 서비스단을 분리시켜 컨트롤러에서 비지니스 로직이 있는 서비스를 호출하는 패턴이 있다.
- 사실 첫 번째 방식을 사용하게 되면 컨트롤러가 너무 많은 역할을 담당하게 된다. 그래서 일반적으로 비지니스 로직을 서비스라는 계층으로 별도로 만드는 방식을 사용한다.
- 자 이제 서블릿은 컨트롤러로 사용하고, JSP를 뷰로 사용해서 MVC 패턴을 적용해보자!
1. 회원 등록 폼
// 컨트롤러
@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);
}
}
// 뷰
<%@ 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>
- 컨트롤러는 서블릿으로 구성했다.
- 단순 회원 등록 폼을 띄워주는 기능이므로, 별 다른 비지니스 로직은 없이 viewPath를 지정해주고 disPacther를 통해 JSP로 이동시켜준다.
- HttpServletRequest의 디스패처는 컨트롤러로 들어온 요청을 다른 컨트롤러나, 뷰로 전달(이동)시켜주는 역할을 한다.
- 뷰(JSP)로 이동하게 되면, 폼에 데이터를 입력하고 전송을 누르면 상대경로인 "save"로 데이터를 POST한다.
- 생각해보자. 기존에 접근했던 url은 "/servlet-mvc/members/new-form" 이다.
- 상대경로 "save" 데이터를 보내면 "/servlet-mvc/members/save"를 호출하게 될 것이다!
- 그럼 이후에 생성할 회원 저장 컨트롤러(서블릿)의 url 경로를 "/servlet-mvc/members/save"으로 잡아주면 된다.
- 참고로 디렉토리 WEB-INF 안에 뷰를 담당할 JSP파일들을 생성해놓으면, 외부에서 직접적으로 디렉토리 경로를 통해 접근하는 것을 허용하지 않는다. 이는 컨트롤러를 통해 접근해야한다는 것을 의미한다.
- 이전에 만든 폼과 동일한 화면을 볼 수 있다.
2. 회원 저장
// 컨트롤러
@WebServlet(name = "mvcMemberSaveServlet", urlPatterns = "/servlet-mvc/members/save")
public class MvcMemberSaveServlet extends HttpServlet {
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);
String viewPath = "/WEB-INF/views/save-result.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
}
}
// 뷰
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<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>
- 자, 이전에 HTML Form의 액션 때문에 SaveServlet이 호출되었다.
- 컨트롤러에서는 회원을 저장하는 비지니스 로직을 수행한다.
- 이전처럼 성공적으로 회원가입을 마치고 나면, 성공 메세지와 함께 회원 정보를 HTML로 반환할 것이다.
- 때문에 컨트롤러 -> 뷰로 이동할 때 "모델"에 저장한 회원을 담아서 보내야한다.
- 모델에 데이터를 담는 방법이 바로 request.setAttribute() 이다.
- viewPath를 지정하고 디스패처를 통해 뷰로 이동시키기 전에, request안에 있는 임시 저장소에 방금 저장한 회원을 담아서 보내는 것이다!
- 이후 뷰로 이동하고 나면, 원래는 모델에서 받아온걸 (Member) 로 캐스팅하고~ getId() 하면서 복잡하게 원하는 값을 <%= ... %> 안에 넣어줘야 하는데, JSP는 또 하나의 편의 기능을 지원한다.
- ${...} 안에 바로 모델값을 꺼내서 넣을 수 있다!
- 성공적으로 회원 정보를 저장한 결과창이다.
3. 전체 회원 목록 조회
// 컨트롤러
@WebServlet(name = "mvcMemberListServlet", urlPatterns = "/servlet-mvc/members")
public class MvcMemberListServlet extends HttpServlet {
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);
}
}
// 뷰
<%@ 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>
- 이전과 동일한 메커니즘이다!
- 다만, 뷰에서 전체 회원 목록을 보여줄 때, "jstl" 이라는 라이브러리를 활용했다.
- <c : forEach> ... </c: forEach> 태그를 사용해서 자바에서 Iterator를 돌리듯이 모든 회원을 출력할 수 있었다.
서블릿 + JSP를 활용한 MVC 패턴의 한계
- MVC 패턴을 적용한 덕분에 컨트롤러의 역할과 뷰를 렌더링 하는 역할을 명확히 구분할 수 있었다.
- 특히 뷰는 화면을 그리는 역할에 충실한 결과, 코드가 매우 깔끔하고 직관적이다. 단순히 모델에서 필요한 데이터를 꺼내고 화면을 만든다.
- 하지만 컨트롤러는 딱 봐도 중복되는 코드가 많고, 심지어 필요없는 코드들도 있다.
- 매번 디스패처를 만들며 forward()를 호출해줘야 한다.
- viewPath를 지정해주는 과정도 중복된다.
- 특히 response는 아예 사용되지 않았다.
- 정리해보면, 공통으로 처리할 수 있는 부분을 따로 만들면 중복되는 코드를 작성하지 않을 수 있다.
- 따라서 요청 시 컨트롤러를 호출하기 이전에 먼저 공통되는 기능을 처리하는, 소위 수문장 역할의 프론트 컨트롤러 (Front-Controller) 패턴을 도입하면 이런 문제를 해결할 수 있다.
- 스프링 MVC의 핵심도 바로 이 프론트 컨트롤러에 있다.
- 다음 포스팅부터 프론트 컨트롤러를 도입한 MVC 패턴에 대해서 알아보자!
- 정리
- 서블릿을 이용한 웹 애플리케이션 개발부터 JSP, 서블릿 + JSP를 활용한 MVC 패턴으로 개발까지 총 3가지 방식으로 같은 웹 애플리케이션을 개발해봤다.
- 순서대로 진행해보며 부족한 점을 조금씩 개선해나가는 과정을 살펴봤다.
- 서블릿 + JSP를 활용한 MVC 패턴을 사용했음에도 중복되는 코드와 불필요한 코드가 있었다.
- 이 문제를 해결하기 위해서는 프론트 컨트롤러를 도입해야한다.
출처 : 인프런, 김영한의 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술
'Spring' 카테고리의 다른 글
[Spring] 스프링 MVC 구조 이해하기 (1) | 2023.07.11 |
---|---|
[Spring] 프론트 컨트롤러를 도입한 MVC 프레임워크 만들기 (1) | 2023.07.09 |
[Spring] 서블릿(Servlet) (2) | 2023.07.05 |
[Spring] 스프링 빈 스코프 (0) | 2023.07.03 |
[Spring] 스프링 빈 생명주기 콜백 (0) | 2023.07.03 |