[Spring MVC 1] - 회원 관리 웹 어플리케이션 Servlet으로 구현
회원 관리 웹 어플리케이션 요구사항
- 회원정보
- 이름 : username
- 나이 : age
- 기능 요구사항
- 회원 저장
- 회원 목록 조회
본격적으로 서블릿으로 회원 관리 웹 어플리케이션을 만들어 보자
회원 도메인 모델
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;
Member(){
}
public Member(String username, int age){
this.username = username;
this.age = age;
}
}
위의 패키지 경로에 MembeClass를 만들고, 멤버변수와 기본생성자, usernamer과 age를 받는 생성자를 만든다.
- id는 Member를 회원 저장소에 저장하면 회원 저장소가 할당한다.
회원 저장소
package hello.servlet.domain.member;
import java.util.ArrayList;
import java.util.List;
import java.util.HashMap;
import java.util.Map;
public class MemberRepository {
//static을 써서 아무리 많아도 딱 한번만 생성됨 -싱글톤이라 안써도 되는데 일단 함
private static Map<Long, Member> store = new HashMap<>();
private static long sequence = 0L; // id 증가용
//싱글톤으로 만들 것임. 왜? 현재는 스프링을 톰캣 띄울때만 쓰고 있고 안쓰기 떄문에
private static final MemberRepository instance = new MemberRepository();
//무조건 아래의 메소드로 조회하도록!!
public static MemberRepository getInstance(){
return instance;
}
//싱글톤을 만들 때는 아무나 생성하지 못하도록 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(){
// store에 있는 모든 value를 꺼내 새로운 array에 담아준다. 그렇게 하는 이유는 arraylist를 조작해도 store에 있는 value 보호 목적
return new ArrayList<>(store.values());
}
public void clearStore(){
store.clear();
}
}
- 회원 저장소는 싱글톤 패턴을 적용했다. 스프링을 사용하면 스프링 빈으로 등록하면 되지만, 지금은 최대한 스프링 없이 순수 서블릿 만으로 구현하는 것이 목적이다. 싱글톤 패턴은 객체를 단 하나만 생성해서 공유해야 하므로 생성자를 private 접근자로 막아둔다.
회원 저장소 테스트 코드
package hello.servlet.domain.member;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import java.util.List;
class MemberRepositoryTest {
MemberRepository memberRepository = MemberRepository.getInstance();
@AfterEach
void afterEach(){
memberRepository.clearStore();
}
@Test
void save(){
//given 이런게 주어졌을 때
Member member = new Member("rosieposie",22);
//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("member1", 20);
Member member2 = new Member("member2", 30);
memberRepository.save(member1);
memberRepository.save(member2);
//when
List<Member> result = memberRepository.findAll();
//then
Assertions.assertThat(result.size()).isEqualTo(2); // 리스트의 사이즈가 2인가?
Assertions.assertThat(result).contains(member1, member2); // 리스트에 member1, member2가 포함되어 있는가?
}
}
- 회원을 저장하고, 목록을 조회하는 테스트를 작성했다. 각 테스트가 끝날 때, 다음 테스트에 영향을 주지 않도록 각 테스트의 저장소를 clearStore()를 호출하여 초기화한다.
서블릿으로 회원 관리 웹 어플리케이션 만들기
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 {
//싱글톤이라서 생성자로 막아놓았기 때문에 new MemberRepository() 안 됨
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");
}
}
localhost:8080/servlet/members/new-form 실행
- MemberFormServlet은 단순하게 회원 정보를 입력할 수 있는 HTML Form을 만들어서 응답한다. 자바코드로 HTML을 제공해야 하므로 불편하다
- HTM Form 데이터를 POST로 전송해도, 전달 받는 서블릿을 아직 만들지 않았다. 그래서 오류가 발생한다.
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 {
System.out.println("MemberSaveServlet.service");
String username = request.getParameter("username");
int age = Integer.parseInt(request.getParameter("age")); // request의 getParameter 결과는 항상 문자! 그러므로 타입을 변환해 주어야 한다
Member member = new Member(username,age);
memberRepository.save(member);
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"+
"성공\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>\n");
}
}
localhost:8080/servlet/members/new-form 실행하고 회원 정보 입력하면 데이터가 전송되고, 저장 결과를 확인할 수 있다.
MemberSaveServlet은 다음과 같은 순서대로 동작한다.
- 파라미터를 조회해서 Member 객체를 만든다.
- Member 객체를 MemberRepository를 통해 저장한다.
- Member 객체를 사용해서 화면 결과용 HTML을 동적으로 만들어 응답한다.
MemberListServlet - 회원 목록
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("<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>");
}
}
localhost:8080/servlet/members 를 실행하면 저장된 회원 목록을 확인할 수 있다.
MemberListServlet은 다음과 같은 순서대로 동작한다.
- memberRepository.findAll()을 통해 모든 회원을 조회한다.
- 회원 목록 HTML을 for 루프를 통해 회원 수 만큼 동적으로 생성하고 응답한다.
템플릿 엔진의 등장 배경
지금까지 서블릿과 자바 코드만으로 HTML을 만들어보았는데 서블릿 덕분에 동적으로 원하는 HTML을 만들 수 있었다.
정적인 HTML문서라면 화면이 계속 달라지는 회원의 저장 결과라던지 회원 목록과 같은 동적인 HTML 생성은 불가능 하다.
그런데 이 방법은 매우 비효율적이므로 이런 연유에서 템플릿 엔진이 등장하게 되었다. 템플릿 엔진을 사용하면 HTML 문서에서 필요한 곳만 코드를 적용하여 동적으로 변경할 수 있다.
템플릿 엔진에는 JSP, Thymeleaf, Freemarker, Velocity 등이 있다.
직접 작성해 보고 느낀 점은 옛날 개발자 대단하다 였다.. 진짜 저거 하나 잘못 작성하면 찾을 수도 없고 그때부터 모래사장에서 바늘찾기가 되는 것이다. 진짜 괜히 라떼는 말이야~ 이런 말이 나올 수밖에 없는 ㅋㅋㅋㅋ납득이 가는 상황 중 하나다... 기술의 발전을 응원합니다. 템플릿 엔진을 개발한 사람들은 그 문제를 해결하기 위해 내놓은 대책이겠지? 개발이 아니라 역사를 배우는 기분이다 개발역사란 이런 것. 짱 잼.
@ 스프링 MVC 1편 - 백엔드 웹개발 by 김영한을 참조하고 있습니다.