회원 관리 웹 애플리케이션 요구사항
- 회원 정보
나이 : age
이름 : username
- 기능 요구사항
회원 저장, 회원 목록 조회
- 클래스
- 회원 도메인 모델
package hello.servlet.domain.member; import lombok.Getter; import lombok.Setter; @Getter @Setter public class Member { private Long id; // 회원 저장소에서 id 할당시킴 private String username; private int age; public Member() { } public Member(String username, int age) { this.username = username; this.age = age; } }- 회원 저장소
스프링 ⇒ 스프링 빈이 싱글톤 보장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 static Map<Long, Member> store = new HashMap<>(); //static 사용 private static long sequence = 0L; //static 사용 private static final MemberRepository instance = new MemberRepository(); // 싱글톤 public static MemberRepository getInstance() { return instance; } private MemberRepository() { //private 생성자 => 아무나 생성하지 못하도록 방지 } 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()); //store의 모든 값들 담음 } public void clearStore() { store.clear(); } }
but 지금은 순수 서블릿으로 구현하므로 직접 싱글톤 패턴 적용
- 테스트 코드
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.*;
class MemberRepositoryTest {
MemberRepository memberRepository = MemberRepository.getInstance();
@AfterEach
void afterEach() {
memberRepository.clearStore();
}
@Test
void save() {
//given
Member member = new Member("hello", 20);
//when
Member savedMember = memberRepository.save(member);
//then
Member findMember = memberRepository.findById(savedMember.getId());
assertThat(findMember).isEqualTo(savedMember);
}
@Test
void findAll() {
//given
Member member1 = new Member("member1", 20);
Member member2 = new Member("member2", 30);
memberRepository.save(member1);
memberRepository.save(member2);
//when
List<Member> result = memberRepository.findAll();
//then
assertThat(result.size()).isEqualTo(2);
assertThat(result).contains(member1, member2);
}
}
서블릿으로 회원 관리 웹 애플리케이션 만들기
- 클래스
- 회원 등록 폼
⇒ 자바로 직접 작성해야 하므로 불편package hello.servlet.web.servlet; 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/newform") public class MemberFormServlet extends HttpServlet { @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"); } }- 회원 저장 폼
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 { System.out.println("MemberSaveServlet.service"); 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); 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>"); } }- 회원 목록 폼
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 { response.setContentType("text/html"); response.setCharacterEncoding("utf-8"); List<Member> members = memberRepository.findAll(); 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 문서 생성 가능
⇒ BUT 매우 복잡, 비효율적 ⇒ 차라리 HTML 문서에 동적으로 변경해야 하는 부분만 자바 코드를 넣을 수 있다면 더 편리
⇒ 템플릿 엔진 등장 (HTML 문서에서 필요한 곳만 코드 적용, 동적으로 변경)
ex) JSP, Thymeleaf, Freemarker, Velocity
※ JSP ⇒ 사장되는 추세 / 스프링에서는 Thymeleaf 권장
- html
- main/webapp/index.html
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <ul> <li><a href="basic.html">서블릿 basic</a></li> <li>서블릿 <ul> <li><a href="/servlet/members/new-form">회원가입</a></li> <li><a href="/servlet/members">회원목록</a></li> </ul> </li> <li>JSP <ul> <li><a href="/jsp/members/new-form.jsp">회원가입</a></li> <li><a href="/jsp/members.jsp">회원목록</a></li> </ul> </li> <li>서블릿 MVC <ul> <li><a href="/servlet-mvc/members/new-form">회원가입</a></li> <li><a href="/servlet-mvc/members">회원목록</a></li> </ul> </li> <li>FrontController - v1 <ul> <li><a href="/front-controller/v1/members/new-form">회원가입</a></li> <li><a href="/front-controller/v1/members">회원목록</a></li> </ul> </li> <li>FrontController - v2 <ul> <li><a href="/front-controller/v2/members/new-form">회원가입</a></li> <li><a href="/front-controller/v2/members">회원목록</a></li> </ul> </li> <li>FrontController - v3 <ul> <li><a href="/front-controller/v3/members/new-form">회원가입</a></li> <li><a href="/front-controller/v3/members">회원목록</a></li> </ul> </li> <li>FrontController - v4 <ul> <li><a href="/front-controller/v4/members/new-form">회원가입</a></li> <li><a href="/front-controller/v4/members">회원목록</a></li> </ul> </li> <li>FrontController - v5 - v3 <ul> <li><a href="/front-controller/v5/v3/members/new-form">회원가입</a></ li> <li><a href="/front-controller/v5/v3/members">회원목록</a></li> </ul> </li> <li>FrontController - v5 - v4 <ul> <li><a href="/front-controller/v5/v4/members/new-form">회원가입</a></ li> <li><a href="/front-controller/v5/v4/members">회원목록</a></li> </ul> </li> <li>SpringMVC - v1 <ul> <li><a href="/springmvc/v1/members/new-form">회원가입</a></li> <li><a href="/springmvc/v1/members">회원목록</a></li> </ul> </li> <li>SpringMVC - v2 <ul> <li><a href="/springmvc/v2/members/new-form">회원가입</a></li> <li><a href="/springmvc/v2/members">회원목록</a></li> </ul> </li> <li>SpringMVC - v3 <ul> <li><a href="/springmvc/v3/members/new-form">회원가입</a></li> <li><a href="/springmvc/v3/members">회원목록</a></li> </ul> </li> </ul> </body> </html>
JSP로 회원 관리 웹 애플리케이션 만들기
- 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' //
추가 후 gradles refresh
- JSP
<%@ page import=”” %> ⇒ 자바 import와 동일
<% %> ⇒ 자바 코드 입력
<%= %> ⇒ 자바 코드 출력
- 회원 등록 폼 (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>
- 회원 저장 폼 (main/webapp/jsp/members/save.jsp)
<%@ page import="hello.servlet.domain.member.MemberRepository" %>
<%@ page import="hello.servlet.domain.member.Member" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
// request, response 사용 가능
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>
- 회원 목록 폼 (main/webapp/jsp/members.jsp)
<%@ page import="java.util.List" %>
<%@ page import="hello.servlet.domain.member.MemberRepository" %>
<%@ page import="hello.servlet.domain.member.Member" %>
<%@ 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>
- 서블릿, JSP의 한계
서블릿 ⇒ 뷰 화면을 위한 HTML 생성하는 작업이 자바 코드에 섞여 지저분, 복잡
JSP ⇒ 뷰 생성하는 HTML 작업 깔끔 BUT 비즈니스 로직, 리포지토리 코드 등 너무 다양한 코드가 JSP에 노출됨
JSP가 너무 많은 역할 담당 ⇒ 유지 보수 어려움
⇒ MVC 패턴 등장
비즈니스 로직은 서블릿처럼 다른 곳에서 처리
JSP는 목적에 맞게 HTML로 화면(View)을 그리는 일에 집중
MVC 패턴 - 개요
- 변경의 라이프 사이클
자바, HTML 코드 간 변경의 라이프 사이클이 다름
UI - 비즈니스 로직을 수정하는 일은 각각 다르게 발생할 가능성이 매우 높고 대부분 서로에게 영향을 주지 않음
이러한 변경 라이프 사이클이 다른 부분을 하나의 코드로 관리 => 유지보수에 ▲
(물론 UI가 많이 변하면 함께 변경될 가능성 有)
- 기능 특화
JSP 등 뷰 템플릿 => 화면 렌더링에 최적화
=> 해당 업무만 담당하는 것이 가장 효과적
- Model View Controller
하나의 서블릿/JSP로 처리하던 것을 컨트롤러(Controller)와 뷰(View)라는 영역으로 서로 역할을 나눈 것
컨트롤러: HTTP 요청을 받아 파라미터 검증, 비즈니스 로직 실행 → 뷰에 전달할 결과 데이터를 조회해 모델에 담음
모델: 뷰에 출력할 데이터를 담아둠 => 뷰는 비즈니스 로직이나 데이터 접근을 몰라도 되고, 화면 렌더링에 집중
뷰: 모델에 담겨있는 데이터를 사용해 화면 그리는 일에 집중 (여기서는 HTML을 생성하는 부분)
※
일반적으로 비즈니스 로직은 서비스(Service)라는 계층을 별도 생성, 처리
컨트롤러는 비즈니스 로직이 있는 서비스를 호출
- MVC 패턴 이전

- MVC 패턴 1

- MVC 패턴 2

MVC 패턴 - 적용
Model => HttpServletRequest 객체 사용
request => 내부에 데이터 저장소 가짐
=> request.setAttribute() , request.getAttribute() 로 데이터 보관/조회
- 회원 등록 폼 (MvcMemberFormServlet)
- 컨트롤러
@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); //다른 서블릿이나 JSP로 이동 => 서버 내부에서 다시 호출 발생
}
}
- 뷰 (main/webapp/WEB-INF/views/new-form.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>
※ /WEB-INF
이 경로 안에 JSP가 있으면 외부에서 직접 JSP 호출 불가
=> 항상 컨트롤러를 통해 JSP를 호출
※ redirect vs forward
redirect :
실제 클라이언트(웹 브라우저)에 응답이 나갔다가, 클라이언트가 redirect 경로로 다시 요청
=> 클라이언트가 인지할 수 있고, URL 경로도 실제로 변경됨
forward :
서버 내부에서 일어나는 호출이기 때문에 클라이언트가 전혀 인지 X
- 회원 저장
- 컨트롤러 (MvcMemberSaveServlet)
@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);
System.out.println("member = " + member);
memberRepository.save(member);
request.setAttribute("member", member); //Model에 데이터 보관
String viewPath = "/WEB-INF/views/save-result.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath); // 이동 경로 지정
dispatcher.forward(request, response); // 페이지 이동
}
}
- 뷰 (main/webapp/WEB-INF/views/save-result.jsp)
<%@ 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>
<%= request.getAttribute("member")%> 대신 ${} 사용해 request의 attribute에 담긴 데이터를 편리하게 조회
- 회원 목록 조회
- 컨트롤러
@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 {
System.out.println("MvcMemberListServlet.service");
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);
}
}
- 뷰 (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>
<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>
MVC 패턴 - 한계
MVC 패턴 => 컨트롤러의 역할 - 뷰 렌더링 역할을 명확하게 구분 가능
특히 뷰는 화면을 그리는 역할에 충실
but 컨트롤러는 중복이 많고, 필요하지 않은 코드들도 많음
- 단점
- 포워드 중복
View로 이동하는 코드가 항상 중복 호출됨
이 부분을 메서드로 공통화해도 되지만 해당 메서드도 항상 직접 호출해야 함
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
- ViewPath에 중복
String viewPath = "/WEB-INF/views/new-form.jsp";
prefix: /WEB-INF/views/
suffix: .jsp
=> jsp가 아닌 thymeleaf 같은 다른 뷰로 변경한다면 전체 코드를 다 변경해야 함
- 사용하지 않는 코드
request, response 사용할 때도, 사용하지 않을 때도 있음
테스트 코드 적용도 어려움
- 공통 처리 어려움
기능이 복잡해질 수록 컨트롤러에서 공통 처리해야 하는 부분 점점 증가
단순히 공통 기능을 메서드로 뽑으면 될 것 같아도, 결과적으로 해당 메서드를 항상 호출해야 함(=중복)
=> 프론트 컨트롤러(Front Controller) 패턴
컨트롤러 호출 전에 먼저 공통 기능을 처리
'Programming > 스프링' 카테고리의 다른 글
| [김영한 스프링 MVC 1] 스프링 MVC - 구조 이해 (0) | 2023.10.22 |
|---|---|
| [김영한 스프링 MVC 1] MVC 프레임워크 만들기 (0) | 2023.09.24 |
| [김영한 스프링 MVC 1] 서블릿 (0) | 2023.08.13 |
| [김영한 스프링 MVC 1] 웹 애플리케이션 이해 (0) | 2023.08.06 |
| [스프링] 컨트롤러 파라미터 어노테이션 - 클라이언트 → 서버 (0) | 2023.08.05 |