[Spring]스프링 기초 - MVC 프레임워크 따라만들기(2)

Inung_92·2023년 2월 6일
1

Spring

목록 보기
3/15
post-thumbnail

이번 게시글에서는 이전에 따라만들었던 Front Controller(dispatcherServlet)를 다른 방법을 적용하여 만들어보려고 한다. 그냥 바로 시작해보자.

DispatcherServlet의 역할

이전게시글을 통해 우리는 MVC 모델의 Front Controller가 dispatcherServlet이라는 것을 알 수 있었다. dispatcherServlet은 MVC 모델에서 요청 발생 시 가장 먼저 요청을 접수하는 객체이다. 그렇다면 컨트롤러의 포괄적인 역할 속에서 dispatcherServlet이 수행하는 역할을 다시 한번 생각해보자.

⚡️ 컨트롤러의 역할

  • 1단계 : 요청이 발생하면 요청을 접수

  • 2단계 : 접수한 요청에 대한 유형 분석 및 유형에 적합한 하위 컨트롤러에게 요청 위임

  • 3단계 : 요청 처리에 따른 비즈니스 로직 수행 지시

  • 4단계 : 처리 결과가 있을 경우 결과를 저장

  • 5단계 : 저장된 결과를 출력하기 위하여 view로 전송

컨트롤러의 5가지 역할 중 1단계, 2단계, 5단계의 역할을 하는 것이 dispatcherServlet이다. 그렇기 때문에 이전 예제에서는 dispathcerServlet이 유형을 분석하기 쉽도록 handleMappingviewResolver를 사용하여 비즈니스로 로직을 수행할 하위 컨트롤러의 인스턴스를 매핑하였으며, 처리 결과를 출력할 view를 viewResolver를 통해 전달하여 주었다. 이번 게시글에서는 해당 역할을 dispatcherServlet안에서 매핑파일을 읽어와 자동으로 수행되도록 코드를 작성해보겠다.


DispatcherServlet 예시(2)

⚡️ 규칙

  • handleMapping(컨트롤러 매핑)과 viewResolver(view 경로 반환) 미사용
  • 다른 코드가 수정되어도 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 객체를 사용하는 것이다. propertiesHashTable을 상속하고 있으며, HashTable은 곧 Map이기 때문에 Key와 Value로 호출하여 사용이 가능하다. properties객체의 getProperty()를 호출할 때 ()안에 매핑 파일의 Key값을 입력해주면 된다. 참고로 '='으로 기준으로 좌측은 Key, 우측은 Value이다.
그렇다면 매핑 파일과 dispatcherServlet을 연결하기 위해서 Web.xml은 어떻게 세팅이 되어있는지 확인해보자.

🖥️ 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의 동작순서를 다시 한번 알아보자.

⚡️ 흐름

전체 코드를 설명하는 것은 너무 장황하고 효율이 떨어지니 컨트롤러의 역할 측면에서 단계별로 어떤 코드가 사용되었는지 확인하고 내부적으로 어떤 동작들이 흘러가는지 확인하자.

🖥️ 1단계 : 요청 접수

예를 들어 게시물을 보기 위해 사용자가 '게시물 목록 보기'라는 버튼을 클릭했다고 가정하자.

요청이 최초로 발생하면 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이 생성되는 것이다.

🖥️ 2단계 : 요청 유형 분석 및 위임

자 이제 요청을 접수했으니 해당 요청을 분석하고, 요청 유형에 맞는 하위 컨트롤러에게 위임해보겠다.

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()해준다. 이 상태가 되어야지 해당 컨트롤러의 인스턴스화가 완료된 것이다. 여기서 컨트롤러가 수행하는 메서드명은 각각의 컨트롤러가 동일하며, 내부로직은 요청 처리에 적합하게 오버라이드되어있다. 컨트롤러 코드는 이전 게시글을 참고하자.

🖥️ 3, 4단계 : 요청에 적합한 비즈니스 로직 수행 지시

요청을 위임받은 하위 컨트롤러는 해당 요청을 처리하기 위해서 Model에게 로직 수행을 지시한다.(아직 Service의 개념을 도입하지 않은 상태임을 감안하기 바란다.) Model이 로직을 수행하면서 DB에 접근하여 필요한 데이터를 받아오거나 데이터를 DB에 저장만하고 비즈니스 로직이 완료되어질 수 있다. 아래 그림을 보자.

3단계까지 완료하고, 결과가 있다면 request 객체에 저장하고 view경로를 반환한 상태로 하위 컨트롤러의 임무는 종료되어진다. 이를 전달받은 dispatcherServlet은 5단계를 진행한다.

🖥️ 5단계 : 결과 및 View 경로 전달

비즈니스 로직까지 수행이 완료되면 남은 일은 클라이언트에게 해당 결과를 전달하는 것이다. 이때 두가지 상황으로 나뉠 수 있다.

  • 저장해야할 결과가 있는 경우(예 : 리스트 목록 등)
  • 저장해야할 결과가 없는 경우(예 : 글 등록, 삭제 등)

이런 상황을 판단하기 위해 컨트롤러로부터 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를 할 경우 어떤 문제가 있는지 알아보자. 그전에 forwordredirection에 대해서 간략하게 설명하겠다.

  • forword : 요청 결과가 있는 경우 request 객체해 해당 요청결과를 저장하여 지정한 경로로 전송. 이때, 출력되는 URI은 최초 요청 발생 url로 유지
  • redirection : 요청 결과가 없는 경우 해당 페이지로 재접속하는 개념. 출력되는 url은 실제로 보고있는 페이지의 URI 출력

너무 간단하게 작성해서 이해가 되지 않을 것이다. 지금은 결과가 있을때는 forword, 없을때는 redirection을 쓴다는 정도로만 이해해두자. 다시 본론으로 돌아와 위에서 게시물을 등록하고 forword를 해버리면 발생하는 문제점이 무엇인지 알아보겠다.

  • 최초 /board/regist.do로 요청이 발생
  • 요청에 대한 모든 로직이 수행되고 결과 저장없이 view 경로만 반환
  • forword로 인해서 브라우저에 출력되는 경로의 변화없이 응답완료
  • 최종적으로 남아있는 URI는 최초와 동일하게 /board/regist.do로 출력

모든 작업이 완료되어도 최초 요청과 같은 URI가 남는다면 브라우저를 새로고침할 경우 동일한 작업을 반복수행하게 되는 결과를 맞이하게된다. 그렇기 때문에 결과의 여부를 판단하여 forwordredirection 수행에 대한 판단을 해야하는 것이다.

정리하자면 아래의 그림과 같다.


마무리

이렇게해서 이번 게시글에서는 특정 file에서 매핑 정보를 읽어와 dispatcherServlet에서 자동으로 매핑하는 MVC 모델의 front controller를 작성해보았다.

⚡️ 느낀점

  • Properties 객체를 이용하여 xml 및 json를 이용한 매핑설정 방법에 대한 기초를 경험함.
  • 요청에 대한 URI 분석을 통해 개발자가 직접 객체를 생성하는 것이 아닌 매핑에 의하여 생성되는 유연성을 경험함.
  • 한번 정의된 클래스를 외부의 영향없이 지속사용할 수 있는 것에 대하여 경험하고, 결합도가 낮을수록 코드의 재사용 및 유지보수가 편해진다는 것을 경험하였음.
  • 요청 처리에 대하여 결과가 있는 경우와 없는 경우 등을 가정하여 forword 및 redirection을 처리하는 절차를 확인하였고, 각 상황에서 발생하는 문제점을 알 수 있었음.
  • 인터페이스 사용을 통해 클래스를 다중상속 받지 못하는 단점을 보완하는 방법을 확인함.

위 예제를 작성하면서 MVC모델의 전반적인 흐름과 Spring을 들어가기에 앞서 어떻게 설정을 하고 동작하는지에 대하여 알 수 있었다.

그럼 이만.👊🏽

해당 글에 대한 전체 코드는 여기를 참고하면된다.

profile
서핑하는 개발자🏄🏽

0개의 댓글