Framework/Spring

[IBAS] Spring MVC의 Handler Mapping 동작 원리 (2) Customizing 편

MINGYUM 2022. 2. 15. 22:07

https://www.baeldung.com/spring-handler-mappings

이 문서를 참고해보면 다음과 같이 나와있다. 

In Spring MVC, the DispatcherServlet acts as front controller – receiving all incoming HTTP requests and processing them.
Simply put, the processing occurs by passing the requests to the relevant component with the help of handler mappings.
HandlerMapping is an interface that defines a mapping between requests and handler objects. While Spring MVC framework provides some ready-made implementations, the interface can be implemented by developers to provide customized mapping strategy.
This article discusses some of the implementations provided by Spring MVC namely BeanNameUrlHandlerMapping, SimpleUrlHandlerMapping, ControllerClassNameHandlerMapping, their configuration, and the differences between them.

HandlerMapping을 구현하여 목적을 달성할 수 있다. 

 

먼저 HandlerMapping을 구현한 예시 중에 하나인 BeanNameUrlHanderMapping 코드를 살펴보자. 

가장 기본적으로 Spring MVC에서 제공되는 Mapping 방식이다. 

 

구현되는 구조는 다음과 같고, 개발자가 Customizing한 구현체도 같은 방식으로 구현하면 될 듯하다. 

참고로, SimpleUrlHandlerMapping의 경우 AbstractDetectingUrlHandlerMapping을 거치지 않고 AbstractUrlHanderMapping에서 바로 상속을 받는다. )

 

https://programmer.group/customized-request-mapping-handler-mapping-for-spring-mvc.html

 

Customized Request Mapping Handler Mapping for Spring Mvc

Keywords: Spring xml encoding JSP Above Request Mapping Handler Mapping Matching in Spring MVC We mentioned that Spring not only matches url, method, contentType and other conditions, but also matches the customized conditions provided by users. The custom

programmer.group

Handler mapping customizing에 대한 자료가 너무 없음..

 

이 자료의 본문을 간단하게 해석하자면

 

1.   한 housing resources 판매 사이트를 운영하고 있고, 사용자마다 고유한 웹사이트 도메인을 제공하고 있다. 
이 디자인의 장점은 사용자가 웹 사이트를 SEO해야 하는 경우 통합된 방식으로 처리할 수 있고, 보조 도메인 이름을 통해 현재 웹 사이트의 사용자가 누구인지 알 수 있다는 것이다.
2.  문제는 같은 page에서 다른 template를 가지고 있기 때문에 page마다의 세부적인 디테일이 다른 것이다. 따라서 인터페이스를 사용할 수 있는데, 이 경우 Client가 인터페이스를 호출할때마다 현재 사용자가 사용하고 있는 템플릿들을 결정하고 다른 인터페이스를 만들어야하는 것이다. 
템플릿 페이지가 늘어날수록, 코드를 유지보수하기가 힘들어지는 상황이다. 
3. 사용자의 현재 템플릿과 상관없이 front-end에서 같은 페이지를 요청하면 동일한 URL을 사용하여 요청할 수 있으므로 복잡도가 감소된다. 즉 a.house.com/user/detail 의 도메인에 접근했을 때, 서버가 도메인 이름으로 현재 템플릿 셋을 가져올 수 있도록 인터페이스에 요청을 배포해야한다. 

 


위의 자료들을 참고해 Handler Mapping을 구현하고자 하였다.

@Configuration
@RequiredArgsConstructor
public class BeanNameUrlHandlerMappingConfig {

    private final BoardService normalBoardService;

    private final ContestBoardService contestBoardService;

    private final MenuRepository menuRepository;

    @Bean
    BeanNameUrlHandlerMapping beanNameUrlHandlerMapping(){
        return new BeanNameUrlHandlerMapping();
    }

    
    @Bean("/board/all")
    public BoardController boardController() {
        RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
        if (attributes instanceof NativeWebRequest) {
            HttpServletRequest request = ((HttpServletRequest) ((NativeWebRequest) attributes).getNativeRequest());
            MenuType menuType = menuRepository.findMenuTypeByMenuId(Integer.parseInt(request.getParameter("menuId"))).get();
            switch (menuType) {
                case CONTEST:
                    return new ContestBoardController(contestBoardService);
                default:
                    return new BoardController(normalBoardService);
            }
        }
        return null;
    }

      protected void ThreadSafeMethod (HttpServletRequest request){ }
}

처음에는 이렇게 request 객체를 (억지로) 가져와서 menuType을 찾는데 필요한 정보를 URL에서 빼오고, 

그에 맞는 Controller를 반환하면 되는 줄 알았다. 

그런데 여기서 첫번째 문제 발견

@Configuration
@RequiredArgsConstructor
public class BeanNameUrlHandlerMappingConfig {

    private final ContestBoardService contestBoardService;

    @Bean
    BeanNameUrlHandlerMapping beanNameUrlHandlerMapping(){
        return new BeanNameUrlHandlerMapping();
    }

    @Bean
    public RequestContextListener requestContextListener(){
        return new RequestContextListener();
    }

    @Bean("/board/all/6")
    public ContestBoardController contestBoardController(){
        return new ContestBoardController(contestBoardService);
    }

}

그래서 저렇게 리팩토링한 코드에서 두 번째 문제 발견

정리하자면, 

핸들러 매핑으로 menuId와 Controller를 매핑하는 것의 문제점

1. Request 객체를 정상적으로 가져올 수 없음 - Server가 켜질 때부터 애초에  모든 Controller 의 handler method와 url이 매핑되어있어야 한다. 그래서 Bean NameUrlHandlerMapping으로 menuId = 6에 대한 정보를 '미리' 가져와 처리할 수 없음. request 객체가 생성되기 전에 미리 매핑할 수 없으므로

2. 중복 불가. - 위의 내용과 조금 겹칠 수 있는데, 이미 Controller에는 url 매핑이 모두 이루어져 있다. 그 와중에 (api/board/all)에 대한 url을 매핑하려고 시도해봤자, menuId의 값과 상관없이 같은 url로 인식되기 때문에 error

 

.... 이러한 이유로 핸들러 매핑은 막을 내렸다. 

 

추가적으로 Interceptor를 사용해서 request 객체를 받은뒤 menuId를 추출해서 MenuType을 찾는 것까지 생각해봤지만

Inteceptor 특성상 다른 Controller로 매핑하는 것은 안되고 

preHandle을 거친 다음에 무조건 지정된 Controller로 매핑되기 때문에 적합하지 않다고 판단

 

 

그래서 생각해 낸 2차 방법론은 바로 

Front Controller Pattern을 본 떠, board와 관련된 모든 URL 요청을 받는 BoardController를 프론트 컨트롤러로 설정해 작업을 한 곳에서 수행하고, menuId로 ContestBoardController를 매핑하는 방식으로 진행하면 될듯하다. 

 

이러한 역할을 Spring MVC에서는 Dispatcher Servlet이 수행해주고 있었고,

 이 로직을 그대로 본따와서 BoardController와 NormalBoard를 확장한 Entity들의 Controller를 매핑하면 된다. 

 

이는 다음 포스팅에서 이어서 작성하겠다.