간단한 회원관리 웹 애플리케이션을 만들어보면서 MVC 패턴이 왜 도입됬는지 알아보자. 회원 정보를 저장하고 조회하는 기능을 구현한다. 프로젝트 구성은 아래의 글의 구성과 같다.
https://gunjoon.tistory.com/136
Spring Boot에서 Servlet 사용하기
프로젝트 구성 IDE : Intellj JDK 1.8 Maven Project (groupId = hello, artifactId = servlet, packaging = jar) Spring Boot 2.4.x Dependency (Spring Web, Lombok) HelloServlet 전통적인 스프링 웹 애플리케이션은 web.xml에 서블릿을 등록
gunjoon.tistory.com
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;
}
}
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();
}
}
- 싱글톤 패턴을 적용하여 서블릿 컨테이너에서 하나의 저장소만 동작한다.
- 저장소로 간단하게 HashMap을 사용한다.
서블릿으로 회원 관리 웹 애플리케이션 만들기
1. MemberFormServlet - 회원 가입 폼
회원 가입 페이지를 반환하는 서블릿
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 방식으로 전달한다.
2. MemberSaveServlet - 회원 저장
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>");
}
}
- getParameter 메서드로 Form 파라미터를 꺼낼 수 있다.
- 저장소에 회원을 저장한 뒤 저장 결과를 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>");
}
}
불편한 HTML 응답 메세지 작성
지금까지 서블릿에서 HTML 응답 메세지를 동적으로 생성해 반환했다. 하지만 HTML 응답 메세지를 동적으로 생성하는 과정이 너무 불편하다. 자바 코드에서 HTML를 생성하는 것이 아닌 HTML에서 자바를 사용하면 더 편리하지 않을까? 그래서 템플릿 엔진이 등장했다. 템플릿 엔진을 사용하면 HTML에 프로그래밍 언어를 적용할 수 있다. 대표적인 템플릿 엔진으로 JSP, Thymeleaf, Freemarker, Velocity 등이 있다. JSP를 사용해 위 로직을 똑같이 구현해보자.
JSP로 회원 관리 어플리케이션 만들기
1. JSP 라이브러리 추가
pom.xml에 아래의 종속성을 추가한다.
application.properties 파일에 아래 내용을 추가한다.
jsp 디렉터리를 생성한다. (경로 : /src/main/webapp/WEB-INF/jsp/)
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>
- 첫 줄은 이 문서가 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>
- JSP에서 자바 코드를 그대로 사용할 수 있다.
- <%@ page import ...%> : 자바의 import 문
- <% ~~ %> : 자바 코드 입력 블럭
- <%= ~~ %> : 자바 코드 출력 블럭
4. 회원 등록 폼 JSP
생성 경로 : main/webapp/jsp/members/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>
서블릿과 JSP의 한계
- 하나의 서블릿이나 JSP만으로 비즈니스 로직과 뷰 렌더링까지 하게 되면 하나의 영역에서 너무 많은 역할을 부담하게 된다. 이는 유지보수가 어려워지는 결과를 낳는다.
- 변경의 라이프 사이클 : 비즈니스 로직과 뷰 로직의 라이프사이클은 서로 다르다. 라이프사이클이 다른 영역을 하나의 코드로 관리하는 것은 유지보수에 좋지 않다.
- 기능 특화 : JSP와 같은 뷰 템플릿 엔진은 뷰를 만드는데 특화되었기 때문에 뷰만 만들도록 하는게 좋다.
MVC 패턴
MVC (Model View Controller) 패턴은 소프트웨어 디자인 패턴으로 비즈니스 로직과 뷰 로직을 분리시켜 유지 보수를 쉽게 할 수 있다. 현재 대부분의 웹 어플리케이션이 MVC 패턴을 도입하고 있다. 위에 코드에 MVC 패턴을 적용하면 서블릿을 컨트롤러로 JSP를 뷰로 만들 수 있을 것이다.
웹 어플리케이션의 MVC 패턴
- 컨트롤러 : HTTP 요청을 받아 파라미터를 검증하고 비즈니스 로직을 수행한다. 그리고 뷰에 전달할 데이터를 모델에 담는다.
- 모델 : 뷰에 출력할 데이터를 담아둔다.
- 뷰 : 모델에 담겨있는 데이터로 화면에 그리는 일을 수행한다. 여기서는 HTML을 생성하는 부분을 말한다.
컨트롤러에서 비즈니스 로직까지 수행하면 너무 많을 일을 수행한다. 그래서 일반적으로 비즈니스 로직은 서비스 계층에서 처리하도록 분리한다.
'Spring > Spring MVC' 카테고리의 다른 글
스프링 검증 (1) (0) | 2023.03.27 |
---|---|
스프링 MVC 기본 기능 (0) | 2023.02.28 |
스프링 MVC (2) (0) | 2023.02.21 |
Spring Boot에서 Servlet 사용하기 (0) | 2023.02.16 |
서블릿 (0) | 2023.02.14 |