MVC 패턴 - 개요
서블릿과 JSP의 한계
서블릿으로 개발할 때는 뷰(View) 화면을 위한 HTML을 만드는 작업이 자바 코드에 섞여 지저분하고 복잡해진다. JSP를 사용하면 뷰를 생성하는 HTML 작업을 깔끔하게 가져가고, 동적으로 변경이 필요한 부분에만 자바코드를 적용할 수 있다.
하지만 JSP 코드 상위 부분의 절반은 비즈니스 로직이고, 나머지 하위 절반은 결과를 HTML로 보여주기 위한 뷰 영역이 된다. JSP에는 Java 코드, Repository 등 다양한 코드가 모두 노출되며 너무 많은 역할을 가져가게 된다.
하나의 서블릿이나 JSP로 비즈니스 로직과 뷰 렌더링을 모두 처리하게 되면 유지보수가 어려워진다. 제일 큰 문제점은 둘 사이에 변경의 라이프 사이클이 다르다는 점이다. 예를 들면 UI를 일부 수정하는 일과 비즈니스 로직을 수정하는 일은 각각 다르게 발생할 가능성이 매우 높고 대부분 서로에게 영향을 주지 않는다. 이렇듯 변경 라이프 사이클이 다른 부분을 하나의 코드로 관리하는 것은 유지보수에 좋지 않다.
Model View Controller
- 컨트롤러: HTTP 요청을 받아 파라미터를 검증하고 비즈니스 로직을 실행한다. 뷰에 전달할 결과 데이터를 조회해서 모델에 담는다.
- 모델: 뷰에 츌력할 데이터를 남는다. 뷰가 필요한 데이터를 모델에 담아 전달해주기 때문에 비즈니스 로직이나 데이터 접근을 몰라도 되고 화면을 렌더링 하는 일에 집중할 수 있다.
- 뷰: 모델에 담겨있는 데이터를 사용해서 화면에 그린다.(HTML 생성)
컨트롤러에 비즈니스 로직을 두게 되면 컨트롤러가 너무 많은 역할을 하게 되므로 비즈니스 로직은 서비스(Service) 계층에서 별도로 처리한다. 그리고 컨트롤러는 비즈니스 로직이 있는 서비스를 호출하는 역할을 담당한다.
MVC 패턴 - 적용
서비스를 컨트롤러로 사용하고 JSP를 뷰로 사용해서 MVC 패턴을 적용해본다.
Model은 HttpServletRequest 객체를 사용한다. request는 내부에 데이터 저장소를 가지고 있는데,
request.setAttribute(), request.getAttribute()를 사용하면 데이터를 보관하고 조회할 수 있다.
package hello.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 request, HttpServletResponse response) throws ServletException, IOException {
String viewPath = "/WEB-INF/views/new-form.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response); // 다른 서블릿이나 JSP로 이동. 서버 내부에서 다시 호출 발생.
}
}
dispatcher.forward()는 다른 서블릿이나 JSP로 이동할 수 있는 기능이며 서버 내부에서 다시 호출이 발생한다.
/WEB-INF 경로 안에 JSP가 있으면 외부에서 직접 JSP를 호출할 수 없다.
redirect는 실제 클라이언트(웹 브라우저)에 응답이 나갔다가 클라이언트가 redirect 경로로 다시 요청하므로 클라이언트가 인지할 수 있고 URL 경로도 실제로 변경된다. forward는 서버 내부에서 일어나는 호출이기 때문에 클라이언트가 전혀 인지하지 못한다.
package hello.servlet.web.servletmvc;
import hello.servlet.basic.domain.member.Member;
import hello.servlet.basic.domain.member.MemberRepository;
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="/mvcMemberSaveServlet", urlPatterns = "/servlet-mvc/members/save")
public class MvcMemberSaveServlet extends HttpServlet {
private MemberRepository memberRepository = MemberRepository.getInstance();
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String username = request.getParameter("username");
int age = Integer.valueOf(request.getParameter("age"));
Member member = new Member(username, age);
memberRepository.save(member);
// Model에 data를 보관한다.
request.setAttribute("member", member);
String viewPath = "/WEB-INF/views/save-result.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
}
}
HttpServletRequest를 Model로 사용한다. request가 제공하는 setAttribute()를 사용하면 request 객체에 데이터를 보관해서 뷰에 전달할 수 있다. 뷰는 request.getAttribute()를 사용해서 데이터를 꺼낸다.
MVC를 사용해서 컨트롤러 로직과 뷰 로직을 분리할 수 있으며 화면 수정이 필요할 경우에는 뷰 로직만 변경하면 된다.
package hello.servlet.web.servletmvc;
import hello.servlet.basic.domain.member.Member;
import hello.servlet.basic.domain.member.MemberRepository;
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;
import java.util.List;
@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);
}
}
request 객체를 사용해서 List<Member> members를 모델에 보관한다.
MVC 패턴 - 한계
MVC 패턴은 컨트롤러와 뷰의 역할을 명확하게 구분할 수 있게 해준다. 뷰는 화면을 그리는 역할에 충실해지기 때문에 코드가 깔끔하고 직관적이다. 단순하게 모델에서 필요한 데이터를 꺼내 화면을 만들 수 있다. 하지만 컨트롤러는 중복이 너무 많고 필요하지 않은 코드들도 아직 많아 보인다.
1. forward 중복
View로 이동하는 코드가 항상 중복 호출된다. 메서드를 공통화해도 되지만 해당 메서드도 항상 직접 호출해야하는 단점이 있다.
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
2. ViewPath 중복
JSP가 아닌 thymeleaf와 같은 다른 뷰로 변경하게 된다면 전체 코드를 변경해야 하는 단점이 있다.
String viewPath = "/WEB-INF/views/...";
3. 사용하지 않는 코드
HttpServletRequest, HttpServletResponse를 사용할 때도 있고 사용하지 않을 때도 있다. 해당 코드를 사용하면 테스트 케이스를 작성하기 어려워지기도 한다.
4. 공통 처리 어려움
기능이 복잡해질 수록 컨트롤러에서 공통으로 처리해야 하는 부분이 증가하게 되는데, 메서드를 만들어도 항상 호출해야 하고 호출하는 자체도 중복이 된다.
이 문제를 해결하기 위해 컨트롤러 호출 전에 공통 기능을 처리해야 한다. 프론트 컨트롤러(Front Controller) 패턴을 사용하면 문제를 해결할 수 있으며 스프링 MVC 핵심도 프론트 컨트롤러에 있다.
🐣 출처: 인프런 김영한님 강의
이 글은 인프런의 김영한님 스프링 강의를 보고 작성한 글입니다.
강의를 들으면서 정리한 글이므로 틀린 내용이나 오타가 있을 수 있습니다.