Spring MVC

Spring MVC없이 Servlet, JSP로 MVC 구현해보기 - 3

cwchoiit 2024. 5. 7. 14:03
728x90
반응형
SMALL

참고자료:

 

스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 | 김영한 - 인프런

김영한 | 웹 애플리케이션을 개발할 때 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. 스프링 MVC의 핵심 원리와 구조를 이해하고, 더 깊이있는 백엔드 개발자로 성장할 수 있습

www.inflearn.com

 

MVC 패턴이란건 왜 나왔는가? 이전 포스팅까지 그 이유를 알아봤다.

서블릿, JSP를 사용해보니 여러 불편한 점이 많았고 그 중 JSP는 서블릿보단 HTML을 만들어내기가 쉽지만 담당하고 있는게 너무 많아져버린다. 화면과 비즈니스 로직을 전부 담당하고 나니 지저분해지고 보기가 힘들어진다. 이는 곧 유지보수가 어려워진다. 

 

그래서 화면은 딱 화면을 담당하는 쪽에서만, 비즈니스 로직은 비즈니스 로직을 담당하는 쪽에서만 관리하고 처리하게 하고 싶은것이다.

 

그리고 또 하나는 둘 간의 변경 사이클이 다를 확률이 높다. 무슨 말이냐면 화면에 보이는 버튼의 위치를 바꾸고 싶다는 요구사항이 생길 때 비즈니스 로직을 건들 필요가 없다. 반대로 비즈니스 로직을 해결한 기술을 바꾸고 싶을 때 화면을 구성하는 어떤 부분도 변경할 필요가 없다. 근데 두 코드가 같은 파일에 있다는 것은 유지보수하기 좋지 않다. 

 

그래서 MVC 패턴이 등장한다. Model, View, Controller로 영역을 나눈것이다.

  • 컨트롤러: HTTP 요청을 받아서 파라미터를 검증하고, 비즈니스 로직을 실행한다(호출한다). 그리고 뷰에 전달할 결과 데이터를 조회해서 모델에 담는다.
  • 모델: 뷰에 출력할 데이터를 담아둔다. 뷰가 필요한 데이터를 모두 모델에 담아서 전달해주는 덕분에 뷰는 비즈니스 로직이나 데이터 접근을 몰라도 되고, 화면을 렌더링 하는 일에 집중할 수 있다.
  • : 모델에 담겨있는 데이터를 사용해서 화면을 그리는 일에 집중한다. 여기서는 HTML을 생성하는 부분을 말한다.
참고: 컨트롤러가 비즈니스 로직을 실행할 수도 있다. 근데 사이즈가 조금만 크다면 실행하지 말고 호출해라. 비즈니스 로직을 수행하는 부분을 컨트롤러로부터 떼어내는 것이다. 일반적으로 잘 알려진 '서비스'라는 레이어로 말이다.

 

 

 

그래서, 지금부터 할 내용은 작성한 JSP 파일에서 비즈니스 로직과 뷰 부분을 떼어낼 것이다. 서블릿을 컨트롤러로 사용하고 JSP를 뷰로 사용해서 MVC 패턴을 적용해보자.

서블릿을 컨트롤러로, JSP를 뷰로

우선, 서블릿 하나를 만들자. 멤버를 생성하는 폼에 대한 컨트롤러이다.

경로는 /path/your/package/web/servletmvc로 만들었다.

 

MvcMemberFormServlet

package org.example.servlet.web.servletmvc;

import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.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 req, HttpServletResponse resp) throws ServletException, IOException {

        String viewPath = "/WEB-INF/views/new-form.jsp";
        RequestDispatcher dispatcher = req.getRequestDispatcher(viewPath);
        dispatcher.forward(req, resp);
    }
}

여기서 보면, 화면으로 뿌려줄 JSP파일의 경로를 작성해서 RequestDispatcher 객체로 만들었다. 이 객체는 서버로 이 요청이 들어오면 forward()를 통해 JSP파일로 요청을 전달해버리는 기능을 가진다. 리다이렉트와 유사한것 같지만 리다이렉트는 클라이언트와 통신을 두번한다.

 

최초의 요청 -> 서버는 리다이렉트 경로를 알려주는 정보를 가지고 응답 -> 응답 데이터에 있는 리다이렉트 확인 ->  다시 해당 정보로 서버에 요청 -> 서버가 응답.

 

이게 리다이렉트라면 이 forward()는 요청이 들어와서 서버 내부에서 호출을 해서 최종 결과를 클라이언트에게 전달해준다. 그래서 클라이언트와 서버 간 통신은 한번뿐이다. 

 

그리고 WEB-INF는 뭐냐면 기존에는 webapp안에 jsp/members/new-form.jsp 이렇게 경로를 지정해서 JSP 파일을 만들었다. 그리고 이 경로 그대로 URL에 입력하면 JSP 파일이 딱 브라우저에 뜬다. 근데 그걸 못하게 하는 것이다. WEB-INF 내부에 있는 자원들은 외부에서 직접적으로 접근하지 못하고 항상 컨트롤러를 통해 호출된다. 

 

src/main/resources/WEB-INF/views/new-form.jsp

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

여기서는 action의 경로를 유심히 봐야한다. 단순히 "save"가 끝이다. 즉, 상대경로로 POST 요청이 날라가게 설정했다. 이렇게 해두면 현재 URL에 뒤에 "/save"가 붙은 경로로 보낸다.

 

예를 들어 이 new-form.jsp를 보여주기 위한 URL은 "http://localhost:8080/servlet-mvc/members/new-form"이다. 그럼 이제 전송 버튼을 클릭하면 경로가 "http://localhost:8080/servlet-mvc/members/save"인 곳으로 POST 요청이 날라간다.

 

그럼 이제 멤버를 저장할 서블릿과 JSP를 만들어야한다.

MvcMemberSaveServlet

package org.example.servlet.web.servletmvc;

import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.example.servlet.domain.member.Member;
import org.example.servlet.domain.member.MemberRepository;

import java.io.IOException;

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

    private final MemberRepository memberRepository = MemberRepository.getInstance();

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String username = req.getParameter("username");
        int age = Integer.parseInt(req.getParameter("age"));

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

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

        String viewPath = "/WEB-INF/views/save-result.jsp";
        RequestDispatcher dispatcher = req.getRequestDispatcher(viewPath);
        dispatcher.forward(req, resp);
    }
}

 

여기서 중요한 건 모델을 만들어서 뷰에게 전달해줘야한다. 그 부분이 바로 req.setAttribute("member", member);이다.

그 다음은 똑같이 forward()를 통해 JSP 파일을 호출한다.

 

src/main/resources/WEB-INF/views/save-result.jsp

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

모델에 담긴 데이터를 꺼내는 방법은 "${}"를 사용하면 된다. 해당 객체가 가지고 있는 프로퍼티를 그대로 꺼내서 사용한다.

한번 잘 나오는지 직접 테스트해보자.

 

이제 회원목록을 보여주는 서블릿과 JSP 파일을 작성하자.

MvcMemberListServlet

package org.example.servlet.web.servletmvc;

import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.example.servlet.domain.member.Member;
import org.example.servlet.domain.member.MemberRepository;

import java.io.IOException;
import java.util.List;

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

    private final MemberRepository memberRepository = MemberRepository.getInstance();

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        List<Member> members = memberRepository.findAll();

        // Model에 보관
        req.setAttribute("members", members);

        String viewPath = "/WEB-INF/views/members.jsp";
        RequestDispatcher dispatcher = req.getRequestDispatcher(viewPath);
        dispatcher.forward(req, resp);
    }
}

 

src/main/resources/WEB-INF/views/members.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
<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>

 

JSP에서는 여러 데이터를 하나씩 뽑아서 뿌려주는 방법이 있는데 그 중 하나가 이 녀석을 사용하는 것이다.

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

근데 이거 진짜 그냥 이런게 있구나하고 넘어가도 된다. 필요하면 찾아보면 되는거고 JSP 쓸 일 거의 없다. 이렇게 하고 리스트를 보면 잘 나온다.

 

 

결론

이렇게 서블릿과 JSP를 사용해서 한 곳에서 작성되던 뷰와 비즈니스 로직을 쪼개서 각자가 자기것만 잘 담당할 수 있도록 해봤다. 그러다보니 코드가 이전보다 깔끔해졌다. 그러나 여전히 100% 만족스럽지 않다. 왜냐하면 서블릿에서 지금 중복 코드가 계속 반복되고 있기 때문이다. 다음 코드를 보자.

String viewPath = "/WEB-INF/views/new-form.jsp";
RequestDispatcher dispatcher = req.getRequestDispatcher(viewPath);
dispatcher.forward(req, resp);
String viewPath = "/WEB-INF/views/members.jsp";
RequestDispatcher dispatcher = req.getRequestDispatcher(viewPath);
dispatcher.forward(req, resp);
String viewPath = "/WEB-INF/views/save-result.jsp";
RequestDispatcher dispatcher = req.getRequestDispatcher(viewPath);
dispatcher.forward(req, resp);

 

viewPath의 실제 경로 말고 모든게 똑같다. 이런 공통적으로 처리될 부분들이 계속해서 중복되고 있다. 이걸 처리하는 유틸성 메서드를 만들면 조금 더 나아지겠지만 그것을 매번 호출하는 것 역시 중복이다. 그래서 이것을 더 개선하고 싶어진다.

 

공통된 부분들은 앞에서 미리 다 처리하고 들어오는 방식으로 말이다. 수문장 하나가 맨 앞에서 모든 요청을 받고 그 요청마다 공통적으로 처리되는 부분들을 거기서 전부 해결하고난 후 각각의 컨트롤러로 요청을 전달해주는 것이다. 이런 패턴을 Front Controller 패턴이라고 하고 스프링 MVC도 이 패턴을 잘 구현한 방식이다.

 

한번 직접 이 Front Controller를 만들고 MVC 패턴을 구현해보자. 

728x90
반응형
LIST