Servlet, JSP, MVC 패턴

뚝딱이·2022년 7월 26일
0

스프링 MVC

목록 보기
3/23

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

회원 정보

  • 이름 : username
  • 나이 : age

기능 요구사항으로는 회원 저장, 회원 목록 조회가 있다.

회원의 id, username, age를 담은 회원 도메인이 필요하고, 회원을 저장할 저장소가 필요하다. 이때, 저장소는 싱글톤패턴을 적용한다.

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

회원 정보를 입력할 수 있는 HTML Form을 만들어서 응답해보자.

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

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

위의 코드를 보면 바로 알 수 있듯이, 자바 코드로 HTML을 제공해야한다. 이는 가독성도 떨어지며 개발자가 작성시에도 불편하다.

사용자가 form의 전송 버튼을 누르면 회원정보가 저장되어야 한다.
만들어보자.

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

위 코드의 작동원리는 아래와 같다.

  1. urlPatterns = "/servlet/members/save" 이므로 사용자가 전송을 눌렀을 때 service 메서드가 동작한다.

    • "<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" +
      전송을 눌렀을 때 /servlet/members/save으로 이동하는 것을 볼 수 있다.
  2. getParamater를 통해 요청으로 들어온 username과 age를 각각 저장한다.

  3. member 객체를 생성한다.

  4. memberRepository에 저장한다.

  5. Member 객체를 사용해서 결과 화면용 HTML을 동적으로 만들어서 응답한다.

회원 조회 클래스도 위와 비슷한 방식을 이용하여 만든다. findAll을 사용하면 된다.

위의 코드를 통해 우리는 서블릿으로 동적으로 원하는 HTML을 만들 수 있음을 알았다. 하지만 위에서 말했듯 매우 복잡하고 비효율적이다. html코드를 따로 분리하는 것이 간편할 것임을 보기만 해도 알 수 있을 것이다.

이러한 것을 해줄 수 있는게 템플릿 엔진이다. 템플릿 엔진을 사용하면 HTML 문서에서 필요한 곳만 코드를 적용해 동적으로 변경할 수 있다. 템플릿 엔진에는 JSP, Thymeleaf, Freemarker, Velocity등이 있다.

요즘은 스프링과 잘 통합되는 Tymeleaf를 사용한다.

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

JSP를 사용하려면 아래의 라이브러리를 추가해야한다.

//JSP 추가 시작
implementation 'org.apache.tomcat.embed:tomcat-embed-jasper'
implementation 'javax.servlet:jstl'
//JSP 추가 끝

이제 위에서 만들었던 form을 보여주는 new-form을 만들어보자.

<%@ 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 는 <%@ page contentType="text/html;charset=UTF-8" language="java" %>로 시작한다. 이는 JSP문서라는 것을 알려준다.

위의 코드를 보면 HTML가 완전히 똑같아 보인다. JSP는 서버 내부에서 서블릿으로 변환되는데, 위에서 만들었던 서블릿과 거의 비슷한 모습으로 변환된다.

그렇다면 회원 저장 JSP도 만들어 보자.


<%@ page import="hello.servlet.domain.member.Member" %>
<%@ page import="hello.servlet.domain.member.MemberRepository" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
    //request, response 사용 가능 자동으로 서블릿으로 변환돼서 사용
    MemberRepository memberRepository = MemberRepository.getInstance();

    System.out.println("MemberSaveServlet.service");
    String username = request.getParameter("username");
    int age = Integer.parseInt(request.getParameter("age"));

    Member member = new Member(username, age);
    memberRepository.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>

JSP는 <% ~ %>를 통해 자바 코드를 입력할 수 있고, <%= ~ %>를 통해 코드를 출력할 수 있다. 또한 <%@ page import="hello.servlet.domain.member.Member" %>를 통해 import문도 확인할 수 있다.

위의 코드를 보면 서블릿과 대조되는 것이 있을 텐데, 서블릿은 자바 코드에 HTML을 넣는 느낌이라면 JSP는 HTML에 자바 코드를 넣는 모양이다.

즉, 서블릿 : 자바 중심의 HTML 입력 / JSP : HTML 중심의 자바 입력
이라고 할 수 있겠다.

한계

서블릿에선 화면을 보여주는 뷰, 즉 HTML 코드가 지저분해 JSP에서 뷰를 깔끔하게 작성했다. 동시에 자바코드를 넣어 동적인 변경도 적용했다. 하지만, JSP를 보면 상위 절반은 비즈니스 로직이고 하위 절반만 뷰 영역이다.

즉, 자바 코드가 모두 JSP에 노출되어 있어 JSP가 너무 많은 역할을 하는 것을 볼 수 있다. 지금은 간단한 프로젝트이니 JSP가 많은 역할을 한다고 해도 와닿지 않을수도 있다. 하지만 엄청나게 큰 프로젝트에서 JSP에 모든 것이 담겨있다면, 유지보수는 그야말로 지옥일 것이다.

따라서 비즈니스 로직은 서블릿에서 처리하고 뷰는 JSP에서 처리하는 MVC 패턴을 적용해 만들어보자.

MVC 패턴

너무 많은 역할

위에서 우리는 JSP와 서블릿만으로 프로젝트를 진행했을 때 너무 많은 역할을 갖게 되어 유지보수가 어려워짐을 깨달았다. 따라서 역할 분담이 안되어있어 비즈니스 로직을 변경할 때도, 뷰를 변경할 때도 같은 파일을 수정해야하는 일이 생기는 것이다.

변경의 라이프 사이클

우리가 프로젝트에서 변경할 사항이 있다고 생각해보자. 버튼 하나를 바꾸는 변경사항과 비즈니스로직을 변경해야하는 일이 동시에 영향을 주면서 일어날 일이 있을까. 각각의 변경사항은 다르게 발생할 가능성이 매우 높고 서로에게 영향도 주지 않을 것이다. 따라서 변경의 라이프 사이클이 다르므로 같은 파일에 관리하는 것은 좋지 않다.

변경 주기가 다를 때 분리한다.

기능 특화

특히 JSP 같은 뷰 템플릿은 화면을 렌더링하는데 최적화 되어있으므로 이 부분의 업무만 담당하는것이 제일 효과적이다.

Model View Controller

  • 컨트롤러: HTTP 요청을 받아서 파라미터를 검증하고, 비즈니스 로직을 실행한다. 그리고 뷰에 전달할 결과 데이터를 조회해서 모델에 담는다.

  • 모델: 뷰에 출력할 데이터를 담아둔다. 뷰가 필요한 데이터를 모두 모델에 담아서 전달해주는 덕분에 뷰는 비즈니스 로직이나 데이터 접근을 몰라도 되고, 화면을 렌더링 하는 일에 집중할 수 있다.

  • 뷰: 모델에 담겨있는 데이터를 사용해서 화면을 그리는 일에 집중한다. 여기서는 HTML을 생성하는 부분을 말한다.

(참고) 컨트롤러에 비즈니스 로직을 두면 컨트롤러가 너무 많은 역할을 담당하므로 비즈니스 로직은 보통 서비스라는 계층을 별도로 만들어 처리한다.

위의 그림은 위에서 우리가 해보았던, 서블릿만으로, jsp만으로 만들었던 프로그램이다.

위의 그림이 우리가 이제 만들 MVC 패턴이다.

  1. 클라이언트의 요청

  2. Controller가 요청을 받아 파라미터를 꺼내 HTTP 요청 스펙을 확인한다.

  3. 서비스, 리포지토리를 통해 Model에 데이터를 담는다.

  4. Controller가 View를 호출한다.

  5. View는 Model의 데이터를 참조하여 응답한다.

MVC 패턴 적용

JSP는 request에 데이터 저장소를 둬 데이터를 조회하고 보관한다.
request.setAttribute()request.getAttribute를 사용한다.

회원 등록 폼 컨트롤러를 만들어보자.

@WebServlet(name = "mvcMemberFormServlet", urlPatterns = "/servlet-mvc/members/new-form")
public class MvcMemberFormServlet extends HttpServlet {//form을 보여주고 싶은 것이므로 서블릿에선 할 일이 없다. view로 얼른 넘겨주자
    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String viewPath = "/WEB-INF/views/new-form.jsp";
        RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);//controller에서 view로 이동할 때 사용
        dispatcher.forward(request,response);//서블릿에서 jsp 호출

        /**
         * 요청이 들어오면 service 실행
         * jsp 경로를 다시 호출, 즉 viewPath를 호출한다.
         * 따라서 서버에서 서버끼리 내부에서 호출하는 것.
         * 제어권을 view로 넘겨주는 것
         */
    }

forward는 서블릿에서 JSP를 호출해주는데, 이것을 redirect와 비슷하다 생각할 수 있다.
하지만 redirect와는 엄연히 다른데, redirect는 클라이언트(웹 브라우저)에게 응답이 가서 클라이언트(웹브라우저)가 바뀐 URI로 재요청하는 것이다. 따라서 클라이언트도 인지할 수 있으며 URI또한 변경된다.
하지만 forward는 서버 내부에서 호출이 발생하여 클라이언트는 알아채지 못하고 URI 또한 변하지 않는다.
실제로 form을 실행해보면 JSP의 경로는 /WEB-INF/views/new-form.jsp이지만, URI는 urlPatterns인 /servlet-mvc/members/new-form인 것을 확인할 수 있다.

또한 위의 코드를 보면 JSP의 경로가 /WEB-INF 안에 있는 것을 볼 수 있다. JSP가 이 경로 안에 있으면 해당 JSP는 외부에서 직접 호출할 수 없고 항상 컨트롤러를 통해야 호출가능하다.

이제 JSP 코드를 살펴보자.

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

위에서는 상대경로를 사용했는데 보통은 절대경로로 하는 것을 추천한다.

우리는 회원 정보 FORM 에 관련한 코드들을 살펴봤다.

save와 list에 관한 코드 또한 동작 방식이 비슷하다.
관련해서 참고할 점만 짚고 넘어가자.

  • ${member.id} : ((Member)request.getAttribute("member)).getId() 프로퍼티 접근법이라고 한다.
  • <c:forEach> 기능을 사용하려면 <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>를 선언해야한다.

MVC 패턴의 한계

MVC 패턴을 사용하여 뷰와 컨트롤러의 역할을 구분했다. 뷰는 화면을 그리는 역할에 충실한 덕에 코드가 깔끔하고 직관적이다. 하지만 컨트롤러는 dispatcherforward관련 코드가 중복되고, form 컨트롤러에선 필요하지 않았던 request와 response가 보였다.

또한 viewPath도 중복되는 것을 보였는데,

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

prefix: /WEB-INF/views/
suffix: .jsp

같이 prefix와 suffix가 계속해서 중복된다. 또한 jsp가 아닌 다른 뷰로 변경한다면 전체 코드를 다 변경해야하는 불상사가 생긴다.

공통처리가 어렵다.

기능이 복잡해질 수 록 컨트롤러에서 공통으로 처리해야 하는 부분이 점점 더 많이 증가할 것이다. 단순히 공통 기능을 메서드로 뽑으면 될 것 같지만, 결과적으로 해당 메서드를 항상 호출해야 하고, 실수로 호출하지 않으면 문제가 될 것이다. 그리고 호출하는 것 자체도 중복이다.

따라서 컨트롤러 호출 전에 먼저 공통 기능을 처리해야한다. 따라서 프론트 컨트롤러를 토입하여 문제를 해결해보자.


출처 : 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술

profile
백엔드 개발자 지망생

0개의 댓글