이번 게시글에서는 이전에 따라만들었던 Front Controller(dispatcherServlet)를 다른 방법을 적용하여 만들어보려고 한다. 그냥 바로 시작해보자.
이전게시글을 통해 우리는 MVC 모델의 Front Controller가 dispatcherServlet
이라는 것을 알 수 있었다. dispatcherServlet
은 MVC 모델에서 요청 발생 시 가장 먼저 요청을 접수하는 객체이다. 그렇다면 컨트롤러의 포괄적인 역할 속에서 dispatcherServlet
이 수행하는 역할을 다시 한번 생각해보자.
1단계 : 요청이 발생하면 요청을 접수
2단계 : 접수한 요청에 대한 유형 분석 및 유형에 적합한 하위 컨트롤러에게 요청 위임
3단계 : 요청 처리에 따른 비즈니스 로직 수행 지시
4단계 : 처리 결과가 있을 경우 결과를 저장
5단계 : 저장된 결과를 출력하기 위하여 view로 전송
컨트롤러의 5가지 역할 중 1단계, 2단계, 5단계
의 역할을 하는 것이 dispatcherServlet
이다. 그렇기 때문에 이전 예제에서는 dispathcerServlet
이 유형을 분석하기 쉽도록 handleMapping
과 viewResolver
를 사용하여 비즈니스로 로직을 수행할 하위 컨트롤러의 인스턴스를 매핑하였으며, 처리 결과를 출력할 view를 viewResolver를 통해 전달하여 주었다. 이번 게시글에서는 해당 역할을 dispatcherServlet
안에서 매핑파일을 읽어와 자동으로 수행되도록 코드를 작성해보겠다.
dispatcherServlet
의 코드는 수정되어서는 안됨dispatcherServlet
의 코드에는 파일의 경로 및 직접적인 데이터 입력 불가크게 위의 세가지 규칙을 준수하면서 작성해보겠다.
//import 생략...
public class dispatcherServlet extends HttpServlet{
Properties props; //설정 파일 정보를 읽어드릴 객체
FileInputStream fis; //file에 대한 inputStream 차후 close를 위해 멤버로 보유
/*
- 최초에 요청이 발생하면 servlet 객체 생성
- 생성과 동시에 controller 및 view 정보파일을 읽어드릴 properties객체 생성
- properties가 load할 파일경로 세팅
*/
public init(ServletConfig servletConfig) throws ServletException{
props = new Properties();
//servlet 초기 설정 파일 내 init 파라미터를 읽어드림
//해당 파라미터에 저장된 파일경로를 등록
String configContextLocation = servletConfig.getInitParmeter("configContextLocation);
String realPath = servletConfig.getServletContext().getRealPath(configContextLocation);
try(){
fis = new FileInputStream();
props.load(fis);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doRequest(request, response);
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doRequest(request, response);
}
//공통으로 수행될 메서드
protected void doRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//인코딩 세팅
request.serCharacterEncoding("utf-8");
//요청 uri 분석
String uri = request.getRequestURI();
//props에서 uri에 적합한 컨트롤러 경로 추출
String controllerPath = props.getProperty(uri);
try{
//props로 추출한 컨트롤러 경로를 forName() 매개변수로 전달
//Class 저장
Class controllerClass = Class.forName(controllerPath);
//Controller형으로 요청유형에 매핑된 객체 대입
Controller controller = controllerClass.getDeclaredConstructor().newInstance();
//인터페이스 공통 메서드 수행
controller.execute(request, response);
//비즈니스 로직 수행 후 결과를 반환할 view경로
String viewName = controller.getViewName();
//반환된 경로에 맞는 Page 경로 획득
String viewPage = props.getProperty(viewName);
}
if(controller.isForword()){
//획득된 Page 경로 전달
RequestDispatcher dis = request.getDispatcher(viewPage);
dis.forword(request, response);
} else{
response.sendRedirect(viewPage);
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (SecurityException e) {
e.printStackTrace();
}
}
@Override
public void destroy() {
if(fis != null) {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
이렇게해서 dispatcherServlet
의 코드가 작성이 완료되었다. 매핑을 위해 사용한 파일은 일반 파일로 XML을 사용해도되나 설정값이 다르기에 다음 예제에서 사용하도록하려고 한다. 일반 파일의 상태를 아래 코드를 통해 보자.
#controller 매핑
/blood.do=com.mvc.controller.BloodController
/movie.do=com.mvc.controller.MovieController
#view 매핑
/blood/view=/blood/result.jsp
/movie/view=/movie/result.jsp
XML 또는 Json등은 파서를 통해 데이터로 변환하여 사용할 수 있지만 일반파일은 properties 객체
를 사용하는 것이다. properties
는 HashTable
을 상속하고 있으며, HashTable
은 곧 Map
이기 때문에 Key와 Value로 호출하여 사용이 가능하다. properties객체의 getProperty()를 호출할 때 ()안에 매핑 파일의 Key값을 입력해주면 된다. 참고로 '='
으로 기준으로 좌측은 Key, 우측은 Value이다.
그렇다면 매핑 파일과 dispatcherServlet
을 연결하기 위해서 Web.xml은 어떻게 세팅이 되어있는지 확인해보자.
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://xmlns.jcp.org/xml/ns/javaee" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" id="WebApp_ID" version="3.1">
<display-name>MVCApplication</display-name>
<!-- 웹 어플리케이션에 모든 요청이 FrontController에 모이도록 세팅 -->
<servlet>
<servlet-name>dispatcherServlet</servlet-name>
<servlet-class>com.mvc.controller.DispathcerServlet</servlet-class>
<!-- 생성시 전달할 정보를 지정 -->
<init-param>
<param-name>configContextLocation</param-name>
<param-value>/WEB-INF/mapping.data</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>dispatcherServlet</servlet-name>
<url-pattern>*.do</url-pattern>
</servlet-mapping>
</web-app>
Servlet 클래스와 경로를 매핑해주고 init-param
을 통해 생성 시 전달할 정보파일의 경로를 지정해준다. 이렇게 설정을 해주어야 dispatcherServlet
코드에서 ServletConfing
객체가 얻어올 수 있는 것이다.
그림을 통해서 dispatcherServlet
의 동작순서를 다시 한번 알아보자.
전체 코드를 설명하는 것은 너무 장황하고 효율이 떨어지니 컨트롤러의 역할 측면에서 단계별로 어떤 코드가 사용되었는지 확인하고 내부적으로 어떤 동작들이 흘러가는지 확인하자.
예를 들어 게시물을 보기 위해 사용자가 '게시물 목록 보기'라는 버튼을 클릭했다고 가정하자.
요청이 최초로 발생하면 dispatcherServlet
을 통해서 요청이 접수된다. 이때 dispatcherSerlvet
이 생성되면서 init()
가 호출된다.
public init(ServletConfig servletConfig) throws ServletException{
props = new Properties();
//servlet 초기 설정 파일 내 init 파라미터를 읽어드림
//해당 파라미터에 저장된 파일경로를 등록
String configContextLocation = servletConfig.getInitParmeter("configContextLocation);
String realPath = servletConfig.getServletContext().getRealPath(configContextLocation);
try(){
fis = new FileInputStream();
props.load(fis);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
이때 dispatcherServlet
은 매핑정보
를 읽어드릴 파일이 저장된 경로를 얻어와 해당 설정파일 내의 데이터를 읽어오며 매핑 정보
를 가진 dispatcherServlet
이 생성되는 것이다.
자 이제 요청을 접수했으니 해당 요청을 분석하고, 요청 유형에 맞는 하위 컨트롤러에게 위임해보겠다.
dispatcherServlet
은 요청을 분석하고 분석이 끝나면 해당 요청에 매핑되어있는 하위 컨트롤러의 인스턴스화를 진행한다. 해당 단계의 코드를 보자.
//공통으로 수행될 메서드
protected void doRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//인코딩 세팅
request.serCharacterEncoding("utf-8");
//요청 uri 분석
String uri = request.getRequestURI();
//props에서 uri에 적합한 컨트롤러 경로 추출
String controllerPath = props.getProperty(uri);
try{
//props로 추출한 컨트롤러 경로를 forName() 매개변수로 전달
//Class 저장
Class controllerClass = Class.forName(controllerPath);
//Controller형으로 요청유형에 매핑된 객체 대입
Controller controller = controllerClass.getDeclaredConstructor().newInstance();
아래코드 의도적으로 생략.....다음단계를 위해
}
위 코드에서 보는 것처럼 props
객체가 Key값으로 해당 컨트롤러의 경로를 받아오며 Class의 forName()
메서드를 호출하여 해당 클래스 경로를 newInstance()
해준다. 이 상태가 되어야지 해당 컨트롤러의 인스턴스화가 완료된 것이다. 여기서 컨트롤러가 수행하는 메서드명은 각각의 컨트롤러가 동일하며, 내부로직은 요청 처리에 적합하게 오버라이드
되어있다. 컨트롤러 코드는 이전 게시글을 참고하자.
요청을 위임받은 하위 컨트롤러는 해당 요청을 처리하기 위해서 Model
에게 로직 수행을 지시한다.(아직 Service의 개념을 도입하지 않은 상태임을 감안하기 바란다.) Model
이 로직을 수행하면서 DB에 접근하여 필요한 데이터를 받아오거나 데이터를 DB에 저장만하고 비즈니스 로직이 완료되어질 수 있다. 아래 그림을 보자.
3단계까지 완료하고, 결과가 있다면 request 객체
에 저장하고 view경로를 반환한 상태로 하위 컨트롤러의 임무는 종료되어진다. 이를 전달받은 dispatcherServlet
은 5단계를 진행한다.
비즈니스 로직까지 수행이 완료되면 남은 일은 클라이언트에게 해당 결과를 전달하는 것이다. 이때 두가지 상황으로 나뉠 수 있다.
이런 상황을 판단하기 위해 컨트롤러로부터 forword
여부를 반환하는 메서드를 통해 조건을 판단하면된다. 아래 코드를 보자.
//doRequest()의 일부내용
//인터페이스 공통 메서드 수행
controller.execute(request, response);
//비즈니스 로직 수행 후 결과를 반환할 view경로
String viewName = controller.getViewName();
//반환된 경로에 맞는 Page 경로 획득
String viewPage = props.getProperty(viewName);
if(controller.isForword()){
//저장할 결과가 있는 경우
RequestDispatcher dis = request.getDispatcher(viewPage);
dis.forword(request, response);
} else{
//저장할 결과가 없는 경우
response.sendRedirect(viewPage);
}
이렇게 forword
여부에 대해 판단하지 않으면 발생하는 문제점은 많다. 그 중 대표적으로 글을 등록하는 경우를 예로 들어보자.
위 그림의 흐름을 따라가면서 forword
를 할 경우 어떤 문제가 있는지 알아보자. 그전에 forword
와 redirection
에 대해서 간략하게 설명하겠다.
너무 간단하게 작성해서 이해가 되지 않을 것이다. 지금은 결과가 있을때는 forword
, 없을때는 redirection
을 쓴다는 정도로만 이해해두자. 다시 본론으로 돌아와 위에서 게시물을 등록하고 forword
를 해버리면 발생하는 문제점이 무엇인지 알아보겠다.
모든 작업이 완료되어도 최초 요청과 같은 URI
가 남는다면 브라우저를 새로고침할 경우 동일한 작업을 반복수행하게 되는 결과를 맞이하게된다. 그렇기 때문에 결과의 여부를 판단하여 forword
와 redirection
수행에 대한 판단을 해야하는 것이다.
정리하자면 아래의 그림과 같다.
이렇게해서 이번 게시글에서는 특정 file에서 매핑 정보를 읽어와 dispatcherServlet에서 자동으로 매핑하는 MVC 모델의 front controller를 작성해보았다.
위 예제를 작성하면서 MVC모델의 전반적인 흐름과 Spring을 들어가기에 앞서 어떻게 설정을 하고 동작하는지에 대하여 알 수 있었다.
그럼 이만.👊🏽
해당 글에 대한 전체 코드는 여기를 참고하면된다.