1️⃣ MVC 패턴 구조
서블릿을 활용해 간단한 MVC 패턴을 구현해보자.
- 클라이언트가 특정 URL을 호출하면 컨트롤러는 해당 요청을 받는다.
- 클라이언트가 전달한 데이터를 모델에 담는다.
- 컨트롤러는 모델을 뷰로 전달한다.
- 뷰는 모델을 담아 필요한 화면을 클라이언트에게 전달한다.
위 흐름대로 로직을 작성해보자. 또한 컨트롤러는 서블릿 객체를, 모델은 HttpServletRequest 객체를, 뷰는 JSP로 구현할 것이다.
HttpServletRequest 객체의 request 내부에는 데이터 저장소가 있다. 여기선 따로 모델 객체를 만들지 않고 이를 활용하자.
- 모델에 데이터 저장 : request.setAttribute()
- 모델의 데이터 조회 : request.getAttribute()
먼저 실습에 필요한 멤버 객체를 하나 만들자.
public class Member {
String username;
int age;
public Member(String username, int age) {
this.username = username;
this.age = age;
}
}
2️⃣ 컨트롤러 구현
1) 등록 컨트롤러
// ServletController.class
@WebServlet(name = "servletController", urlPatterns = "/servlet-mvc/members/new")
public class ServletController 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);
}
}
물론 Service - Repository 를 가지는 구조라면 컨트롤러에서 서비스 로직을 호출할 것이다. 하지만 해당 컨트롤러는 특별한 로직을 수행하지 않고 바로 JSP를 호출하면 된다.
따라서 JSP의 경로를 지정해준 뒤 'RequestDispatcher'의 'forward()' 메서드로 바로 JSP를 반환하면 된다. 또한 모델로 'HttpServletRequest' 객체를 사용할 것이기 때문에 request를 함께 전달한다. 물론 response도 전달해야 한다.
- WEB-INF : WAS 의 규칙, 해당 패키지 내부의 자원은 외부에서 호출할 수 없음. 따라서 컨트롤러를 통해 호출해야 한다.
- dispatcher.forward() : 다른 서블릿이나 JSP로 이동할 수 있는 기능으로, 서버 내부에서 다시 호출이 발생. 이는 redirect랑은 다르다.
2) 결과 컨트롤러
// SaveServletController
@WebServlet(name = "saveServletController", urlPatterns = "/servlet-mvc/members/save")
public class SaveServletController extends HttpServlet {
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String name = request.getParameter("username");
int age = Integer.parseInt(request.getParameter("age"));
Member member = new Member(name, age);
// 레포지토리에 저장하는 로직
...
// Model에 저장하는 로직
request.setAttribute("member", member);
String viewPath = "/WEB-INF/views/save-result.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
}
- request.getParameter() 메서드를 통해 각각 이름과 나이를 가져온다. 이는 아래 '등록 뷰'의 폼 데이터로부터 전달 받은 데이터이다. 실제로 input 태그의 'username'과 'age'라는 파라미터 이름을 매핑하기 때문에 잘 확인해줘야 한다.
- 만약 service - repository 까지 존재한다면 레포지토리에 저장하는 로직을 호출한다.
- 모델인 'HttpServletRequest'에 저장하기 위해 request.setAttribute() 메서드를 호출한다. 이 때 앞에서 저장한 이름과 나이를 통해 'Member'객체를 만들고 이를 전달한다.
- request.setAttribute("이름", 데이터) : 여기서 이름은 뷰에 전달할 데이터 이름, 데이터는 전달할 데이터를 넣어준다.
- 뷰를 호출하는 로직
3️⃣ 뷰 구현
1) 등록 뷰
<-- new-form.jsp -->
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="save" method="post">
name: <input type="text" name="username" />
age: <input type="text" name="age" />
<button type="submit">제출</button>
</form>
</body>
</html>
2) 결과 뷰
<-- save-result.jsp -->
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<ul>
<li>name = ${member.username}</li>
<li>age = ${member.age}</li>
</ul>
</body>
</html>
위의 'SaveServletController'로부터 호출되어 사용자에게 전달된다. 이 때 컨트롤러에서 모델에 'member'라는 이름으로 Member 객체를 전달하였다. JSP에서는 이를 '${member.username}' 처럼 객체에 접근하듯 데이터를 가져오면 된다.
4️⃣ 한계
MVC 패턴을 적용했기 때문에 컨트롤러와 뷰의 역할을 분리할 수 있었다. 하지만 서블릿으로 구현한 해당 MVC 패턴은 다음과 같은 한계가 존재한다.
1) 중복 코드
@WebServlet(name = "xxxController", urlPatterns = "/xxx/...")
public class ServletController extends HttpServlet {
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String viewPath = "/WEB-INF/views/...";
// 내부 로직
...
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
}
}
간단한 로직이었기 때문에 비교적 코드가 길지 않았다. 그럼에도 불구하고 위처럼 중복 코드가 발생했다. 만약 조금 더 복잡한 구조라면? 한두 개가 아닌 몇십 개 이상의 컨트롤러를 가진다면? 중복된 코드가 계속 발생할 것이다. 예시만 봐도 뷰의 경로를 설정하는 코드, 뷰로 이동하는 코드 등 같은 코드가 반복된다. 실수로 해당 로직을 작성하지 않는다면 오류 발생은 물론 복잡한 프로그램에선 찾기도 쉽지 않을 것이다.
만약 뷰를 JSP가 아닌 타임리프나 그 외 다른 템플릿으로 바꾼다면 관련 로직을 하나씩 찾아서 다 수정해줘야 한다. 물론 정적 변수로 선언하는 등의 방법이 있겠지만, 그래도 변경이나 확장에 있어서는 어려움을 겪을 수 있다.
2) 불필요한 코드
예시에서는 모델을 HttpServletRequest 객체로 사용했다. 하지만 어떤 프로그램에서는 사용하지 않을 수 있다. 마찬가지로 HttpServletResponse 객체는 사용하지 않았다. 이처럼 서블릿으로 MVC 패턴을 구현했을 때 불필요한 코드를 작성해야할 수 있다. 또한 HttpServletRequest, HttpServletResponse 등을 사용하는 코드는 테스트 케이스를 작성하기도 어렵다.
3) 공통 처리의 어려움
기능이 복잡해진다면 컨트롤러에서 공통 처리해야할 부분이 많아진다. 공통 기능을 가진 부분을 메서드로 구현하면 될 것 같지만, 결과적으로 해당 메서드를 항상 호출해야 한다. 또한 실수로 호출하지 않는다면 오류가 발생할 수도 있다. 또한 공통으로 처리할 로직이 많아진다는 것은 중복 코드를 작성해야한다는 의미일 수도 있다. 만약 이를 해결하기 위해 공통으로 처리해야 할 컨트롤러를 하나 구현하고, 나머지 컨트롤러들은 해당 역할에 맞는 로직만 수행한다면 문제를 어느정도 해결할 수 있을 것이다.
참고
인프런 김영한 : 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 강의