김영한님의 스프링 MVC 1편 강의를 듣고 정리하는 글입니다
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-1/dashboard
1. 개요
이전 포스팅에서 서블릿만을 사용하여 HTML 문서를 동적으로 생성하여 뷰로 표현 해보았다.
https://soonmin.tistory.com/79
하지만 서블릿만 사용한다면, 뷰를 생성하는 내용과 애플리케이션 로직 섞여서 유지보수도 힘들고 버그도 찾기 힘들 것이다. 이번장에서 이런 문제점을 개선하기 위해 JSP와 서블릿을 활용하여 단계적으로 MVC 패턴을 적용해보겠다.
간단한 회원가입 후 회원목록을 보여주는 예제를 서블릿과 JSP로 각각 구현해 본 다음 서블릿 + JSP로 구현해보겠다.
먼저, Member 도메인과 MeberRepository를 구현해보자.
Member.java
- id, username, age 정보를 가지는 Member 도메인이다.
@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;
}
}
MemberRepository.java
- MemberRepository는 싱글톤으로 관리되도록 한다.
- 간단하게 store에 대한 crud를 메서드를 제공한다.
- Member 데이터를 Map으로 저장하여 관리한다. (간단한 예제이므로 HashMap을 사용하긴 했지만, 실무에서는 동시성 이슈를 고려해야 하기 때문에 ConcurrentHashMap 사용을 고려해야 한다.
public class MemberRepository {
private 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;
}
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();
}
}
1. 서블릿(회원관리 예제)
a. 서블릿 예제
MemberFormServlet.java
- HTML 구조를 반환하는 서블릿 코드이다.
- username과 age를 폼 데이터를 입력하고 '/servlet/members/save' post로 전송한다.
@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");
}
}
MemberSaveServlet.java
- MemberFormServlet에서 전송한 폼 데이터를 MemberRespository에 저장하는 코드이다.
- 응답데이터로 HTML 구조로 저장된 구조로 Member 정보를 반환한다.
@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"));
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>");
}
}
MemberListServlet.java
- MemberRepository에 저장된 멤버 목록을 조회하는 코드이다.
- members를 순회하면서 HTML을 만들어 반환한다.
@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>");
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>");
}
}
b. 서블릿 한계
서블릿만을 사용하여 HTML과 애플리케이션 로직을 구현해 보았다. 동적으로 HTML을 만들 수 있어 사용자에게 다양한 정보를 제공할 수 있을 것이다.
하지만, HTML을 처음부터 끝까지 자바코드로 생성해야 하고, 애플리케이션 로직과 뒤섞여서 보기 힘들다.
HTML을 동적으로 변경해야 하는 부분만 자바 코드를 넣어서 작성하기 위해 템플릿엔진이 나왔다.
템플릿엔진은 JSP, 타임리프 등이 있는데, JSP를 사용한 예제를 보자.
2. JSP(회원 관리 예제)
a. JSP 예제
build.gradle
- JSP를 사용허기에 앞서 디펜던시를 추가해줘야 한다.
// 스프링부트 3.0 이하
implementation 'org.apache.tomcat.embed:tomcat-embed-jasper'
implementation 'javax.servlet:jstl'
// 스프링 부트 3.0 이상
implementation 'org.apache.tomcat.embed:tomcat-embed-jasper'
implementation 'jakarta.servlet:jakarta.servlet-api'
implementation 'jakarta.servlet.jsp.jstl:jakarta.servlet.jsp.jstl-api'
implementation 'org.glassfish.web:jakarta.servlet.jsp.jstl'
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
- JSP를 사용하기 위해 첫 줄에 들어가야 하는 내용이다. JSP 문서라는 뜻이다.
<%@ page import="hello.servlet.domain.member.MemberRepository" %>
- 자바의 import 문과 같다.
<% ~~ %>
- 자바 코드를 입력하는 부분이다.
<%= ~~ %>
- 자바 코드를 출력할 수 있는 부분이다.
main/webapp/jsp/members 디렉토리 jsp 파일을 생성했다.
new-from.jsp
- 앞에서 자바코드로 작성한 HTML 폼과 동일하다.
<%@ 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>
save.jsp
- <% page import= ~~~ %> 로 사용할 클래스들을 import 한다.
- <% %> 애플리케이션 로직을 자바 코드로 작성한다.
- <%= %> 출력하고 싶은 내용을 넣어준다.
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="hello.servlet.domain.member.Member" %>
<%@ page import="hello.servlet.domain.member.MemberRepository" %>
<%
//request, response 사용 가능
MemberRepository memberRepository = MemberRepository.getInstance();
System.out.println("MemberSaveServlet.service");
String username = request.getParameter("username");
int age = Integer.parseInt(request.getParameter("age"));
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>
members.jsp
- MemberRepository에서 조회한 결과 List을 사용하여 반복해서 출력한다.
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="hello.servlet.domain.member.Member" %>
<%@ page import="java.util.List" %>
<%@ page import="hello.servlet.domain.member.MemberRepository" %>
<%
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>
main/webapp/jsp/members 디렉토리 jsp 파일을 했다.
b. JSP 한계
JSP를 사용하면 동적으로 변경되는 부분만 자바코드를 적용하면 된다는 장점이 있다.
하지만, JSP 파일 하나에 너무 많은 애플리케이션 로직이 함께 있어 유지보수가 매우 힘들 것이다.
이런 문제점들을 해결하기 위해 MVC 패턴이 등장하게 되었다.
3. MVC 패턴(개요)
너무 많은 역할
- 앞에서 이야기했듯이, 서블릿이나 JSP 각각을 가지고 비즈니스 로직과 화면을 그리는 작업을 모두 처리하게 되면 유지보수의 어려움이 있다. 즉 너무 많은 역할을 책임지게 되어 유지보수에 어려움이 있다.
변경의 라이프 사이클
- 너무 많은 역할을 전담하고 있게 되면, 예를 들어 UI를 일부 수정하는 일이 발생했을 때 비즈니스 로직에도 영향을 주게 된다. 이런 식으로 변경의 라이프 사이클이 다른 부분을 하나의 코드(파일)로 관리하게 되면 유지보수하기에 좋지 않다.
MVC(Model View Controller)
하나의 서블릿이나 JSP로 처리하던 것을 Controller와 View라는 영역으로 역할을 나눈 패턴이다. 현재는 보통 웹 애플리케이션에 보통 mvc패턴을 사용한다.
- Controller(컨트롤러): HTTP 요청을 받아서 파라미터를 검증하고, 비즈니스 로직을 실행하고, 그리고 뷰에 전달한 결과 데이터를 모델에 담는다.
- Model(모델): 뷰에 출력할 데이터를 담아둔다. 뷰가 필요한 데이터를 모두 모델에 담아서 전달해 주기 때문에 비즈니스이나 데이터 접근을 몰라도 되고, 화면 렌더링 하는 일에 집중할 수 있다.
- View(뷰): 모델에 담겨있는 데이터를 사용해서 화면에 그리는 일에 집중한다. (HTML 생성)
컨트롤러에서 비즈니스 로직을 처리할 수 있지만, 이렇게 되면 컨트롤러에서 너무 많은 역할을 가진다. 그래서 비즈니스 로직을 담당하는 Service 계층을 별도로 만들어서 처리한다.
4. MVC 패턴(적용)
지금부터 MVC 패턴을 적용하여 회원관리 예제를 구현해 보자.
jsp 파일들을 /WEB-INF에 생성할 것인데, /WEB-INF에 있는 jsp는 외부에서 직접 접근할 수 없다.
redirect vs forward
- redirect: 클라이언트에 응답이 나갔다가, 브라우저에서 해당 redirect 경로로 다시 요청하는 것이므로 url 변경이 일어남.
- forward: 서버 내부에서 일어나는 호출이기 때문에 클라이언트가 전혀 인지하지 못하므로, url 경로가 변경되지 않음.
MvcMemberFormServlet.java
- 해당 url로 요청이 오면 /WEB-INF/views/new-form.jsp로 이동하는 코드이다.
- dispatcher.forwad(req, res) 서버내부에서 다시 호출이 발생한다.
- 즉, /servlet-mvc/members/new-form 요청이 오면 서버 내부에서 /WEB-INF/views/new-form.jsp 호출하게 된다.
@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);
}
}
new-form.jsp
- MvcMemberFormServlet에서 forward 하게 되면 호출되는 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>
MvcMemberSaveServlet.java
- 폼 데이터를 입력하게 되면 호출되는 서블릿이다.
- MemberRepository에 저장하는 로직이다.
- request.setAttribute()를 통해 데이터를 저장한다.(모델)
- 마찬가지로 forward()로 jsp를 호출한다.
@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);
String viewPath = "/WEB-INF/views/save-result.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
}
}
save-result.jsp
- <%=request.getAttribute("member") %>로 모델에 저장한 member 객체를 꺼내서 사용할 수 있지만 코드가 복잡해진다.
- jsp에서 ${} 를 제공하여 request에 담긴 attriubte를 편하게 조회할 수 있다.
<%@ 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>
MvcMemberListServlet.java
- MemberRepository에서 조회한 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);
}
}
members.jsp
- 모델에 담긴 List를 <c:forEach> 기능을 사용하여 출력하였다.
- <c:forEach>를 사용하기 위해서는 <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>를 선언해야 한다.
- jsp는 이와 같이 화면 렌더에 특화된 다양한 기능을 제공한다.
<%@ 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>
5. 서블릿 + jsp로 구현한 MVC 패턴의 한계
코드 중복
서블릿 + jsp로 구현한 mvc 패턴 또한 문제점은 viewPath를 지정하고, forward 하는 코드가 공통적으로 들어간다는 것이다. 만약 템플릿엔진을 jsp에서 타임리프로 모두 변경한다면, viewPath를 하나하나 찾아서 변경해줘야 하는 번거러움이 있을 것이다.
사용하지 않는 코드
서블릿의 경우 HttpResponse, HttpRequest를 사용하지 않는 부분도 있을 수 있는데 무조건 포함되어 있다. 쓸 모 없는 코드를 포함하는 것도 mvc 패턴의 한계이다.
공통처리의 어려움
또한 공통처리가 어려울 것이다. 모든 http 요청에 대한 공통로직을 메서드를 정의해서 메서드를 호출하면 된다고 생각지만, 호출하는 것 자체도 반복이며, 실수로 호출하지 않으면 문제가 발생할 것이다.
스프링 mvc의 핵심이 되는 프론트 컨트롤러를 통해 mvc 패턴의 한계를 해결할 수 있다. 다음 포스팅에서는 프론트 컨트롤러에 대해 정리 보겠다.
'Programming > Spring' 카테고리의 다른 글
[Spring] 스프링 MVC 1편(MVC 구조 이해) (0) | 2024.04.02 |
---|---|
[Spring] 스프링 MVC 1편(MVC 프레임워크 만들기) (4) | 2024.03.14 |
[Spring] 스프링 MVC 1편(서블릿) (0) | 2024.02.02 |
[Spring] 스프링 MVC 1편(웹 애플리케이션 이해) (0) | 2024.01.22 |
[Spring] 웹 스코프란? (1) | 2024.01.11 |