Servlet, JSP, MVC 패턴 기반으로 회원 관리 앱 만들기

Business Requirements

Member Class

@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;
    }
}

멤버 클래스의 id는 sequence를 통해 자동적으로 할당받도록 설정

Member Repository

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 생성자 생성해서 싱글톤 상태로 유지
    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();
    }
}

일단은 메모리에 저장하기 위해 Map 활용, sequence 변수를 둬서 멤버를 추가할때 자동으로 증가한다. 또한, private 생성자를 활용해 싱글톤 객체로 생성해서 여러 곳에 공유할 수 있도록 한다.

MemberRepository Test

class MemberRepositoryTest {
    MemberRepository memberRepository=MemberRepository.getInstance();

    //각각의 테스트를 수행하고 나면 깔끔하게 memory를 비운다.
    @AfterEach
    void afterEach(){
        memberRepository.clearStore();
    }
    ...

Save Test

//저장 관련 기능 테스트
@Test
void save(){
    //given
    Member member=new Member("hello",20);
    //when
    Member saved=memberRepository.save(member);
    //then
    assertThat(saved).isEqualTo(member);
}

Find Test

//저장되어 있는 모든 멤버 조회
@Test
void findAll(){
    //given
    Member member1=new Member("hello1",20);
    Member member2=new Member("hello2",30);

    //when

    Member saved1=memberRepository.save(member1);
    Member saved2=memberRepository.save(member2);

    //then
    List<Member> results= memberRepository.findAll();
    assertThat(results.size()).isEqualTo(2);
    assertThat(results).contains(member1,member2);
}

Application using only servlets

회원 등록 폼

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


    }
}

/servlet/members/new-form 으로 접속하게 되면 아래의 html-form 객체가 유저에게 보이게 된다.

<html>
    <head>
        <title>Title</title>
    </head>
        <body>
            <form action="/servlet/members/save" method="post">
                username: <input type="text" name="username" />
                age: <input type="text" name="age" />
                <button type="submit">전송</button>
            </form>
        </body>
</html>

회원 등록을 처리하는 servlet

@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 newMember=new Member();
        newMember.setUsername(username);
        newMember.setAge(age);

        memberRepository.save(newMember);

        response.setContentType("text/html");
        response.setCharacterEncoding("utf-8");
        PrintWriter writer=response.getWriter();
        writer.write("<html>\n" +
                "<head>\n" +
                " <meta charset=\"UTF-8\">\n" +
                "</head>\n" +
                "<body>\n" +
                "성공\n" +
                "<ul>\n" +
                " <li>id="+newMember.getId()+"</li>\n" +
                " <li>username="+newMember.getUsername()+"</li>\n" +
                " <li>age="+newMember.getAge()+"</li>\n" +
                "</ul>\n" +
                "<a href=\"/index.html\">메인</a>\n" +
                "</body>\n" +
                "</html>");

    }

}

Request query parameters

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

request 객체를 이용해서 query parameter을 가져와서

Save in Members

Member newMember=new Member();
newMember.setUsername(username);
newMember.setAge(age);

memberRepository.save(newMember);

MemberRepository에 멤버 객체를 저장한다.

Result

<html>
        <head>
            <title>Title</title>
        </head>
        <body>
        성공
        <ul>
            <li>id=</li>
            <li>username=</li>
            <li>age=</li>
        </ul>
        <a href="/index.html">메인</a>
        </body>
</html>

그리고 위의 html이 유저에게 출력된다.

회원 목록 조회

@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>");
        /*
        w.write(" <tr>");
        w.write(" <td>1</td>");
        w.write(" <td>userA</td>");
        w.write(" <td>10</td>");
        w.write(" </tr>");
        */

        members.stream().forEach(member ->{
            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 find all

List<Member> members=memberRepository.findAll();

MemberRepository 객체를 이용해서 모든 멤버 객체를 다 조회한다.

Result

<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>
            
                <tr>
                    <td>1</td>
                    <td>user1</td>
                    <td>12</td>
                </tr>
            
                <tr>
                    <td>2</td>
                    <td>user2</td>
                    <td>23</td>
                </tr>
            
        </tbody>
    </table>
    </body>
</html>

위와 같이 테이블 형태로 유저 목록을 출력하게 된다.

servlet 한계점

servlet만으로 어플리케이션을 구성하게 되면, html을 처리하는 부분이 매우 복잡한 것을 확인할 수 있다. 자바코드를 이용해서 html을 생성하다 보니 html 코드를 만들어내기가 어렵다.

이런 html을 효율적으로 생성해주는 것이 바로 template engine인데, 대표적으로 jsp, Thymeleaf, Freemarker, 등이 있다.

Applcation using jsp

우선 jsp를 사용하기 위해 build.gradle에 라이브러리를 추가한다.

build.gradle

	//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>

jsp 파일은 html 태그를 이용한 뷰 환경 구성이 가능하다.

회원 등록을 담당하는 jsp

<%@ 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();
    String username = request.getParameter("username");
    int age = Integer.parseInt(request.getParameter("age"));

    Member newMember=new Member();
    newMember.setUsername(username);
    newMember.setAge(age);

    memberRepository.save(newMember);
%>
<html>
    <head>
        <title>Title</title>
    </head>
    <body>
    성공
    <ul>
        <li>id=<%=newMember.getId()%></li>
        <li>username=<%=newMember.getUsername()%></li>
        <li>age=<%=newMember.getAge()%></li>
    </ul>
    <a href="/index.html">메인</a>
    </body>
</html>

관련 class Import

<%@ page import="hello.servlet.domain.member.MemberRepository" %>
<%@ page import="hello.servlet.domain.member.Member" %>

관련 클래스들을 import 해주기 위해 <%@ page import %> 사용

Member Save Logic

<%
    MemberRepository memberRepository = MemberRepository.getInstance();
    String username = request.getParameter("username");
    int age = Integer.parseInt(request.getParameter("age"));

    Member newMember=new Member();
    newMember.setUsername(username);
    newMember.setAge(age);

    memberRepository.save(newMember);
%>

<% %> 안에서 자바 코드가 실행될 수 있다. (회원 등록 로직은 위의 서블릿 방식과 동일하다.)

Result

<html>
    <head>
        <title>Title</title>
    </head>
    <body>
    성공
    <ul>
        <li>id=<%=newMember.getId()%></li>
        <li>username=<%=newMember.getUsername()%></li>
        <li>age=<%=newMember.getAge()%></li>
    </ul>
    <a href="/index.html">메인</a>
    </body>
</html>

회원 등록이 정상적으로 이루어지면 해당 html이 출력된다. 이때, <%= %>를 이용해서 자바 코드 출력결과를 이용할 수 있다.

회원 목록 조회

<%@ page import="java.util.List" %>
<%@ page import="hello.servlet.domain.member.Member" %>
<%@ page import="hello.servlet.domain.member.MemberRepository" %>
<%@ 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>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) {
               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>

Member Find

<%
    MemberRepository memberRepository = MemberRepository.getInstance();
    List<Member> members=memberRepository.findAll();
%>

멤버 리스트를 Member Repository로부터 가져온다.

Member List View

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

각각의 멤버에 대해 테이블 row을 제작해준다.

jsp의 한계점

분명, 서블릿만 사용한 방식을 개선한 부분이 있지만, 그래도 몇가지 한계점이 존재한다.

우선, 하나의 jsp 파일에 비즈니스 로직을 담당하는 자바 코드와 ui를 담당하는 html 부분이 섞여서 존재한다. 이렇게 변경 라이프 사이클이 다른 두 가지가 혼재 되어 있으면, 수정이 독립적으로 이루어 질 수 없다.

따라서, 기능을 담당하는 비즈니스 로직과 화면 출력을 담당하는 뷰 파트를 분리하는 것이 중요한데, 이렇게 해서 나온 개념이 MVC 패턴이다.

Application using MVC

MVC

mvc_pattern

mvc pattern은 이전에도 다뤘듯이 위와 같은 방식으로 구조로 동작한다.

Controller

Http Request을 받아서, query parameter을 분석한 다음, 적절 Service 를 호출해서 작업을 수행하고, 수행 결과물을 Model에 담는다.

View

Model에서 데이터를 뽑아와서 해당 데이터를 화면에 출력하는 역할을 수행한다.

Model

Controller 와 View 사이에서 데이터를 옮기기 위한 매개체로 동작하며, Controller는 비즈니스 로직만 신경쓸 수 있게, 뷰는 화면 출력만 수행할 수 있도록 역할을 분리시켜준다.

여기서 Model은 request 객체의 getAttribute/setAttribute을 통해서 사용하겠다.

회원 등록 폼

Controller

이전에는 회원 등록폼 jsp가 바로 출력하도록 하였는데, 이제는 모든 http request를 controller가 처리할 수 있도록 하고, 해당 form 또한 controller가 호출할 수 있도록 한다.

@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 requestDispatcher = request.getRequestDispatcher(viewPath);
        requestDispatcher.forward(request,response);
    }
}

/servlet-mvc/members/new-form로 접속하게 되면 controller는 /WEB-INF/views/new-form.jsp 파일을 사용자에게 출력하게 된다.

String viewPath="/WEB-INF/views/new-form.jsp";
RequestDispatcher requestDispatcher = request.getRequestDispatcher(viewPath);
requestDispatcher.forward(request,response);

controller 동작과정에 있어, forward 메소드를 이용하게 된다. forward 메소들 사용하게 되면, redirect 방식과 같이 새로운 경로로 새로운 요청을 수행하는 것이 아니라, 서버 내부에서 해당 경로를 자동으로 호출해주는 것이다. 그렇게 되면 url이 바뀌는 redirect 방식과 달리 forward는 url이 바뀌지 않는다.

url는 그대로 /servlet-mvc/members/new-form 이고, 화면만 WEB-INF/views/new-form.jsp이 출력되는 것이다.

/WEB-INF 아래에 있는 파일에 대해서는 직접 접근이 안되며, 오직 controller을 통한 JSP 호출 방식으로 접근이 가능하다.

View

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
    <html>
        <head>
            <title>Title</title>
        </head>
        <body>
        <form action="save" method="post">
            username: <input type="text" name="username" />
            age: <input type="text" name="age" />
            <button type="submit">전송</button>
        </form>
    </body>
</html>

회원 등록 html form 기존과 유사하다. 다만, form action 부분의 save가 상대 경로임을 주의하자. form 이 /servlet-mvc/members/save로 전송되게 된다.

회원 저장

Controller

Member Join

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

Member newMember=new Member();
newMember.setUsername(username);
newMember.setAge(age);

memberRepository.save(newMember);     

Put data in model

//Model에 데이터 보관
request.setAttribute("member",newMember);

//View로 이동동
String viewPath = "/WEB-INF/views/save-result.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);

다음과 같이 setAttribute을 이용해서 Member 객체를 저장한다.

View

<%@ 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>

setAttribute에 저장한 모델에 대해 jsp에서는 ${파라미터명.필드} 방식으로 객체에 접근할 수 있도록 지원한다.

회원 목록 조회

Controller

Member Join

List<Member> members=memberRepository.findAll();

Put data in model

//Model에 데이터 보관
request.setAttribute("members",members);

//View로 이동동
String viewPath = "/WEB-INF/views/members.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);

View

<%@ 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>
<c:forEach var="item" items="${members}">
                <tr>
                    <td>${item.id}</td>
                    <td>${item.username}</td>
                    <td>${item.age}</td>
                </tr>
</c:forEach>

c 태그를 활용해서 리스트 형태의 변수에 대해 반복 처리를 쉽게 할 수 있다.

c 태그를 사용하기 위해 아래의 선언문을 추가해줘야한다.

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>

MVC 패턴이 가지는 한계점

뷰와 컨틀롤러를 분리하면서 코드가 직관적으로 변환되었지만, 개선할 부분이 존재한다.

컨트롤러 부분에 중복되는 코드가 많이 있다.

Duplcated Forwards

RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);

forward 처리를 하는 코드가 모든 controller에 중복된다.

Duplicated ViewPath

String viewPath = "/WEB-INF/views/new-form.jsp";

ViewPath의 prefix 부분이 “/WEB-INF/views/”, postfix 부분이 .jsp로 공통되는 부분이 존재한다.

이런식으로 공통적으로 수행해야되는 부분에 대하 처리가 이루어져야 한다. 이를 Front Controller 패턴을 이용해, Controller 이전에서 공통 기능들을 처리하는 Front Controller을 두어서 처리하도록 한다.

References

link: inflearn

link:springmvc

댓글남기기