728x90
1. domain, repository
- 도메인, 리포지토리 생성
- repository로 바로 service역할까지 수행
package hello.servlet,domain.member;
// 회원 도메인 모델 = entity model
@Getter @Setter
public class Member {
private Long id; //repository에서 id 자동으로 줌
// 생성자 주입으로 받음
private String username;
pirvate int age;
public Member(){};
public Member(String username, int age){
this.username = username;
this.age = age;
}
}
- public Member findAll(){ return new ArrayList<>(Store.values()); }
-> new ArrayList를 반환해서 실제 store를 보호
package hello.servlet.domain.member;
// 회원 저장소 repository 생성
public class MemberRepository{
// 저장소 map {key, value} = {member.id , member 객체}
private static Map<Long, Member> store = new HashMap<>;
private static Long sequence = 0L;
// 1. singleton 객체 생성
private static final MemberREpository instance = new MemberRepository();
public static MemberRepository getInstance() { return instance; }
// 2. singleton - 외부에서 객체 생성 못하도록 함
private MemberRepository(){}
public Member save(Member member){
member.setId(sequence++);
store.put(member.getId(), member);
return member;
}
public Member findById(Long id){ store.get(id); }
// new ArrayList를 반환해서 실제 store를 보호
public Member findAll(){ return new ArrayList<>(Store.values()); }
public void clearStore(){ store.clear(); }
}
Test code - 개인 연습용
더보기
public class MemberRepositoryTest {
MemberRepository memberRepository = MemberRepository.getInstance();
@AfterEach
void afterEach(){ memberRepository.clearStore();}
@Test
void save(){
//given
Member member = new Member("woowee", "24");
//when
Member savedMember = memberRepository.save(member);
//then
Member findMember = memberRepository.findById(savedMember.getId());
Assertions.assertThat(findMember).isEqualTo(savedMember);
}
@Test
void findAll(){
//given
Member member1 = new Member("1",12);
Member member2 = new Member("2",12);
memberRepository.save(member1)
memberRepository.save(member2)
//when
List<Member> result = memberRepository.findAll();
//then
Assertions.assertThat(result.size()).equalTo(2);
Assertions.assertThat(result).contains(member1,member2) // contains()
}
}
2. 서블릿으로 회원 관리 웹 애플리케이션 만들기
2.1. 회원 등록 form 생성
@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();
// servlet에서 직접 html code 생성
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");
}
}
2.2. 회원 저장 code 생성
- 2.1. 회원 등록 form 생성으로부터 온 요청 data는 queary params의 형태를 가지고 있어서 queary method() 사용가능
- getParameter(): string으로만 반환, int가 필요시 변환 필요
- servlet 코드로부터 html 생성하는 것이여서 java code를 이용한 동적 html 생성가능
@WebServlet(name = "memberSaveServlet", urlPatterns = "/servlet/members/save")
pubic class MemberSaveServlet extends HttpServlet {
// singleton으로 공유되는 repository
private MemberRepository memberRepository = MemberRepository.getInstance();
@Override
private void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException{
// 필요 request data 추출
// form data도 요청으로 올때 queary params로 오기 때문에 queary method 사용 가능
String username = requset.getParameter("username");
int age = Integer.parseInt(request.getParameter("age")); // getParameter은 string으로만 반환되므로 int로 변경 필요
// request data 저장
Member member = new Member(username, age);
memberRepository.save(member);
// 응답 data header 준비
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>");
}
}
2.3. 회원 전체 목록 생성
@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>");
}
}
2.4. servlet 정리
- 동적 html을 만들수 있는 장점이 있지만 html코드 생성이 매우 복잡하고 비효율적
- 자바코드로 html 만드느 것보다 html 문서 내에 동적으로 변경해야 되는 부분만 자바 코드 넣는 것이 편리
- 위의 문제 해결을 위해 template engine이 존재
ex) JSP, Thymeleaf, Freemarker, Velocity 등
3. JSP로 회원 관리 웹 애플리케이션 만들기
- 참고
- jsp: 점점 사향하는 추세(springboot에선 권장하지 않음)
- spring은 thymeleaf를 밀고 있는 추세이므로 이제 template engine을 접하는 사람들은 thymeleaf를 사용하는 것을 추천
- templete engine
- 원래 response 응답을 보낼때 response 헤더정보들과 getWriter()를 이용한 http body data를 추가해줘야한다.
- templete engine 사용 시 html 내에 header 정보를 넣을 수 있고 html 코드들이 getWriter() 내부에 들어가서 http body data에 들어간다. 그래서 따로 response.xxx 를 사용하지 않는다.
3.1. 설정
- library 추가(springboot 3.0 이상)
// build.gradle
// dependencies{} 내부에 작성
dependencies{
//JSP 추가 시작
implementation 'org.apache.tomcat.embed:tomcat-embed-jasper'
implementation 'jakarta.servlet:jakarta.servlet-api' //스프링부트 3.0 이상
implementation 'jakarta.servlet.jsp.jstl:jakarta.servlet.jsp.jstl-api' //스프링부트 3.0 이상
implementation 'org.glassfish.web:jakarta.servlet.jsp.jstl' //스프링부트 3.0 이상
//JSP 추가 끝
}
- webapp 하위에 .jsp 생성
- webapp - jsp - members - new-form.jsp 생성
- servlet과 달리 따로 요청 url을 작성하지 않음
- http://localhost:8080/jsp/members/new-form.jsp(webapp 파일경로와 동일)으로 요청 오면 해당 file의 내용을 응답으로 보내줌
- webapp에 jsp 넣는 이유
- template engine이기 때문에 webapp에 넣어줘야함
- webapp이 root 시작 위치이다.
from action을 보면 /members/~~ 이렇게 작성을 하면 프로젝트 파일로 봤을때 webapp/members/~~ 와 같은 의미
3.2. 회원 등록 form 생성
- <%@ page contentType="text/html;charset=UTF-8" language="java" %> : jsp file이란 의미. 필수값
<%@ 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>
3.3. 회원 저장 code 생성
- <%@ page import= %>: 자바의 import 문
- <% ~~ %>: java code 입력가능
- <%= ~~ %>: java code 출력가능
- HttpServlet 의 request, response: 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 사용 가능: jsp에서 지원해줌 --%>
<%
MemberRepository memberRepository = MemberRepository.getInstance();
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 시작 --%>
<html>
<head>
<title>Title</title>
<meta charset="UTF-8">
</head>
<body>
success
<ul>
<li>id=<%=member.getId()%></li>
<li>id=<%=member.getUsername()%></li>
<li>id=<%=member.getAge()%></li>
</ul>
<a href="/index.html">메인</a>
</body>
</html>
3.4. 회원 전체 목록 생성
- out: getWriter() 같은 예약어
<%@ 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 시작 --%>
<html>
<head>
<title>Title</title>
<meta charset="UTF-8">
</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>
3.5. jsp 정리
- servlet보다는 확실히 정리된 느낌이긴 하지만 JSP가 너무 많은 역할을 한다.
java code, 데이터 조회, html 출력 - 비즈니스 로직은 서블릿이, html은 template engine이 가각 분업을 통한 집중을 하는 것이 좋다.
이를 실현한 것이 MVC 패턴이다.
4. MVC 패턴
4.1. 개요
- 서블릿, jsp만으로 모든 작업을 수행시 유지보수가 어려워진다.
- 중요
- 분리는 변경 주기(변경의 라이프 사이클)가 다를 경우 분리한다를 기준으로 둔다.
- ex) UI 변경하는 일과 비즈니스 로직 변경하는 일은 서로 따로 따로 일어난다.
- Model View Controller (참고. spring mvc 패턴으로 설명되어 있다.)
- 실제 mvc 패턴 링크
- 컨트롤러
- HTTP 요청을 받아서 파라미터를 검증하고, 비즈니스 로직을 실행
- 비즈니스 로직은 원래 service나 domain에서 처리 → repository에 저장 (요 덩어리들을 model이라고 한다.)
- 현재는 실습이여서 controller에서 service logic 처리를 하고 domain에 저장
- 뷰에 전달할 결과 데이터를 조회해서 모델에 담는다.
- HTTP 요청을 받아서 파라미터를 검증하고, 비즈니스 로직을 실행
- 모델
- mvc 패턴의 추상적인 개념인 model이 아닌 spring web에서 말하는 구체적인 model을 의미
- 데이터를 이동시켜주는 박스 정도로 생각하면 된다.
- 뷰에 출력할 데이터를 담는다.
- 뷰가 필요한 데이터를 모두 모델에 담아서 전달해주는 덕분에 뷰는 비즈니스 로직이나 데이터 접근을 몰라도 되고, 화면을 렌더링 하는 일에 집중
= 말이 모델이 하는거지 크게 보면 controller 가 view와 model(추상개념인 domain 의미)을 분리시켜준다.
- 뷰
- 모델에 담겨있는 데이터를 사용해서 화면을 그리는 일에 집중
- HTML을 생성(xml, excel 도 가능)하는 부분을 의미
- 컨트롤러
4.2. MVC 패턴 적용
- servlet: controller로 사용
- jsp: view로 사용
- request 임사공간 : model로 사용
- 구체 내용
- Model : HttpServletRequest 객체 사용 - request 내부에 데이터 저장소가 존재
- request.setAttribute(): 데이터 보관
- request.getAttribute(): 데이터 조회
- /WEB-INF: was의 규칙
- 해당 경로 내에 jsp가 존재시 외부에서 직접 jsp 호출이 불가
- 항상 controller를 통해서 jsp 호출
- RequestDispatcher
- controller servlet의 request, response를 다른 source code에서도 (=jsp 파일) 동일하게 사용할 수 있도록 해주는 class
- getRequsetDispatcher(path) : RequestDispatcher class로 포장된 dispatcher 객체를 얻을 수 있는 기능.
- request.getRequestDispatcher(viewPath);
request 내부에 path를 가진 dispatcher를 담는다. - 객체바인딩으로 만들어진 객체.
- 객체바인딩: 개발자의 코드가 아닌 데이터로 객체를 만들어내는 과정
- 해당 viewPath에서 현 page의 req, res를 사용하겠다는 의미
- request.getRequestDispatcher(viewPath);
- dispatcher.forward(req, res)
- request에 dispatcher의 path정보가 들어 있다.
- 더 나아가서 request에 model을 담으면 해당 model도 넘어가진다.
- 사용할 request, response를 getRequsetDispatcher(path)의 path로 보내주는 method
- 포워딩은 다른 서블릿이나 JSP로 이동할 수 있는 기능. 서버 내부에서 재호출이 발생
- controller servlet의 request, response를 다른 source code에서도 (=jsp 파일) 동일하게 사용할 수 있도록 해주는 class
- Model : HttpServletRequest 객체 사용 - request 내부에 데이터 저장소가 존재
- forward, redirect 차이점
- redirect: 2번의 요청과 응답으로 url이 변경
- forward: 서버 내부적으로 servlet -> jsp로 변경되는 것으로 url이 변경되지 않는다.
4.3. 회원 등록 form 생성
- 현재 보낼 logic이 없어도 controller -> view로 가는 mvc 규칙을 지킴
package hello.servlet.web.servletmvc;
// controller
@WebServlet(name= "mcvMemberFormServlet", urlPatterns= "/servlet-mvc/members/new-form")
public class McvMemberFormServlet extends HttpServlet{
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String viewPath = "/WEB-INF/views/new-form.jsp";
// 해당 경로로 가는 RequestDispatcher class 의 객체 만들기 - 그래야 포워딩 기능을 수행할 수 있기 때문
RequestDispatcher dispatcher = requeset.getRequestDispatcher(viewPath);
dispatcher.forward(requset, response);
}
- 상대 경로 사용
- /servlet-mvc/members/new-form -> /servlet-mvc/members/save
- 절대 경로 사용
- /servlet-mvc/members/new-form -> /servlet-mvc/members/new-form/save
// view
<form action="save" method="post">
username: <input type="text" name="username" />
age: <input type="text" name="age" />
<button type="submit">전송</button>
4.4. 회원 저장 code 생성
- 원래 받은 request에 Attribute 추가(member) -> dispatcher 추가 -> forward
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 {
//Controller
String username = request.getParameter("username");
int age = Integer.parseInt(reqeust.getParameter("age"));
Member member = new Member(username, age);
memberRepository.save(member);
//Model
request.setAttribute("member", member); // {key, value}
//View
String viewPath = "/WEB-INF/views/save-result.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request.response);
}
- setAttribute로 저장한 member을 view에서 사용
- ${~~} : view에 특화된 logic, Model로 부터 받은 member을 바로 쓸 수 있다.
//view
<ul>
<li>id=${member.id}</li>
<li>username=${member.username}</li>
<li>age=${member.age}</li>
</ul>
4.5. 회원 전체 목록 생성
- <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>: jsp file이라는 뜻
- for문을 더 간략하게 해줌
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 {
List<Member> members = memberRepository.findAll();
// model 담기
request.setAttribute("members", members);
String viewPath = "/WEB-INF/views/members.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request,response);
}
- ${} : property 접근법, view에 특화된 logic, model로부터 받은 member를 바로 꺼내서 사용 가능
//view
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<html>
</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>
</html>
4.6. mvc 패턴 정리
- 단점
- forward 중복, ViewPath 중복, 사용하지 않는 코드 == 공통처리가 어렵다.
- 해결방안
- 프론트 컨트롤러 패턴 사용: controller 호출전에 먼저 공통 기능을 처리하는 역할
- jsp 정리
- <% %> : java code 작성란, request, response 사용 가능
- <%= %> : <% %>에서 생성한 변수 사용
- <%-- --%> : 주석
- ${} : servlet에서 전달한 변수 사용
- <c:forEach> </c:forEach> : 반복문
이전 발행글 : 스프링 MVC 1 - 서블릿
다음 발행글 : 스프링 MVC 3 - MVC 프레임워크 만들기
출처 인프런 김영한의 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술