이 글은 스프링 [스프링 MVC 1편]을 듣고 정리한 내용입니다
아주 기본적인 기능인 회원 저장, 회원 목록 조회 기능만 보도록 하겠다.
회원 도메인 모델
package hello.servlet.domain.member;
@Getter
@Setter
public class Member {
private Long id; //Member를 회원 저장소에 저장하면 회원 저장소가 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;
/**
* 동시성 문제가 고려되어 있지 않음, 실무에서는 ConcurrentHashMap, AtomicLong 사용 고려
*/
public class MemberRepository {
private static Map<Long,Member> store = new HashMap<>();
private static long sequence = 0L;
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());
}
public void clearStore(){
store.clear();
}
}
회원 저장소는 싱글톤 패턴을 적용했다.
스프링을 사용하면 스프링 빈으로 등록해서 알아서 싱글톤으로 관리되지만, 지금은 최대한 순수 서블릿 만으로만 구현해보자
회원 저장소 테스트 코드
package hello.servlet.domain.member;
import java.util.List;
import static org.assertj.core.api.Assertions.*;
//Junit5 부터는 public 없어도 됨.
class MemberRepositoryTest {
//싱글톤이므로 new로하면 안됨 (스프링을 쓰면 스프링 자체가 싱글톤을 보장해 주므로 쓸필요 없음)
MemberRepository memberRepository = MemberRepository.getInstance();
@AfterEach //테스트가 끝날때마다 깔끔하게 초기화
void afterEach(){
memberRepository.clearStore(); //이 과정이 없으면 test함수 순서가 보장이 안되기 때문에 각 테스트(함수)가 끝날때마다 afterEach함수가 실행되어 clearStore해준다.
}
@Test
void save(){ //새로 생성한 멤버를 저장하고, 저장한 멤버와 id를 통해 찾은 멤버가 같은지 확인하기
//given
Member member = new Member("hello", 20);
//when
Member saveMember = memberRepository.save(member);
//then
Member findMember = memberRepository.findById(saveMember.getId());
assertThat(findMember).isEqualTo(saveMember);
}
@Test
void findAll(){ //전체 멤버 수는 맞는지, 각각의 멤버들은 포함한거 맞는지 확인
//given
Member member1 = new Member("member1", 20);
Member member2 = new Member("member2", 30);
memberRepository.save(member1);
memberRepository.save(member2);
//given
List<Member> result = memberRepository.findAll();
//then
assertThat(result.size()).isEqualTo(2);
assertThat(result).contains(member1,member2); //result가 member1, member2가지고 있는지 확인
}
}
package hello.servlet.web.servlet;
@WebServlet(name="memberFormServlet", urlPatterns = "/servlet/members/new-form")
public class MemberFormServlet extends HttpServlet {
//싱글톤이므로 new 안된다(생성자를 private으로 막아놨었음) -> 그래서 getInstacne()로 가져온다!
private MemberRepository memberRepository = MemberRepository.getInstance();
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.setContentType("text/html");
response.setCharacterEncoding("utf-8");
//서블릿으로 하면, 다음과같이 자바코드로 html코드를 다 더해야 하기때문에 굉장히 불편
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을 직접 작성해야 하므로 매우 불편한다.
package hello.servlet.web.servlet;
@WebServlet(name="memberServlet", 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")); //request.getParameter한 값은 문자타입이므로 int형으로 변환
Member member = new Member(username, age);
memberRepository.save(member);
//멤버 저장이 잘됐는지 확인하기 위해, 응답을 html코드로 내려보자
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>");
}
}
MemberSaveServlet의 동작순서
1. 파라미터를 조회해서 Member객체를 만든다.
2. Member 객체를 MemberRepository를 통해서 저장한다
3. Member 객체를 사용해서 결과 화면용 HTML을 동적으로 만들어서 응답한다.
package hello.servlet.web.servlet;
@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>");
}
}
MemberListServlet의 동작과정
1.memberRepository.findAll()을 통해 모든 회원을 조회한다.
2.회원 목록 HTML을 for루프를 통해서 회원수 만큼 동적으로 생성하고 응답한다.
*JSP 참고
- JSP는 성능과 기능 면에서 점점 안쓰는 추세이다. 요즘은 스프링과 잘 통합되는 Thymeleaf를 쓴다!!
//JSP 추가 시작
implementation 'org.apache.tomcat.embed:tomcat-embed-jasper'
implementation 'javax.servlet:jstl'
//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>
실행 url : http://localhost:8080/jsp/members/new-form.jsp
실행시 .jsp
까지 함께 적어야 한다.
main/webapp/jsp/members/save.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="hello.servlet.domain.member.Member" %> //자바의 import문과 같다.
<%@ page import="hello.servlet.domain.member.MemberRepository" %>
<% //이 부분에는 자바코드를 입력할 수 있다.
//request, response 그냥 사용 가능 (jsp에서 문법상 지원이 된다)
MemberRepository memberRepository = MemberRepository.getInstance();
System.out.println("MemberSaveServlet.service");
String username = request.getParameter("username");
int age = Integer.parseInt(request.getParameter("age")); //request.getParameter한 값은 문자타입이므로 int형으로 변환
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()%></>
</ul>
<a href="/index.html">메인</a>
</body>
</html>
<% ~~%>
:이 부분에서는 자바코드를 입력할 수 있다.
<%= ~~ %>
: 이 부분에서는 자바코드를 출력할 수 있다.
<%@ 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>
<a href="/index.html">메인</a>
<table>
<thead>
<th>id</th>
<th>username</th>
<th>age</th>
</thead>
<tbody>
<%
for (Member member : members){ 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의 경우, HTML작업이 깔끔하고 중간중간 동적 변경이 필요한 부분만 자바코드를 적용할 수 있어서 조금더 편리하였다.
그러나, JSP에서 회원을 저장하는 비즈니스로직, 결과를 보여주는 html등 너무 많은 역할을 한다. -> 요구사항이 복잡해지면 지옥같은 코드가 될것이다.
*참고
- 컨트롤러에 비즈니스 로직을 둘 수도 있지만, 이것은 컨트롤러가 너무 많은 역할을 담당하게 된다.
- 일반적으로 비즈니스 로직은 서비스(Service) 계층을 변도로 만들어서 처리한다
- 그리고 컨트롤러는 비즈니스 로직이 있는 서비스를 호출하는 역할을 한다.
- 비즈니스 로직을 변경하면, 비즈니스 로직을 호출하는 컨트롤러의 코드 도 변경될 수 있음을 알고있자!!
request.setAttribute()
, request.getAttribute()
를사용하면 데이터를 보관하고, 조회할 수 있다. 회원 등록 폼 - 컨트롤러
package hello.servlet.web.servletmvc;
@WebServlet(name="mvcMemverFormServlet", 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 호출
}
}
dispatcher.forward()
: 다른 서블릿이나 JSP로 이동할 수 있는 기능. 서버 내부에서 다시 호출이 발생한다.
WEB-INF
- 이 경로안에 JSP가 있으면 외부에서 직접 JSP를 호출할 수 없다. 컨트롤러를 통해서 JSP를 호출하기 위해 사용한 폴더.(관례적이다)
- Redirect vs Forward
- 리다이렉트: 실제 클라이언트(웹 브라우저)에 응답이 나갔다가, 클라이언트가 redirect경로로 다시 요청하므로, 클라이언트가 인지할 수 있고 URL 경로도 실제로 변경된다
- 포워드: 서버 내부에서 일어나는 호출이므로 클라이언트가 전혀 알지 못한다.
회원 등록 폼 - 뷰
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<%--코드 재활용하기 위해 상대 경로 사용, [현재 URL이 속한 계층 경로 + /save] --%>
<%--이렇게 /(슬래쉬) 없이 쓰면 알아서 [현재 경로 + /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>
회원 저장 - 컨트롤러
package hello.servlet.web.servletmvc;
@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); //requset객체의 내부 저장소에 저장함. (map같은것들이 있음)
String viewPath = "/WEB-INF/views/save-result.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request,response);
}
}
HttpServletRequest
를 Model로 사용한다setAttribute()
를 사용하면 request객체에 데이터를 보관해서 뷰에 전달할 수 있다.request.getAttribute()
를 사용해서 데이터를 꺼내면 된다.회원저장 - 뷰
<%@ 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>
${}
문법을 재공해서 이것을 통해 request의 attribute에 담긴 데이터를 편하게 조회할 수 있다.회원 목록 조회 - 컨트롤러
package hello.servlet.web.servletmvc;
@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); //request 객체를 사용하여 List<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>
<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>
모델에 담아둔 members를 JSP가 제공하는 taglib
기능을 사용하여 반복하면서 출력하였다.
<c:forEach>
이 기능을 사용하려면 다음과 같이 선언해야 한다.
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
JSP는 요즘 거의 사용하지 않으므로, 이런게 있구나 하고 넘어가자!!
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
String viewPath = "/WEB-INF/views/new-form.jsp";
사용하지 않는 코드
다음 코드를 사용할 때도 있고, 사용하지 않는 코드도 있다.
HttpServlet request, HttpServletResponse respone
공통 처리가 어렵다