회원관리 웹 애플리케이션 요구사항
회원관리 웹을 만들고자 한다
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;
} }
회원 정보는 다음과 같다
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() {
}
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는 위와 같이 구성했다.
Repository는 private로 선언하고 get문으로 받을 수 있게 했다.
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 foundMember = memberRepository.findById(savedMember.getId());
assertThat(foundMember).isEqualTo(savedMember);
}
@Test
void findAll() {
// given
Member member1 = new Member("member1", 30);
Member member2 = new Member("member2", 25);
memberRepository.save(member1);
memberRepository.save(member2);
// when
List<Member> result = memberRepository.findAll();
// then
assertThat(result.size()).isEqualTo(2);
assertThat(result).contains(member1, member2);
}
}
이건 테스트코드이다!
assertThat는 static으로 선언해두면 Assertion을 쓰지 않아도 된다.
그리고 clearStore()을 통해서 초기화 해야 db가 초기화되므로 afterEach에 넣어주었다.
서블릿으로 회원 관리 웹 애플리케이션 만들기
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/new-form")
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");
}
}
이건 이전부터 봐왔던 서블릿이다!
html을 응답할 것이니까 contentType는 text/html로 두고, printWriter을 통해서 클라이언트로부터 정보를 입력받을 폼을 만든다.
저 폼에서 데이터를 입력하고 전송을 누르면 데이터가 저장되도록 하는 코드를 짜보자
저 위에서 form action부분에 /save를 넣어두었다.
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>");
}
}
파라미터를 조회해서 멤버 객체를 만들고, repository를 통해서 저장한다. 그 다음 member 객체를 사용해서 결과 화면용 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>Member List</title>");
w.write("</head>");
w.write("<body>");
w.write("<a href=\"/index.html\">메인</a>");
w.write("<table border='1'>");
w.write(" <thead>");
w.write(" <tr>");
w.write(" <th>id</th>");
w.write(" <th>username</th>");
w.write(" <th>age</th>");
w.write(" </tr>");
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>");
}
}
member을 list로 받고, for문을 통해서 순차적으로 탐색하면서 표시하도록 하였다.
하지만 이 방법은 복잡하고 비효율적이다. 이것을 위해 나온 것이 템플릿엔지니다.
템플릿 엔진에는 JSP, Thumeleaf 등이 있다.
JSP로 회원 관리 웹 애플리케이션 만들기
jsp 라이브러리 추가해아한다.
`main/webapp/jsp/members/new-form.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>
다음은 입력 form 코드이다.
JSP는 서버 내부에서 서블릿으로 반환되는데, 우리가 만들었던 서블릿과 거의 비슷한 모습으로 변환된다.
http://~/new-form.jsp처럼 마지막에 .jsp를 찍어주어야한다.
main/webapp/jsp/members/save.jsp`
```html
<%@ 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>
<%~%>이 부분에는 자바 코드를 입력할 수 있고 ,
<%=~%>는 html 중간에 자바 코드를 출력할 수 있다.
<%@ 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>Member List</title>
</head>
<body>
<a href="/index.html">메인</a>
<table border="1">
<thead>
<tr>
<th>id</th>
<th>username</th>
<th>age</th>
</tr>
</thead>
<tbody>
<%
for (Member member : members) {
%>
<tr>
<td><%= member.getId() %></td>
<td><%= member.getUsername() %></td>
<td><%= member.getAge() %></td>
</tr>
<%
}
%>
</tbody>
</table>
</body>
</html>
또한 위와 같이 <tr>태그로 리스트를 출력할 수 있다.
서블릿으로 개발할 때 화면을 생성하기 위한 html을 만드는 작업이 자바 코드에 섞여서 지저분하다.
jsp를 사용하여 뷰를 사용하는 HTML 작업을 깔끔하게 가져가고, 중간중간 동적으로 변경이 필요한 부분에만 자바코드를 적용했다.
하지만, JSP에 java코드, 데이터 조회 등의 너무 많은 코드가 jsp에 노출되어있다. 이렇게 하면 나중에 프로젝트가 커졌을 때 유지보수가 어렵다.
MVC 패턴 - 개요
하나의 서블릿이나 jsp로 비지니스 로직, 뷰 랜더링까지 하면 너무 많은 역할을 하게 된다. -> 유지보수 어려워짐
진짜 문제는 UI와 비지니스 로직은 변경의 라이프사이클이 다르다는 것이다.
MVC는 model, view, controller 세가지 영역으로 역할을 나눈 것을 말한다.
웹 애플리케이션은 보통 이 MVC패턴을 사용한다.
컨트롤러 : HTTP 요청을 받아서 파라미터를 검증하고, 비지니스 로직 수행, 그리고 뷰에 전달할 데이터를 조회해서 모델에 담는다
모델 : 뷰에 출력할 데이터를 담아둔다. 뷰가 필요한 데이터를 모두 모델에 담아서 전달해서 뷰는 비지니스 로직이나 데이터 접근을 몰라도 되고, 화면 렌더링에만 집중할 수 있다.
뷰 : 모델에 담겨있는 데이터를 사용해서 화면을 그리는 일에 집중한다.
컨트롤러에 비지니스 로직을 둘수도 있지만 그렇게 하면 컨트롤러가 너무 많은 역할을 담당한다. 그래서 보통 비지니스 로직은 service라는 계층을 별도로 만들어서 처리한다.
그리고 컨트롤러는 비지니스 로직이 있는 서비스를 호출하는 역할을 담당한다.
이전에는 클라이언트가 비지니스 로직과 뷰 로직이 함께 있는 곳에 호출을 했다.
하지만 mvc패턴을 사용하면 controller을 통해서 비지니스 로직을 수행하고, 결과값(데이터)를 model에 담는다.
그러면 뷰 로직은 모델에 있는 데이터 참조를 하여 뷰로 보여준다.
mvc패턴을 전체적으로 보면 위와 같다.
컨트롤러 로직이 비지니스 로직까지 모두 수행하면 너무 복잡해지므로, 비즈니스 로직과 데이터 접근하는 부분은 서비스, 리포지토리로 분리하고, 그 결과값을 모델을 통해 뷰 로직에 전달한다.
MVC패턴 - 적용
서블릿을 컨트롤러로 사용하고, jsp를 뷰로 사용한다.
모델은 httpservletRequest 객체를 사용한다. request는 내부에 저장소를 대상으로 한다.
request.getAttribute(), request.setAttribute를 통해서 모델에 값을 저장하고, 조회할 수 있다.
회원등록
package hello.servlet.web.servletmvc;
import javax.servlet.RequestDispatcher;
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;
@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);
}
}
사용자가 servlet-mvc/members/new-form으로 접속하면
위 서블릿으로 접근한다. 이 때 RequestDispatcher을 통해서 뷰의 경로를 설정해주고, forward로 다른 jsp로 이동한다.
forward는 다른 서블릿이나 JSP로 이동할 수 있는 기능이고, 서버 내부에서 다시 호출이 발생한다.
/WEB-INF는 이 경로에 jsp가 있으면 외부에서 jsp를 호출할 수 없다. 이 jsp에 접근하려면 오직 컨트롤러를 통해서 접근해야한다.
리다이렉트는 실제 클라이언트에 응답이 나갔다가, 클라이언트가 리다이렉트 경로로 다시 요청한다.
하지만 forward는 서버 내부에서 일어나는 호출이기 때문에 클라이언트가 전혀 인지하지 못한다.
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<meta charset="UTF-8">
<title>회원 등록 폼</title>
</head>
<body>
<!-- 상대경로 사용, [현재 URL이 속한 계층 경로 + /save] -->
<form action="save" method="post">
<div>
username: <input type="text" name="username" />
</div>
<div>
age: <input type="text" name="age" />
</div>
<div>
<button type="submit">전송</button>
</div>
</form>
</body>
</html>
여기서 form action에 "save"라는 상대경로가 들어가있다. 그럼 현재 url이 속한 계층 결로 + save가 호출된다.
회원저장
package hello.servlet.web.servletmvc;
import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
import javax.servlet.RequestDispatcher;
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;
@WebServlet(name = "mvcMemberSaveServlet", urlPatterns = "/servlet-mvc/members/save")
public class MvcMemberSaveServlet extends HttpServlet {
private final 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);
// Model에 데이터를 보관한다.
request.setAttribute("member", member);
String viewPath = "/WEB-INF/views/save-result.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
}
}
여기서도 마찬가지로 디스패쳐와 forward를 사용한다.
다른 점은 여기서는 request로 들어온 값들을 조회해서 저장한 다음 이 결과값을 requset에 저장해서 jsp에 넘긴다.
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<meta charset="UTF-8">
<title>회원 저장 결과</title>
</head>
<body>
<h1>회원 저장 성공</h1>
<ul>
<li>id = ${member.id}</li>
<li>username = ${member.username}</li>
<li>age = ${member.age}</li>
</ul>
<a href="/index.html">메인으로</a>
</body>
</html>
이건 jsp 코드이다. jsp에서는 ${}문법으로 request의 attribute에 담긴 데이터를 편리하게 조회할 수 있게 한다.
회원 목록 조회
package hello.servlet.web.servletmvc;
import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
import javax.servlet.RequestDispatcher;
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.util.List;
@WebServlet(name = "mvcMemberListServlet", urlPatterns = "/servlet-mvc/members")
public class MvcMemberListServlet extends HttpServlet {
private final 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);
}
}
request 객체를 사용해서 List<Member> member 객체를 모델에 보관한다.
`main/webapp/WEB-INF/views/members.jsp` ```html
<%@ 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>
jsp는 위와 같이 list로 된 객체들을 쉽게 다룰 수 있도록 taglib 기능을 제공하낟.
MVC 패턴 - 한계
mvc 패턴을 적용해서 컨트롤러의 역할과 뷰를 렌더링하는 역할을 명확하게 구분할 수 있다.
하지만 컨트롤러를 보면 딱 봐도 중복이 많고, 필요하지 않은 코드들도 많다.
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response); ```
이처럼 뷰로 이동하는 코드가 항상 중복된다.
String viewPath = "/WEB-INF/views/new-form.jsp";
```
또한 jsp가 아니라 다른 뷰로 변경한다면 전체 코드를 다 변경해야한다.
또한 HttpServletRequest request, response 는 사용하지 않을 때가 많다.
또한 이런 코드들은 테스트케이스를 작성하기도 어렵다.
그리고 기능이 복잡해질수록 컨트롤러에서 공통으로 처리해야 할 부분이 증가한다.
공통기능을 메서드로 만들어도 되지만 결과적으로 해당 메서드를 항상 호출해야하고, 만약 호출하지 않으면 문제가 된다.
정리하면 공통처리가 어려워 수문장 역할을 하는 기능이 필요하다. 프론트 컨트롤러 패턴을 도입하면 이런 문제를 깔끔하게 해결할 수 있다.
출처
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-1/dashboard
스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 강의 | 김영한 - 인프런
김영한 | , 원리를 알아야 핵심이 보인다!김영한의 스프링 MVC 기본편 👨💻 📌 수강 전 확인해주세요! 본 강의는 자바 스프링 완전 정복 시리즈의 네 번째 강의입니다. 우아한형제들 최연소
www.inflearn.com