스프링 공부 (인프런 김영한 선생님)/스프링 MVC 1편

[스프링 웹 MVC 1편] 7. 서블릿 구현

2023. 5. 21. 17:47

서블릿에서 JSP로 JSP의 한계에서 MVC 패턴으로 점진적으로 발전한 과정을 알아보자

 

먼저 실습을 위해 다음 요구사항들을 적용했다

 

1. 멤버 도메인 클래스

package hello.servlet.domain.member;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class Member {

    private Long id; // DB에 저장할때 발급
    private String username;
    private int age;

    public Member(String username, int age) {
        this.username = username;
        this.age = age;
    }

    public Member() {
    }
}

id는 Member를 회원 저장소에 저장하면 회원 저장소에 의해 할당된다.

자바 접근자, 설정자를 메서드 구현하는 대신 @Getter와 @Setter를 적용했다 (Lombok Library)

 


2. 회원 저장소

package hello.servlet.domain.member;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

// 프로젝트가 크면 repository 패키지 따로 생성/ 작은 프로젝트라 안한다.
public class MemberRepository {
    private static Map<Long, Member> store = new HashMap<>(); // 참고로 AtomicMap, AtomicLong 만 사용하자

    private static long sequence = 0L;

    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());
        // 이렇게 생성해주는 이유? -> store에 있는 모든 value를 다 꺼내서 새로운 ArrayList를 만들어줌, 이때 이 값을 변경해도 store의 값에는 영향이 없음.
        // store 자체를 보호하기 위해 사용한 방법임.
    }

    public void clearStore(){
        store.clear(); // 테스트 코드에서 사용
    }
}

먼저 크기가 큰 프로젝트라면 마땅히 Repository 패키지를 생성하여 분리해주는게 맞으나, 실습 편의상 member 패키지 안에 구현한다.

 

Map을 통해 Member들을 저장하는데 이때 Key, Value => Long, Member로 갖는다.

스프링 컨테이너를 이용한다면, 객체를 생성해도 DI 컨테이너에 의해 싱글턴 객체가 보장되지만,

현재 여기서는 Spring Bean을 이용하지 않고 Servlet을 이용하고 있기 때문에 직접 싱글턴을 보장해야 할 필요가 있다.

 

따라서 static final을 통해, 단 하나의 MemberRepository 객체를 공유하는 것을 보장하고

객체 인수(Receive)는 getInstance() 메서드를 통해 받아오며, 생성자의 접근자를 private으로 제한함으로써 추가 객체 생성을 막는다.

 

멤버의 저장과 Id를 기준으로 한 검색, 전체 검색 메서드인 save, findById, findAll() 메서드를 구현했는데

이때 findAll()에서 new ArrayList로 새로운 리스트를 생성한 것은 검색 결과에 영향을 주는 코드를 작성하더라도

원본 저장소 Map은 영향을 받지 않게 하기 위함이다

 

clearStore()의 경우 테스트 코드 사용시 저장소 초기화를 위해 사용한다.


3. 회원 저장소를 테스트하기 위한 테스트 코드

package hello.servlet.domain.member;

import static org.junit.jupiter.api.Assertions.*;

import java.util.List;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;

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());
        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);
    }

}

@AfterEach Annotation 뒤에 작성된 메서드는 각각의 테스트 메서드 실행 이후 실행된다.

주어진 코드의 clearStore() 메서드는 save(), findAll() 메서드의 실행 이후 실행되며

각각의 테스트 메서드 실행 도중, 메모리에 등록되는 Member 변수들(명확히 하자면 Map 저장소)을 다음 테스트 실행을 위해 초기화한다.

 


본격적인 서블릿을 이용한 웹 애플리케이션 만들기

 

본격적으로 서블릿을 이용한 회원 관리 웹 애플리케이션을 만들어보도록 하자.

package hello.servlet.web.servlet;

import hello.servlet.domain.member.MemberRepository;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet(name = "memberFormServlet", urlPatterns = "/servlet/members/new-form")
public class MemberFormServlet extends HttpServlet {

    MemberRepository memberRepository = MemberRepository.getInstance();

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {
        // 응답에 관한 내용만 필요(HTML이 나가야함)

        // Content Body를 잡아줘야 함.

        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을 전부 짜주는거 상당히 불편함
        // /save가 아직 구현되지 않았기 때문에 whitelabelerror 발생이 정상임.
        // 개발자 도구에서 form data를 확인하면 제대로 나가는지 확인할 수 있음.


    }
}

MemberFormServlet은 단순하게 회원정보를 입력할 수 있는 HTML Form을 만들어서 입력한다.

자바 코드로 HTML을 제공해야하기 때문에 쉽지 않다, 뷰와 자바 로직이 한꺼번에 담겨있어서 둘중 하나를 수정할때,

실수로 다른 코드까지 임의적으로 건들여버릴 수 있는 리스크가 존재한다.

 

이 서블릿만 구현하면 submit 버튼을 눌렀을때, whitelabelerror가 발생하는데 당연하다.

form의 action에서 버튼을 누르면 가야하는 /save 서블릿을 아직 구현하지 않았기 때문이다.

 

 

package hello.servlet.web.servlet;

import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@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");
        // FORM DATA 읽어오기

        String username = request.getParameter("username");
        int age = Integer.parseInt(request.getParameter("age"));

        Member member = new Member(username, age);
        memberRepository.save(member);

        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은 먼저 request.getParameter를 이용해 일전의 MemberFormServlet에서 form에 의해 전송된 파라미터를 파싱한다, 이때 Parameter의 경우 모두 문자열로 오고가기 때문에 정수열로 받아져야 하는 age 파라미터의 경우 .parseInt() 메서드를 이용하여 정수화해준다.

 

그리고 받아온 Parameter를 통해 Member 객체를 생성하고, 이를 MemberRepository를 통해 저장한다.

 

마지막으로 getWriter를 이용하여, Member 객체를 사용해서 결과화면용 HTML 페이지를 동적으로 생성한다.

 

마지막으로 등록된 MemberList를 HTML 페이지에 동적으로 생성하는 MemberListServlet을 구현하겠다.

package hello.servlet.web.servlet;

import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@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();

        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>");
    }

}

단순히 MemberRepository.findAll() 메서드를 이용하여 Map 내에 존재하는 모든 멤버 변수를 List에 담고

getWriter()에서 write를 통해 그리고 iteration을 통해 객체 인스턴스들을 조회하고 동적으로 HTML을 만들어낼 수 있다.

 


위 코드들만 봐도 알 수 있지만,

자바 로직 안에 뷰 렌더링이 같이 포함되어있다, 이러면 디버깅에서 어려움이 생기기 때문에 템플릿 엔진이 등장한다.

템플릿 엔진을 사용하면 HTML 문서에서 필요한곳만 코드를 적용해서 동적으로 변경할 수 있다.

'스프링 공부 (인프런 김영한 선생님) > 스프링 MVC 1편' 카테고리의 다른 글

[스프링 웹 MVC 1편] 9. MVC 패턴 - 개요  (0) 2023.05.22
[스프링 웹 MVC 1편] 8. JSP  (0) 2023.05.21
[스프링 웹 MVC 1편] 6. HTTPServletResponse - 기본 사용법  (0) 2023.05.20
[스프링 웹 MVC 1편] 6. HTTP 요청 데이터  (0) 2023.05.20
[스프링 웹 MVC 1편] 5. HttpServletRequest - 개요/기본사용법  (0) 2023.05.19
'스프링 공부 (인프런 김영한 선생님)/스프링 MVC 1편' 카테고리의 다른 글
  • [스프링 웹 MVC 1편] 9. MVC 패턴 - 개요
  • [스프링 웹 MVC 1편] 8. JSP
  • [스프링 웹 MVC 1편] 6. HTTPServletResponse - 기본 사용법
  • [스프링 웹 MVC 1편] 6. HTTP 요청 데이터
ProgYun.
ProgYun.
인내, 일관성, 그리고 꾸준함을 담습니다.
Perseverance, Consistency, Continuity인내, 일관성, 그리고 꾸준함을 담습니다.
ProgYun.
Perseverance, Consistency, Continuity
ProgYun.
전체
오늘
어제
  • 분류 전체보기
    • 칼럼
    • 일상생활
      • 월별 회고
      • 인생 이야기 (대학생활)
      • 취준
      • 운동인증
      • 제품 사용 후기와 추천
    • 스프링 공부 (인프런 김영한 선생님)
      • 스프링 핵심원리
      • 스프링 MVC 1편
      • 스프링 MVC 2편
    • 면접 준비
    • 전공
      • OOP 정리
      • Design Pattern
    • 스터디
    • English
      • Electronics(Laptop)
      • 1일 1단어

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • 윈도우10
  • 일상
  • mason
  • 자가격리
  • 포맷
  • 피로그래밍
  • ssd
  • 컴공
  • 편입생
  • p31
  • 하이닉스
  • 윈도우재설치
  • NVME
  • 확진자
  • 대학생
  • 해외직구
  • 오미크론
  • 코로나19
  • 자존감
  • 코로나

최근 댓글

최근 글

hELLO · Designed By 정상우.
ProgYun.
[스프링 웹 MVC 1편] 7. 서블릿 구현
상단으로

티스토리툴바

개인정보

  • 티스토리 홈
  • 포럼
  • 로그인

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.