Spring/Spring MVC

[스프링 MVC 1편] 서플릿, JSP, MVC 패턴

최진영 2021. 4. 19. 16:32

회원 관리 웹 어플리케이션 요구사항

 지금부터는 회원 가입과 회원 목록 조회의 기능을 통해서 Servlet으로 웹서비스를 개발해보면서 그 구조에 대해서 이해보고자 한다.

 

기능 요구 사항

  • 회원 저장
  • 가입된 회원 목록 조회

 

 일단 회원 저장과 목록을 보려면 회원에 대한 정보를 나타내는 클래스와 회원 정보를 담는 저장소가 필요하다.

 우리는 회원 정보를 나타내는 회원 도메인 모델을 만들 수 있다.

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;

    public Member() {
    }

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

 id를 생성자에 포함하지 않은 이유는 이후 만들 Repository에서 저장할 때 setter를 통해서 Repository에서 id값을 처리해주기 위함이다.

 

 이후 회원 정보를 저장할 Repository를 생성한다.

package hello.servlet.domain.member;

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

public class MemberRepsoitory {

    private static Map<Long, Member> store = new HashMap<>();
    private static long sequence = 0L;

    private static final MemberRepsoitory instance = new MemberRepsoitory();

    public static MemberRepsoitory getInstance() {
        return instance;
    }

    private MemberRepsoitory() {
    }

    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 clearStroe() {
        store.clear();
    }
}

 단, 코드 내용을 미리 보면 알 수 있듯이 Repository는 싱글톤으로 구현했다. 싱글톤이란, 전역 변수로 지정하지 않고, 객체 하나만을 생성하도록 하여 생성된 객체를 어디서든 동일하게 참조할 수 있도록 하는 패턴을 말한다. 따라서 Repository에 실제 데이터가 담길 저장소 store를 static 변수로 지정하여 인스턴스 변수로 사용하는 것이아닌 static 변수로써 이를 활용하였다.

 싱글톤으로 구현한 이 Repository는 다른 곳에서 객체를 생성하는 것을 막기 위해서 생성자는 private로 지정하여 다른 클래스에서의 접근을 막았다.

 

 다른 메소드의 경우 메소드 표현대로 member 객체를 받아서 Repository에 저장하는 save()메소드, 원하는 index 위치의 member를 가져오는 findById()메소드, 모든 member를 검색하는 findAll()메소드가 있다.

 그럼 Repository로의 접근은 어떻게 할까? Repository가 생성될 당시의 instance, 주소를 상수로써 저장한다. 이후 getInstance() 메소드를 통해 사용할 때마다 이 저장소 주소를 받아 참조하여서 사용하면 되는 것이다.

 

 설계한대로 Repository를 잘 생성했지만 우리는 항상 의심해야하는 개발자이기 때문에 잘 생성되었는지 테스트코드를 통해서 알아보도록 한다.

package hello.servlet.domain.member;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;

import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.*;

class MemberRepsoitoryTest {

    MemberRepsoitory memberRepsoitory = MemberRepsoitory.getInstance();

    @AfterEach
    void afterEach() {
        memberRepsoitory.clearStroe();
    }

    @Test
    void save() {

        //given
        Member member = new Member("hello", 20);

        //when
        Member savedMember = memberRepsoitory.save(member);

        //then
        Member findMember = memberRepsoitory.findById(savedMember.getId());
        assertThat(findMember).isEqualTo(savedMember);
    }

    @Test
    void findAll() {

        //given
        Member member1 = new Member("member1", 20);
        Member member2 = new Member("member2", 30);

        memberRepsoitory.save(member1);
        memberRepsoitory.save(member2);

        //when
        List<Member> result = memberRepsoitory.findAll();

        //then
        assertThat(result.size()).isEqualTo(2);
        assertThat(result).contains(member1, member2);
    }
}
  • afterEach() : member를 저장하는 테스트이기 때문에 서로 다른 테스트의 repository에 영향이 갈 수 있어 각 테스트 메소드가 끝난 후 Repository를 초기화해준다.
  • save() : Repository에 한명의 사람을 저장하고, 가장 처음 저장된 사람이 방금 저장한 사람인지 확인한다.
  • findAll() : Repository에 여러명의 사람을 저장하고, 각 사람이 잘 저장되어있는지 확인한다.

 

Servlet으로 회원 관리 웹 어플리케이션 만들기

 우리는 Servlet으로 지금까지 클라이언트와 서버를 통신하는 것을 배웠으니 Servlet만으로 클라이언트로부터 회원정보를 받아서 저장하고 회원 목록을 클라이언트로 주는 기능을 만들어보도록 한다.

 

MemberFormServlet : 회원 가입 form

 일단, Servlet으로 회원 저장을 하는 웹뷰를 만들어야 한다.

package hello.servlet.web.servlet;

import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepsoitory;

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 {

    private MemberRepsoitory memberRepsoitory = MemberRepsoitory.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");
    }
}

 HTML 웹뷰를 만들 것이기 때문에 response의 content type은 반드시 "text/html"로 지정해야한다.

 2차시에서 했던 servlet의 내용을 그대로 사용한 것이다. username과 age를 post 방식으로 파라메터를 보내는 방식으로 /servlet/members/save로 전송한다. 따라서 다음에 해야할 것은 이 urlPatterns로 들어올 때 지금 저장한 user를 저장하고 display해주는 servlet을 생성한다.

 

MemberSaveServlet : 회원 저장
package hello.servlet.web.servlet;

import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepsoitory;

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 MemberRepsoitory memberRepsoitory = MemberRepsoitory.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);
        memberRepsoitory.save(member);

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

 회원을 저장하고 방금 저장한 회원의 정보를 display해주는 servlet이다. 지금부터 이 servlet해야하는 역할은 다음과 같다.

  1. post방식으로 받은 username, age를 member 객체에 담는다.
String username = request.getParameter("username");
int age = Integer.parseInt(request.getParameter("age"));

Member member = new Member(username, age);
  1. member 객체를 Repository에 저장한다.
memberRepsoitory.save(member);
  1. 화면에 방금 저장한 member 정보를 보여준다.
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>");

 

MemberListServlet : 전체 회원 조회
package hello.servlet.web.servlet;

import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepsoitory;

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 MemberRepsoitory memberRepsoitory = MemberRepsoitory.getInstance();

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        List<Member> members = memberRepsoitory.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>");
    }
}

 저장중인 member를 전부 확인할 수 있는 servlet이다. 사실 Servlet을 이용하여 자바 내에서 html을 생성하였기 때문에 자바를 다루어봤다면 내용상 크게 어려운 부분이 없다.

  1. 현재 저장된 Repository의 모든 member를 찾는다.
List<Member> members = memberRepsoitory.findAll();
  1. members List에 있는 member의 정보를 출력한다.
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>");
 }

 지금까지 Servlet으로 구현한 웹페이지는 너무 자바스럽다. 기존의 html을 다루어봤던 사람이라면 java 코드안에 html이 들어가있는 것이 매우 어색하게 느껴질 것이다.

 알고있던 "정적" 페이지로써의 html과는 반대로 "동적"으로 저장했던 member가 추가되어도 원하는대로 html을 만들 수 있었다. 자바스럽게 말이다.

 근데 html을 다루어봤던 사람이라면 지금 이 상황이 매우매우 불편할 것이다. 그냥 html 파일로 만들었을 때는 w.write()메소드 없이 html을 만들어내면 되었었는데 지금은 자바안에서 html을 만들어야하기 때문에 w.write() 메소드가 필연적으로 들어가야해서 코드가 매우 난잡해졌다.

 

 이러한 배경에서 나온 것이 JSP이다. 동적인 HTML을 만들고 싶어서 자바코드 내에서 HTML을 만들긴 했는데 차라리 HTML 문서에서 자바 코드를 넣으면 낫지 않을까하는 고민이 있었다. 이러한 고민을 해결해주기 위해서 템플릿 엔진이 나왔고 초기에 가장 많이 쓰인 것이 JSP인 것이다. JSP를 쓰면 HTML 내에서 자바코드를 적용해서 동적으로 코드를 생성할 수 있다. 그럼 JSP에서 회원관리를 해보자.

 

JSP로 회원 관리 웹 어플리케이션 만들기

 JSP를 쓰기위해서 gradle에 라이브러리를 먼저 추가한다.

implementation 'org.apache.tomcat.embed:tomcat-embed-jasper'
implementation 'javax.servlet:jstl'

 

new-form.jsp
<%--
  Created by IntelliJ IDEA.
  User: jinyoungchoi
  Date: 2021/04/18
  Time: 12:03 오전
  To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <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>
</head>
<body>

</body>
</html>

 JSP 사용할 때 항상 JSP 문서라는 의미로

<%@ page contentType="text/html;charset=UTF-8" language="java" %>

을 상단에 명시해주어야 한다.

 

 회원 입력 Form의 경우 Servlet과 거의 동일하다. PrintWriter를 사용하지 않고 html에 그대로 표기했기 때문에 좀 더 HTML스럽다.

 JSP파일은 서버 내부에서 서블릿으로 변환되며 결국 이 파일은 실행되는 모양은 앞전에 만들었던 MemberFormServlet과 유사한 형태로 변환되어 사용되는 것이다. 특이 사항은 url에 form으로 들어가고자할 때

http://localhost:8080/jsp/members/new-form.jsp

로 jsp 파일로 경로를 지정해주어야한다는 것이다.

 

save.jsp
<%@ page import="hello.servlet.domain.member.Member" %>
<%@ page import="hello.servlet.domain.member.MemberRepsoitory" %>
<%--
  Created by IntelliJ IDEA.
  User: jinyoungchoi
  Date: 2021/04/18
  Time: 12:20 오전
  To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
    //request, response 문법상 자동 사용 가능
    MemberRepsoitory memberRepsoitory = MemberRepsoitory.getInstance();

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

    Member member = new Member(username, age);
    memberRepsoitory.save(member);
%>
<html>
<head>
    <title>Title</title>
</head>
<body>
<ul>
    <li>id=<%=member.getId()%></li>
    <li>username=<%=member.getUsername()%></li>
    <li>age=<%=member.getAge()%></li>
</ul>
<a href="/index.html">메인</a>
</body>
</html>

new-form.jsp는 로직 없이 파라메터만 전송하는 html었다. 그럼 실제로 member를 저장하는 로직을 가지고 있는 save.jsp에서는 어떻게 자바코드를 사용할까?

  • <%@ page import = "~"%> : 자바의 import 문과 동일하다.
  • <% ~ %> : 자바코드를 입력하여 사용할 수 있다.
  • <%= ~ %> : 자바코드를 출력하여 사용할 수 있다.

 코드가 흩어져있지만 코드내용은 Servlet으로 구현한 것과 같으니 생략하도록 한다.

 중요한 점은 HTML 파일에서 자바코드 로직을 수행하고, 자바코드를 출력하는 행위를 JSP에서 할 수 있다는 것이다.

 

members.jsp
<%@ page import="hello.servlet.domain.member.MemberRepsoitory" %>
<%@ page import="hello.servlet.domain.member.Member" %>
<%@ page import="java.util.List" %><%--
  Created by IntelliJ IDEA.
  User: jinyoungchoi
  Date: 2021/04/18
  Time: 12:39 오전
  To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
    MemberRepsoitory memberRepsoitory = MemberRepsoitory.getInstance();
    List<Member> members = memberRepsoitory.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>

 회원 전체목록의 경우 for문을 돌려가며 원하는 회원을 받아냈었다. 그래서 HTML 코드 내부에 <% %>를 사용하여 자바코드를 통해서 목록을 출력했다.

 

Servlet과 JSP의 한계

 지금까지 해보았던 Servlet으로 구현하는 방법과 JSP로 구현하는 방법은 둘다 명확한 단점이 있었다.

  • Servlet : 화면 출력을 위한 HTML을 자바코드로 만드려니 너무 지저분하다.
  • JSP : 자바코드, 레포지토리가 HTML에 모두 노출이 되어 전송된다.

 JSP만으로 모든 것을 해결하고자 했을 때 모든 비즈니스로직이 JSP에 포함되어 클라이언트에 노출이된다. 지금이야 간단한 코드지만 만약 대형서비스라면? 대형서비스 코드가 3천줄이 넘어간다면? 벌써부터 어질어질하다.

 Servlet으로만, JSP로만으로 개발을 하기에는 하나가 너무 많은 역할을 담당해서 유지보수를 하기 매우 까다롭다. 이때 나온 것이 MVC이다.

 

 

MVC

 MVC패턴이 등장하게된 이유는 앞전에 이야기했던 한계점들을 해결하기 위함이다.

  • Servlet, JSP 하나가 너무 많은 역할을 담당함
  • UI와 비즈니스로직의 변경의 라이프 사이클이 다름
  • JSP같은 view 템플릿은 화면 렌더링에 최적화되어있어 화면단 업무만 담당하는 것이 가장 효과적

 

 따라서 하나의 Servlet, 하나의 JSP로만 처리하는 것이 아닌 Controller와 View로 역할을 분할해서 개발을 하는 것을 MVC 패턴이라 한다.


  • Controller : HTTP 요청을 받아서 파라미터를 검증하고 비즈니스 로직을 실행. View에 전달할 데이터를 Model에 담아 View에 전달
  • View : Model에 담긴 데이터를 사용해서 화면단 생성
  • Model : View에 출력할 데이터를 담아두는 공간. View는 Model에서 필요한 데이터만 꺼내서 화면을 생성하면 된다.

 

MVC2

 하지만 여기서 개발자들은 멈추지 않는다. 아직도 역할이 제대로 분할되지 않다는 것을 깨닫기 때문이다. 즉, 지금과 같은 MVC1의 구조에서는 Controller가 결국 HTTP 요청도 처리하고, 비즈니스로직도 처리해야한다. 우리는 너무 많은 역할을 하나가 감당하기보단 역할을 쪼개는 것을 주도하기 때문에 Service 계층을 만들어 비즈니스 로직을 Service단에서 처리하도록 한다.


 이러한 배경에서 나온 것이 MVC2이며 바라는대로 각각의 역할이 쪼개져서

  • Controller : Http 요청 처리
  • Service : 비즈니스로직
  • Model : View 전달 데이터 저장
  • View : 화면단

의 형태가 지금 가장 많이 쓰이는 MVC 패턴의 모양을 띄게 되는 것이다.

 

MVC 패턴으로 회원 관리 웹 어플리케이션 만들기

 그럼 이제 MVC 패턴으로 회원관리 기능을 만들어보자.

 Controller는 Servlet이, View는 JSP가 담당한다. 이때 데이터를 담아 전달할 Model은 HttpServletRequest객체에 담아 전송한다. request도 내부에 저장소를 가지고 있는데, request.setAttribute(), request.getAttribute()메소드를 통해서 저장하고 조회할 수 있다.

 

MvcMemberFormServlet
package hello.servlet.web.servletmvc;

import javax.servlet.RequestDispatcher;
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;

@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 dispatcher = request.getRequestDispatcher(viewPath);
        dispatcher.forward(request, response);
    }
}
new-form.jsp
<%--
  Created by IntelliJ IDEA.
  User: jinyoungchoi
  Date: 2021/04/18
  Time: 11:26 오후
  To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<!-- 상대경로 사용, [현재 URL이 속한 계층 경로 + /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>

 

 일단 전체적인 진행 순서는 /servlet-mvc/members/new-form으로 url 요청을 받고 new-form.jsp경로를 forward하여 전송하여 view를 보여준다.

 이를 알기위해서는 forward가 뭔지, 그와 비슷한 개념인 redirect가 뭔지를 알아야한다.

forward

 서버 내부에서 일어나는 호출로 클라이언트가 인식하지 못하게 서버 내부에서 경로지정을 다해준다. 클라이언트는 요청을 보내고 받는 단 하나의 과정으로만 보이지만 서버 내부에서는 요청을 받고 그에 맞는 뷰를 전달한다.


 지금 URL1이 /servlet-mvc/members/new-form이고 URL2가 /WEB-INF/views/new-form.jsp인 상황이다. 클라이언트는 URL1을 호출했지만 MvcMemberFormServlet에서 URL2를 담아서 클라이언트에 forward 전송했기때문에 실제로 클라이언트의 url은 URL1이겠지만 보여지는 view는 URL2가 보여지게 된다.

 

redirect

 클라이언트가 서버로 요청을 했지만 서버로부터 요청을받아서 클라이언트가 redirect 경로로 서버에 다시 요청하는 것을 말한다.


 forward와는 반대로 URL1으로 요청했지만 URL1이 URL2로 request하라고 redirect하였고 클라이언트가 다시 URL2로 요청하여 결과를 받는 것이다.

 

 redirect와 forward는 결과적으로 클라이언트가 받는 최종 url은 URL2이긴하지만

  • forward : 클라이언트 url의 변화가 없다, 객체를 재사용할 수 있다.
  • redirect : 클라이언트 url의 변화가 있다, 객체의 재사용이 허용되지 않는다.

 결국 forward는 내가 보냈던 request객체를 가지고 내부에서 이리저리 옮기면서 사용하지만 redirect했을 경우 request, response 객체가 새로 생성된다.

 극단적으로 이 차이를 보여주는 예로 "글쓰기"를 들 수가 있다. forward로 응답페이지를 만든다면 내가 만약에 새로고침을 한다면 그 url로 다시 응답이 가기 때문에 똑같은 글이 여러개 생성된다. 따라서 redirect를 사용해서 일단 결과 url이 나온다면 글 작성에서 보낸 요청정보는 존재하지 않기 때문에 생성되지 않는다.

 

 

 현재 코드는 forward를 사용하였기 때문에 /servlet-mvc/members/new-form로 url 요청을 하여 view는 new-form.jsp를 받지만 url은 처음 요청된 url을 가지고있음을 확인할 수 있다.


 

MvcMemberSaveServlet
package hello.servlet.web.servletmvc;

import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepsoitory;

import javax.servlet.RequestDispatcher;
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;

@WebServlet(name = "mvcMemberSaveServlet", urlPatterns = "/servlet-mvc/members/save")
public class MvcMemberSaveServlet extends HttpServlet {

    private MemberRepsoitory memberRepsoitory = MemberRepsoitory.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);
        memberRepsoitory.save(member);

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

        String viewPath = "/WEB-INF/views/save-result.jsp";
        RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
        dispatcher.forward(request, response);
    }
}
save-result.jsp
<%--
  Created by IntelliJ IDEA.
  User: jinyoungchoi
  Date: 2021/04/18
  Time: 11:57 오후
  To change this template use File | Settings | File Templates.
--%>
<%@ 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>

 사실 이전 Servlet, JSP로 구현한 로직과 큰 차이는 없고, 단지 View단과 Controller단이 구분되었다는 것이 중요하다.

 HttpServletRequest를 Model로써 사용하기 위해 setAttribute()를 통해서 member값을 view로 보내준 것을 확인할 수 있다. forward하여 save-result.jsp가 받아서 이를 display하는데 물론 JSP도 자바문법을 사용하기 때문에 getAttribute()를 사용해서 request.getAttribute("member")와 같은 형태로 print할 수는 있지만 JSP에서는 ${}문법으로 이를 제공한다.

 따라서 member로 Model명을 담아서 보냈다면 ${member.id}로 이를 꺼내서 쓸 수 있다.

 

 

MvcMemberListServlet
package hello.servlet.web.servletmvc;

import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepsoitory;

import javax.servlet.RequestDispatcher;
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.util.List;

@WebServlet(name = "mvcMemberListServlet", urlPatterns = "/servlet-mvc/members")
public class MvcMemberListServlet extends HttpServlet {

    private MemberRepsoitory memberRepsoitory = MemberRepsoitory.getInstance();

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

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

        request.setAttribute("members", members);

        String viewPath = "/WEB-INF/views/members.jsp";
        RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
        dispatcher.forward(request, response);
    }
}
members.jsp
<%--
  Created by IntelliJ IDEA.
  User: jinyoungchoi
  Date: 2021/04/19
  Time: 12:27 오전
  To change this template use File | Settings | File Templates.
--%>
<%@ 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>

 전체조회도 Servlet은 members 객체를 Model에 담아서 전송하기만하면 끝이다.

 그럼 members객체가 List구조로 되어있으니 JSP는 이를 받을 때 taglib기능을 사용하여 반복해서 출력한다.

 members의 list에서 순서대로 item이라는 변수에 넣어서 꺼내고 이를 출력하는 과정을 반복하는, 마치 for each구문이다.

 

 

Servlet + JSP MVC의 한계

 기능마다 MVC로 분할은 한거같은데 뭔가 좀 불편한게 아직까지도 있다.

 

  • 포워드 중복
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);

View를 넘기는 코드가 항상 중복이 되고 이 코드를 계속 반복해서 사용해야 한다.

 

  • ViewPath에 중복
String viewPath = "/WEB-INF/views/new-form.jsp";

만약에 jsp가 아닌 다른 뷰파일로 변경하면 전체코드를 바꿔야 한다.

 

  • 사용하지 않는 코드
HttpServletRequest request, HttpServletResponse response

굳이 사용하지 않을 때도 있고 심지어 response는 지금까지 한번도 사용하지 않았다.

 

  • 공통 처리가 어렵다.

 컨트롤러에서 공통으로 처리해야하는게 점점 더 많아진다고 생각하면 호출하는데 휴먼에러가 발생하는 것을 막지 못한다.

 

 결국 이런 문제는 Controller를 호출하기 전에 먼저 공통 기능을 처리하여야 해결이 된다. 즉 프론트에 대한 수문장 기능을 할 수 있는 프론트 컨트롤러(front controller) 패턴이 필요하며 이를 다음 포스트에서 알아보도록 한다.

 

* 김영한님의 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술을 듣고 기록한 학습 자료입니다.