Spring

[Spring] 서블릿, JSP, MVC 패턴으로 회원 관리 웹 애플리케이션 만들기

이덩우 2023. 7. 6. 02:19

지난 포스팅에서 서블릿을 통해 HTTP 요청 메세지를 어떻게 처리하고 응답을 보내는지 알아봤다.

본 포스팅에서는 간단한? 회원 관리 웹 애플리케이션을 만들어볼 것이다!

서블릿, JSP, MVC 패턴을 이용해서 '동일한 '웹 애플리케이션을 총 3번 만들어 본다.

가장 오래된 기술인 서블릿을 사용하면 생기는 불편한 점과 비효율적인 면이 무엇인지 알아보고, JSP를 사용했음에도 여전히 남아있는 비효율적인 면들을 살펴볼 것이다.

마지막으로 현재로써 최고의 방식인 서블릿 + JSP를 활용한 MVC 패턴에 존재하는 한계점은 무엇인지, 차근차근 알아가보자.

 

 


 

- 회원 관리 웹 애플리케이션의 요구사항

회원 정보

  • 이름 : username
  • 나이 : age

 

기능 요구사항

  • 회원 저장
  • 전체 회원 목록 조회

 

설명

  • 회원 리포지토리를 생성해 로컬 메모리에 List 형태로 회원 정보 저장
  • HTML Form을 통해 회원 등록
  • 성공적으로 저장 시, 저장 된 회원 정보를 보여주는 HTML 반환
  • 전체 회원 목록을 조회할 수 있는 동적 HTML 화면 개발

 


 

- 서블릿으로 회원 관리 웹 애플리케이션 만들기

1. 회원 등록 폼

@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 {
        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");
    }
}
  • @WebServlet에 명시한 url로 접근하면 해당 서블릿에 접근하게 된다.
  • 이 단계에서는, 처음 웹 브라우저에서 접근하는 과정이므로 HTTP 요청 메세지를 처리할 게 없다.
  • 응답으로, HTML Form을 반환해주면 된다.
  • 폼의 액션을 보면, 다음 단계에서 개발할 SaveServlet으로 POST 해주고 있는 것을 확인할 수 있다.
  • 아래는 반환 받은 HTML Form이다.

반환된 HTML Form

  • 이제 전송버튼을 눌러 데이터를 던져줬으니, SaveServlet을 개발하러 가보자.

 

2. 회원 저장

@WebServlet(name = "memberSaveServlet", urlPatterns = "/servlet/members/save")
public class MemberSaveServlet extends HttpServlet {
    MemberRepository memberRepository = MemberRepository.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);
        memberRepository.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>");

    }
}
  • 이전 폼에서 받은 데이터는 서블릿 덕분에 'request' 객체를 통해서 편하게 조회할 수 있다.
  • getParameter() 메소드로 회원 이름, 회원 나이를 꺼내어 변수로 담고 회원을 생성한다.
  • 싱글톤인 MemberRepository 인스턴스를 불러와 회원을 저장한다.
  • 성공적으로 저장되었다는 것을 동적인 HTML 화면으로 반환해주자! 방금 생성된 회원의 고유ID, 회원 이름, 회원 나이를 반환해준다.

회원 저장 성공 페이지 반환

  • 참고로, MemberRepository save() 메소드는 반환타입이 Member 인데 저장된 회원으로 곧바로 추가적인 작업을 할거면 save()를 호출하면서 Member 객체에 담아줘도 되고, 필요없다면 마치 void 처럼 save() 만 호출해도 된다. 자바가 알아서 반환 타입을 담을 객체를 사용하지 않았다면, 리턴 값은 버려준다.

 

3. 전체 회원 목록 조회

@WebServlet(name = "memberListServlet", urlPatterns = "/servlet/members")
public class MemberListServlet extends HttpServlet {

    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>");
    }
}
  • MemberRepositoryfindAll() 메소드를 통해 전체 회원 목록을 리스트 형태로 받는다. 
  • HTML의 table을 만들고, table 내부에 for문을 통해 리스트의 모든 회원정보를 출력해준다.
  • @WebServlet의 url로 접근하면 전체 회원 정보를 확인할 수 있다.

전체 회원 정보 조회

 


 

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

JSP가 뭐야?

  • 지금 만들고 있는 회원 관리 웹 애플리케이션 처럼 동적인 HTML을 만들어 반환하는 과정을 생각해보자.
  • 위에서 서블릿만으로 애플리케이션을 개발할 때, 자바 코드에 HTML을 추가해야 해서 굉장히 지저분한 코드가 완성됐다.

자바에 섞여 지저분한 HTML 코드

  • 요구하는 상황에 따라 적절히 HTML을 동적으로 생성해 반환해야 할 때, 그 때 필요한 것이 뷰 템플릿이다.
  • 뷰 템플릿을 사용하면 HTML을 기반으로 작성하고, 필요한 부분은 자바코드를 추가할 수 있다.
  • 뷰 템플릿에는 JSP, 타임리프 등이 있는데 기능적인 면이나 속도적인 면으로 요즘은 '타임리프'를 많이 사용한다.
  • 그래도 고전이 중요한 만큼! JSP 뷰 템플릿을 활용해 다시 회원 관리 웹 애플리케이션을 개발해보자.

 

1. 회원 등록 폼

<%@ 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>
  • 첫줄의 <%@ page ... %> 은 JSP문서라는 뜻이다.
  • 브라우저에서 해당 JSP파일의 절대 경로로 접근하게 되면 HTML Form이 보여지게 된다.

HTML Form

  • 액션을 보면, 새로운 save.jsp 파일로 POST 방식으로 데이터를 전달하고 있다.
  • 새로운 save.jsp파일을 개발하러 가보자!

 

2. 회원 저장

<%@ page import="hello.servlet.domain.MemberRepository" %>
<%@ page import="hello.servlet.domain.Member" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
  MemberRepository memberRepository = MemberRepository.getInstance();

  System.out.println("save.jsp");
  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>
<head>
      <meta charset="UTF-8">
</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>
  • 회원 저장부터는 기존의 서블릿 내의 service() 내부에서 getParameter() 메소드를 통해 원하는 데이터를 조회하고 회원을 저장하는 로직을 만들었다.
  • JSP는 HTML 기반이다. 어떻게 자바 코드를 추가할까?
  • 파일의 상단을 보면 <% ..... %>  부분이 보일 것이다.
  • 해당 부분 안에 필요한 자바 코드를 넣으면 된다!
  • 그런데 여기서 의문이 든다. 서블릿에서 사용하던 request, response 객체들과 getParameter() 메소드는 어떻게 사용하는걸까?
  • JSP파일은 실행 될 때 내부적으로 서블릿으로 변환되기 때문에 사용이 가능하다!
  • 실제로 service() 단을 서블릿 처럼 적어주지 않았지만, 마치 service() 가 있는 것처럼 개발하면 자동으로 인식된다.
  • 결과적으로 보여주고 싶은 것은 성공적으로 저장된 회원 정보이다.
  • JSP에서는 <%= ... %> 을 통해 보여주고 싶은 자바 코드 값을 직접 넣을 수 있다.

3. 전체 회원 목록 조회

<%@ page import="hello.servlet.domain.MemberRepository" %>
<%@ page import="hello.servlet.domain.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>
<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>
  • 서블릿에서 전체 회원 정보 조회 기능을 개발한 것과 거의 동일하다.
  • 다만 자바 코드 기반 -> HTML 기반의 JSP로 환경이 바뀐 것이다.
  • 결과를 확인해보자!

전체 회원 조회 결과

  • 조금 특이한 점이 있다.
  • 이전에 서블릿으로 웹 애플리케이션을 개발 할 때 등록했던 회원들이 같이 조회되고 있다.
  • MemberRepository를 싱글톤으로 만들었고, 내부의 저장소 storestatic 으로 만들었기 때문에 공유되는 것이다!

 


 

- 서블릿과 JSP의 한계

서블릿의 특징과 한계

  • 서블릿을 통해 임의의 요청에 대한 응답으로 HTML을 반환할 수 있었다.
  • 하지만 작성하다 보니 계속 공통적으로 반복되는 작업들이 존재한다.
  • 또한 가장 중요한 응답으로 내보내주는 HTML을 자바 코드 위에서 작성해야 하기 때문에  너무 지저분하고 복잡하다.
  • HTML을 간결하게 작성하기 위해 다음 단계로 뷰 템플릿인 JSP를 사용해봤다.

 

JSP의 특징과 한계

  • JSP를 통해 기존 자바 코드 속 복잡한 HTML 코드에서, 진짜 HTML 기반으로 뷰를 만들 수 있게 됐다.
  • 필요한 자바 코드들도 JSP가 지원하는 방식으로 같이 작성할 수 있었다.
  • 하지만 이렇게 해도 해결되지 않는 몇 가지 고민이 남는다.
  • JSP 파일을 보면 코드의 상위 절반은 회원을 저장하기 위한 비지니스 로직이고, 나머지 하위 절반만 결과를 HTML로 보여주기 위한 뷰 영역이다. 
  • 코드를 잘 보면, 자바 코드, 데이터를 조회하는 리포지토리 등 다양한 코드가 모두 JSP에 노출되어 있다.
  • JSP 하나에서 너무 많은 역할을 수행한다.
  • 프로젝트 규모가 커질수록 유지보수가 정말 어려울 것이다.
  • 따라서 비지니스 로직의 역할과, 뷰 역할을 분리할 수 있는 방법을 고민해봐야 한다.

 


 

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

MVC 패턴이 뭐야?

  • MVC는 Mode, View, Controller를 의미한다.
  • MVC 패턴은 지금까지 실습한 것 처럼 하나의 서블릿이나, JSP로 처리하던 것을 컨트롤러와 뷰 영역으로 서로 역할을 나눈 것을 말한다.
  • 웹 애플리케이션은 보통 이 MVC 패턴을 사용한다.
  • 컨트롤러 (Controller) : HTTP 요청을 받아서 파라미터를 검증하고, 비지니스 로직을 실행한다. 이후 뷰에 전달할 데이터를 모델에 담는다.
  • 모델 (Model) : 뷰에 출력할 데이터를 담아둔다. 모델 덕분에 뷰는 비지니스 로직이나 데이터베이스에 대한 접근을 몰라도 되고, 화면을 렌더링 하는 일에만 집중할 수 있다. 
  • 뷰 (View) : 모델에 담겨있는 데이터를 사용해서 화면을 렌더링 하는 일에만 집중한다. 여기서는 HTML을 생성하는 부분을 의미한다.
  • MVC 패턴을 그림으로 이해해보자!

MVC 패턴 1
MVC 패턴 2

  • MVC 패턴은 크게 컨트롤러에서 비지니스 로직까지 담당하는 패턴과, 컨트롤러와 서비스단을 분리시켜 컨트롤러에서 비지니스 로직이 있는 서비스를 호출하는 패턴이 있다.
  • 사실 첫 번째 방식을 사용하게 되면 컨트롤러가 너무 많은 역할을 담당하게 된다. 그래서 일반적으로 비지니스 로직을 서비스라는 계층으로 별도로 만드는 방식을 사용한다.
  • 자 이제 서블릿은 컨트롤러로 사용하고, JSP를 뷰로 사용해서 MVC 패턴을 적용해보자!

1. 회원 등록 폼

// 컨트롤러
@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);
    }
}
// 뷰
<%@ 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>
  • 컨트롤러는 서블릿으로 구성했다.
  • 단순 회원 등록 폼을 띄워주는 기능이므로, 별 다른 비지니스 로직은 없이 viewPath를 지정해주고 disPacther를 통해 JSP로 이동시켜준다.
  • HttpServletRequest의 디스패처는 컨트롤러로 들어온 요청을 다른 컨트롤러나, 뷰로 전달(이동)시켜주는 역할을 한다.
  • 뷰(JSP)로 이동하게 되면, 폼에 데이터를 입력하고 전송을 누르면 상대경로인 "save"로 데이터를 POST한다.
  • 생각해보자. 기존에 접근했던 url은 "/servlet-mvc/members/new-form" 이다.
  • 상대경로 "save" 데이터를 보내면 "/servlet-mvc/members/save"를 호출하게 될 것이다!
  • 그럼 이후에 생성할 회원 저장 컨트롤러(서블릿)의 url 경로를 "/servlet-mvc/members/save"으로 잡아주면 된다.
  • 참고로 디렉토리 WEB-INF 안에 뷰를 담당할 JSP파일들을 생성해놓으면, 외부에서 직접적으로 디렉토리 경로를 통해 접근하는 것을 허용하지 않는다. 이는 컨트롤러를 통해 접근해야한다는 것을 의미한다.
  • 이전에 만든 폼과 동일한 화면을 볼 수 있다.

HTML Form

 

2. 회원 저장

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

    MemberRepository memberRepository = MemberRepository.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);
        memberRepository.save(member);

        // Model에 데이터를 담는다
        request.setAttribute("member" , member);

        String viewPath = "/WEB-INF/views/save-result.jsp";
        RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
        dispatcher.forward(request, response);
    }
}
// 뷰
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
  <head>
      <meta charset="UTF-8">
  </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>
  • 자, 이전에 HTML Form의 액션 때문에 SaveServlet이 호출되었다. 
  • 컨트롤러에서는 회원을 저장하는 비지니스 로직을 수행한다.
  • 이전처럼 성공적으로 회원가입을 마치고 나면, 성공 메세지와 함께 회원 정보를 HTML로 반환할 것이다.
  • 때문에 컨트롤러 -> 뷰로 이동할 때 "모델"에 저장한 회원을 담아서 보내야한다.
  • 모델에 데이터를 담는 방법이 바로 request.setAttribute() 이다.
  • viewPath를 지정하고 디스패처를 통해 뷰로 이동시키기 전에, request안에 있는 임시 저장소에 방금 저장한 회원을 담아서 보내는 것이다!
  • 이후 뷰로 이동하고 나면, 원래는 모델에서 받아온걸 (Member) 로 캐스팅하고~ getId() 하면서 복잡하게 원하는 값을 <%= ... %> 안에 넣어줘야 하는데, JSP는 또 하나의 편의 기능을 지원한다.
  • ${...} 안에 바로 모델값을 꺼내서 넣을 수 있다!
  • 성공적으로 회원 정보를 저장한 결과창이다.

회원 저장 성공 페이지 반환

 

3. 전체 회원 목록 조회

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

    MemberRepository memberRepository = MemberRepository.getInstance();

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

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

        request.setAttribute("members", members);

        String viewPath = "/WEB-INF/views/members.jsp";
        RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
        dispatcher.forward(request, response);
    }
}
// 뷰
<%@ 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>
  • 이전과 동일한 메커니즘이다!
  • 다만, 뷰에서 전체 회원 목록을 보여줄 때, "jstl" 이라는 라이브러리를 활용했다.
  • <c : forEach> ... </c: forEach> 태그를 사용해서 자바에서 Iterator를 돌리듯이 모든 회원을 출력할 수 있었다.

 


 

서블릿 + JSP를 활용한 MVC 패턴의 한계

  • MVC 패턴을 적용한 덕분에 컨트롤러의 역할과 뷰를 렌더링 하는 역할을 명확히 구분할 수 있었다.
  • 특히 뷰는 화면을 그리는 역할에 충실한 결과, 코드가 매우 깔끔하고 직관적이다. 단순히 모델에서 필요한 데이터를 꺼내고 화면을 만든다.
  • 하지만 컨트롤러는 딱 봐도 중복되는 코드가 많고, 심지어 필요없는 코드들도 있다.
  • 매번 디스패처를 만들며 forward()를 호출해줘야 한다.
  • viewPath를 지정해주는 과정도 중복된다.
  • 특히 response는 아예 사용되지 않았다.
  • 정리해보면, 공통으로 처리할 수 있는 부분을 따로 만들면 중복되는 코드를 작성하지 않을 수 있다.
  • 따라서 요청 시 컨트롤러를 호출하기 이전에 먼저 공통되는 기능을 처리하는, 소위 수문장 역할의 프론트 컨트롤러 (Front-Controller) 패턴을 도입하면 이런 문제를 해결할 수 있다.
  • 스프링 MVC의 핵심도 바로 이 프론트 컨트롤러에 있다.
  • 다음 포스팅부터 프론트 컨트롤러를 도입한 MVC 패턴에 대해서 알아보자!

 


 

- 정리

  • 서블릿을 이용한 웹 애플리케이션 개발부터 JSP, 서블릿 + JSP를 활용한 MVC 패턴으로 개발까지 총 3가지 방식으로 같은 웹 애플리케이션을 개발해봤다.
  • 순서대로 진행해보며 부족한 점을 조금씩 개선해나가는 과정을 살펴봤다.
  • 서블릿 + JSP를 활용한 MVC 패턴을 사용했음에도 중복되는 코드와 불필요한 코드가 있었다.
  • 이 문제를 해결하기 위해서는 프론트 컨트롤러를 도입해야한다.

 

 

 

 

출처 : 인프런, 김영한의 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술