Spring MVC : 가장 기초적인 부분들 정리

    이 게시글은 인프런의 김영한님 강의를 듣고 개인적으로 정리한 글입니다.

     

    @Component + Controller 인터페이스 구현(BeanNameUrlHandlerMapping)


    가장 원초적인 컨트롤러는 @Component + Controller 인터페이스를 구현하는 것으로 사용할 수 있다. 코드부터 보면 아래와 같다.

    @Component("/springmvc/old-controller")
    public class OldController implements Controller {
    
        @Override
        public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
            System.out.println("OldController.handleRequest");
            return null;
        }
    }
    • @Component로 이 클래스가 스프링 빈으로 등록되게 한다. 그리고 그 스프링 빈의 이름은 "/springmvc/old-controller"로 설정을 해둔다.
    •  Controller 인터페이스를 구현하도록 해둔다. 

    위와 같이 설정한 후  "localhost:8080/springmvc/old-controller"로 접속해본다. 그러면 "OldController.handlerRequest"가 콘솔창에 실행되는 것을 볼 수 있다. 어떻게 이런 일이 가능하게 된 것일까? 

     

    @Component + Controller 인터페이스 구현체가 URL 호출 시, 불려진 이유는? 


    위의 이미지는 스프링 MVC의 전체 구조다. 앞에서 구현했던 FrontController의 역할을 하는 것이 Dispatcher Servlet이다. Dispatcher Servlet은 모든 URL 요청이 왔을 때, 이쪽으로 접근하게 된다. 접근 URL은 "/*"이 된다.

    1. localhost:8080/springmvc/old-controller라는 URL로 접근했기 때문에 DispatcherServlet으로 들어오게 된다.
    2. DispatcherServlet은 내부적으로 HandlerMapping을 가지고 있으며, HandlerMapping에서 URL 매칭이 되는 값을 찾아오게 된다.

    간단히 이야기 하면 위의 순서대로 진행이 되었기 때문에 Old-Controller가 실행되었다. @Component로 이 클래스가 스프링 빈으로 등록되었고, Controller를 구현했기 때문에 Controller로 스프링이 인식을 하는 것이다. 이런 형태로 구현된 Controller를 BeanNameUrlHandler라고 한다. 또한, 이 Handler를 사용하기 위해서는 Handler에 맞는 Adapater가 필요하다. 이 두 가지가 구현되어 있으면 우리는 정상적으로 MVC에서 해당 컨트롤러를 사용할 수 있게 된다.

    그렇다면 Handler와 Handler Adapater는 누가 만들어두는 것일까? 바로 스프링 프레임워크가 필요한 핸들러와 핸들러 어댑터를 다 만들어준다.

     

    스프링 MVC 구조 자세히 살펴보기


    DispatcherServlet

    • DispatcherServlet은 FrontController와 동일하게 동작한다.
    • DispatcherServlet은 URL Mapping이 /*로 되어있다.
    • DispatcherServlet은 HttpServlet을 상속받아서 사용한다.
    • 스프링부트는 DispatcherServlet을 서블릿으로 등록하면서 내장 톰캣 서버를 띄운다.
    • DispatcherServlet은 다음과 같은 형태로 동작한다
      1. HttpServelt이 제공하는 doService()가 호출된다. 
      2. doService()내의 doDispatcher()가 실행된다. Handler,  Adapter를 찾아준다.
        1. ModelAndView 초기화한다.
        2. getHandler() 메서드를 통해서 Handler를 찾는다. 없으면 404 Return한다.
        3. getHandlerAdapter()를 통해서 Adapater를 찾는다.
        4. Adapter.Handle() 실행 한다.
      3. view.render() 메서드로 Model, Request, Response가 넘어가며 View가 렌더링 된다. 

     

    참고사항 : 경로는 더 자세한 경로가 우선순위가 높다. 따라서, 기존에 등록된 @WebServlet이 있다면, 그 경로가 우선적으로 접근되게 된다. 

     

    HandlerMapping

    HandlerMapping에서는 기본적으로 스프링이 자동으로 생성해서 이런 저런 메서드들을 등록해둔다. DispatcherServlet은 이 HandlerMapping에서 URL에 대응되는 Handler를 찾아서, 우선순위대로 먼저 Return해주는 역할을 한다. 

    • 0순위 : RequestMappingHandlerMapping
      • @Controller + @Request로 만든 어노테이션 기반 Controller
    • 1순위 : BeanNameUrlHandlerMapping
      • @Component + Controller Interface 구현한 컨트롤러 

    DispatcherServlet은 내부에 List와 Map을 가지고 있는데, URL 맵핑되는 핸들러를 찾기 위해 List의 Loop문을 돌린 후 찾고, 거기서 Map까지 한번 더 뒤져 HTTP 메서드(GET/POST)도 제대로 맵핑되는지를 확인한 후에 값을 Return 해준다.

     

    HandlerAdapter

    HandlerMapping에서 가져온 Handler에 맞는 Adapater를 DispatcherServlet이 찾아준다. 이 때는 Handler를 넘겨서, Handler에 맞는지 support() 메서드를 활용해서 Loop문으로 확인한다. 그리고 맞을 경우 우선순위에 맞게 반환해준다.

    • 0순위 : RequestMappingHandlerAdapater
      •  RequestMapping에 대응됨
    • 1순위 : HttpRequestHandlerAdapter
      •  HttpRequestHanlder에 대응됨
    • 2순위 : SimpleControllerHandlerAdapater
      •  BeanNameUrlHandlerAdapter에 대응됨

    당연한 이야기지만 해당 핸들러로 호환이 되는 어댑터 중에서 가장 우선순위가 높은 것을 뽑아준다.

     

    ViewResolver

    ViewResolver는 반환된 논리적 ViewName을 실제 ViewPath를 가진 View로 만들어주는 역할을 한다. ViewResolver도 Interface화 되어있으면, 필요한 구현체들을 스프링이 구현해서 스프링 컨테이너에서 사용이 가능하도록 해준다. ViewResolver도 HandlerMapping, HandlerAdapter처럼 우선순위를 가진다.

    • 0순위 : BeanNameViewResolver
      • 엑셀 파일 생성 기능등에 사용
    • 1순위 : InternalResourceViewResolver
      • JSP처럼 내부 Forward()로 이동하는 자원들에게 사용
    • 기타 ThymeleafViewResolver
      • ThymeLeaf전용 ViewResolver

     

    간단한 예시로 위의 OldController를 다시 한번 확인해본다. 

    @Component("/springmvc/old-controller")
    public class OldController implements Controller {
    
        @Override
        public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
            System.out.println("OldController.handleRequest");
            return new ModelAndView("new-form");
        }
    }

     

    현재 코드를 살펴보면 위와 같다. 실제로 위의 컨트롤러 URL인 old-controller로 접속을 해보면 화이트 페이지가 뜬다. 왜 이렇게 되는 것일까? 바로 ModelAndView에 ViewName으로 "new-form"을 넣어주었기 때문이다. 이 "new-form"은 View의 논리적인 이름이고 실제 ViewPath로 변환해주는 작업이 필요하다. 이런 작업을 바로 ViewResolver가 해준다.

    '' apllication.properties
    spring.mvc.view.prefix="/WEB-INF/views/"
    spring.mvc.view.suffix=".jsp"

    JSP처럼 내부적으로 Forward()로 넘어가야 하는 것들을 InternalResourceViewResolver가 처리를 해준다. 이 ViewResolver를 이용해서 ViewPath를 만들기 위해서는 application.properties에 위의 코드를 넣어주어야 한다. 접미사와 접두어는 필요한 형태로 바꾸면 된다. 이렇게 되면 위의 접미사 + 접두어가 반영된 InternalResouceViewResolver가 스프링 컨테이너에 등록되게 되는 것이다.

    실제로 스프링부트가 해주는 일은 다음과 같다.

    @Bean
    InternalResourceViewResolver internalResourceViewResolver(){
    	return new INternalResourceViewResolver("/WEB-INF/view/", ".jsp");
    }

     

    ViewResolver의 동작 방식을 하나씩 살펴보면 다음과 같다.

    1. HandlerAdapter의 실행 결과로 ModelAndView를 반환받았다.
    2. 논리 VIewName으로 ViewResolver가 호출되어야한다. ViewResolver가 호출될 때, 스프링부트가 자동 등록하는 ViewResolver들이 우선순위를 가지고 하나씩 호출된다.
    3. 'new-form'이라는 ViewNam에 맞는 스프링 빈으로 등록된 ViewResolver를 찾아야 하는데 없다. 따라서 internalResourceViewResolver가 호출된다.
    4. 실행 결과로 InternalResorceView가 반환되고, 이 View가 Render()되서 JSP를 실행한다. 

     

     

    HttpRequestHandler + HttpRequestHandlerAdapater 사용해보기


    @Component("/springmvc/request-handler")
    public class MyHttpRequestHandler implements HttpRequestHandler {
        @Override
        public void handleRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
            System.out.println("MyHttpRequestHandler.handleRequest");
        }
    }
    

    HttpRequestHandler를 사용하기 위해서는 다음과 같이 구현하면 된다

    • @Component로 스프링 빈에 등록해줌
    • HttpRequestHandler 구현해준다.

    의 조건을 만족하면 스프링은 해당 클래스를 HttpRequestHandler로 인식한다. 이렇게 될 경우 동작은 어떻게 될까? 

    • 1. HandlerMapping을 순서대로 실행해서 URL과 매칭되는 핸들러를 찾는다.
      • 이 때, 빈 이름으로 핸들러를 찾아주는 BeanNameUrlHandler가 실행에 성공한다.
    • 2. handlerAdapter에서 supports()를 순서대로 호출한다. 
      • 이 때, HttpRequestHandlerAdapter가 HttpRequestHandler Adapter를 지원하므로 대상이 된다.
    • 3. DispatcherServlet은 HandlerAdapter를 대상을로 handle() 메서드를 사용해 서비스 + 비즈니스 로직을 실행한다. 

    여기서 사용된 것을 정리하면 다음과 같다.

    • HandlerMapping = BeanNameUrlHandlerMapping
    • HandlerAdapter = HttpRequesteHandlerAdapter

    댓글

    Designed by JB FACTORY