ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Spring] Dispatcher Servlet에서 핸들러가 저장하고, 매핑되는 과정
    백엔드/Spring 2023. 4. 21. 23:49
    반응형

    프로그램이 실행되면 어떻게 핸들러가 만들어지고, HTTP 요청을 하면 어떻게 매핑이 되는 것일까?

     

     

     

    1. 서론

    한 크루가 컨트롤러 클래스를 @Bean으로 등록해서 사용하려니까 오류가 발생했었다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    @Configuration
    public class SpringConfig {
     
        private final DataSource dataSource;
     
        @Autowired
        public SpringConfig(final DataSource dataSource) {
            this.dataSource = dataSource;
        }
     
        @Bean
        public RacingGameController racingGameController() {
            return new RacingGameController(gameService());
        }
     
        @Bean
        public GameService gameService() {
            return new GameService(gameRepository());
        }
     
        @Bean
        public GameRepository gameRepository() {
            return new JdbcTemplateGameRepository(dataSource);
        }
    }
    cs

     

     

     

    이것만 보면 별 문제가 없어보이지만, 테스트 코드 실행시 아래와 같은 에러 로그가 나오게 된다.

    (다른 에러 로그는 보이지 않음)

     

    404 에러니까 우선 다른 실수가 있어 Bean이 제대로 등록되지가 않는지 궁금하여 등록된 Bean 목록을 확인해보았다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
    class Test {
     
        @Autowired
        ApplicationContext ctx;
     
        @Test
        void test() {
        // Bean 등록된 내용을 출력
            String[] beanNames = ctx.getBeanDefinitionNames();
        for (String beanName : beanNames) {
            System.out.println(beanName);
        }
        }
    cs

    @SpringBootTest가 실제 서버를 돌릴 때 등록하는 모든 Bean을 그대로 등록해준다.

     

    콘솔창을 확인해보니 RacingGameController는 Bean으로 잘 등록이 된 것을 확인할 수 있었다.

    분명 잘 동작하는 것 같은데, 404 에러니까 컨트롤러에 분명 문제가 있는 것 같았다.

    하지만 제대로된 에러 로그가 안보이니.. 어느 부분이 왜 틀렸는지 전혀 갈피를 못 잡고 있었다.

     

     

    찾아보니 진짜 문제는....


    2. 본론

    Dispatcher Servlet이 컨트롤러 클래스만 골라서 핸들러를 매핑하는데에서 문제가 발생한다.

     

    이 말은 컨트롤러 클래스를 인식하는 기준이 따로 있다는 것인데, 어떤 기준으로 컨트롤러 클래스를 인식하는 것일까?

    내부 코드가 복잡하긴한데, 정리해보면 아래와 같은 순서로 등록하게 된다.

     

    1. 등록된 Bean을 모두 가져와서 for문을 돌린다.
    2. Bean이 컨트롤러 클래스인지 확인한다.
      (@Controller, @RequestMapping만 컨트롤러 클래스로 인정)
    3. 컨트롤러 클래스라면 for문으로 해당 클래스가 가지고 있는 메서드를 모두 확인해본다.
    4. 이 과정에서 메서드가 @RequestMapping을 가지고 있는지 확인하여 Map<Method, ?> methods에 저장한다.
    5. 이후 methods 에 저장된 모든 값을 확인하며 핸들러로 등록을 한다.

     

    여기서 1번, 3번, 4번은 글만 봐도 이해가 될 것이다. 하지만 2번과 5번은 이대로 설명하기엔 팩트체크가 필요하기도 하고, 어떤 내용을 저장하는지 궁금하기도 하다.


    [2 - Bean이 컨트롤러 클래스인지 확인]

    어떻게 @Controller, @RequestMapping만 골라서 가져오는 것일까? 

    답은 RequestMappingHandleMapping.java에 isHandler라는 메서드가 존재한다.

     

    1
    2
    3
    4
    5
    @Override
    protected boolean isHandler(Class<?> beanType) {
            return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class||
                    AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class));
    }
    cs

     

    hasAnnotation(beanType, Controller.class)는 Bean에 등록된 클래스(beanType)가 가진 어노테이션 중에 @Controller를 가지고 있는지 확인하는 것이다.

    여기서 주의할 점은 isAnnotation과 hasAnnotation의 차이인데, isAnnotation은 beanType에 직접적으로 등록된 어노테이션이 @Controller인지 판별하는 것이다.

    만약 @RestController를 사용했으면 isAnnotation(beanType, Controller.class)는 false가 나오게 된다.

    하지만 @RestController 안에 @Controller가 있으므로 hasAnnotation(beanType, Controller.class)는 true가 나오게 된다.

     

    디버그 모드를 활용해 @RestController의 데이터를 까보면, annotationData의 annotations라는 곳에 @Controller가 저장되어 있는 것을 확인할 수 있다.

     


    [5 - methods 에 저장된 모든 값을 확인하며 등록]

    methods는 컨트롤러 클래스가 가지는 메서드들중에 @RequestMapping를 가지고 있는 메서드들만을 저장한 것이다.

    AbstractHandlerMethodMapping.java에 들어가보면 내부 클래스로 MappingRegistry 클래스를 가지고 있다.

     

    여기에 나오는 registry, pathLookUp, nameLookUp, corsLookUp에 데이터를 저장하고, readWriteLock을 통해 기록할 때 락을 걸어 동시성 문제를 예방한다.

     

    - 등록 내용

    • pathLookUp: URL 경로와 핸들러 메서드 정보를 등록
    • nameLookUp: 핸들러 메서드 정보와 사용자 정의 이름을 등록
    • corsLookUp: 핸들러 메서드와 CORS 설정을 등록
    • registry: 위에서 저장했던 모든 정보를 재등록

     

    pathLookUp이 HTTP 요청을 하면 무조건 확인하게 되어있다.

    nameLookUp의 경우엔 로그로 어떤 메서드를 실행했다는 기록을 남기는 상황처럼 메서드 이름이 필요한 경우에만 찾아본다

    corsLookUp도 CORS 설정이 있는 경우에만 찾아본다.

    registry의 경우엔 한 군데에서만 사용이 되던데, 요청이 들어올 때 @RequestMapping 경로와 HTTP Method와 일치하는 값이 존재하는지 판별하는 용도로만 쓰인다.


    [클라이언트가 /plays로 POST 요청을 보낼 경우]

    다양한 데이터가 들어오겠지만, 우리는 2개만 보면 된다.

    - lookUpPath = "/plays"

    - request = {POST, /plays, ...}

     

    1. pathLookUp에서 lookUpPath를 key로 가지는 value들을 가지고 옴
      directPathMatches = ["{POST [/plays]}", "{GET [/plays]}"]
    2. directPathMatches에서 request 정보와 일치하는 값들을 matches에 저장함
      matches = "{POST [/plays]}"
    3. matches의 값이 여러 개인 경우, 최선의 값을 선택하는 로직이 동작하여 하나의 핸들러를 bestMatch에 저장함
      여기서는 1개뿐이니 bestMatch는 "{POST [/plays]}"임
    4. bestMatch에 저장된 메서드를 실행함
    5. 이후 나머지 Dispatcher Servlet의 로직을 수행함

     

    bestMatch에는 아래 데이터를 가지고 있는데, 여기서 실행해야할 메서드 이름까지 가지고 있다.

     

    3번에서는 최선의 값을 선택하는 로직도 동작하고, 가끔씩 실수를 할 때 나오는 에러 로그도 여기서 나오게 된다.

    바로 URL과 HTTP Method가 같은 경우이다.

    주로 서로 다른 컨트롤러 클래스에서 /plays 경로로 POST 요청을 처리하는 메서드를 구현했다면, matches는 "{POST [/plays]}" 값을 2개나 가지고 있게 된다. (요청할 때 보내는 데이터도 완전히 같다고 가정)

    이 코드를 보면 bestMatch 말고도, secondBestMatch를 구하게 된다.

    이때 bestMatch와 secondBestMatch가 같은 우선순위를 가지게 된다면 URL과 HTTP Method, 보내는 데이터가 같다는 뜻이므로 어느 핸들러를 실행할지 판단을 할 수 없게 된다.

    그래서 은근히 본 에러 메시지인 "Ambiguous handler methods ..."가 나오게 된다.


    3. 결론

    컨트롤러 클래스는 @Controller 또는 @RequestMapping 어노테이션을 가지고 있어야 Dispatcher Servlet이 컨트롤러 클래스라고 인식을 하게 된다.

     

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
    class Test {
     
        @Autowired
        RequestMappingHandlerMapping handlerMapping;
     
        @Test
        void test() {
        // 저장된 HTTP Method + URL 출력
            final Map<RequestMappingInfo, HandlerMethod> handlerMethods = handlerMapping.getHandlerMethods();
        for (RequestMappingInfo requestMappingInfo : handlerMethods.keySet()) {
            System.out.println(requestMappingInfo);
        }
        }
    cs

    이 코드를 통해 핸들러 목록을 파악할 수 있다.

     

    만약 컨트롤러 클래스에 @Controller 또는 @RequstMapping이 들어있으면 아래와 같은 내용이 출력된다.

    이걸 실행한 코드에는 /plays에 POST 요청을 하는 메서드만 존재해서 3개의 핸들러만 나오게 된다.

     

    하지만 컨트롤러 클래스에 @Bean, @Component를 붙이면 에러 핸들러만 나오게 된다.

    컨트롤러 클래스로 인식을 하지 못해서 핸들러 등록조차 되지 않는다.

     


    어차피 다들 컨트롤러 클래스를 사용하면 @Controller, @RestController는 무조건 달고 있긴하다. 그런데 누군가가 어떻게 등록되는지 궁금해하고, 클라이언트가 요청하면 어떻게 동작하는지 물어본다면 이 내용을 통해 아는척을 해보자

     

    반응형

    댓글

Designed by Tistory.