티스토리 뷰

Spring

[Spring] Spring에서 API에 매핑되는 컨트롤러와 메소드 조회하여 직접 호출하기(HandlerMapping과 HandlerMethod)

망나니개발자 2022. 3. 15. 01:38
반응형

최근에 어디에선가 API에 매핑되는 컨트롤러와 메소드를 찾는 방법이 있냐는 질문을 보게 되었습니다. 마침 최근에 디스패처 서블릿 코드를 보면서, 디스패처 서블릿이 어떻게 컨트롤러로 요청을 위임하는지 알게 되었는데, 이번에는 어떻게 이러한 문제를 해결할 수 있는지 살펴보도록 하겠습니다.

 

 

 

1. HandlerMapping과 HandlerMethod 간단히 살펴보기


[ 핸들러 매핑(HandlerMapping) ]

핸들러 매핑(HandlerMapping)과 RequestMappingHandlerMapping 클래스

스프링은 컨트롤러와 메소드 정보를 관리하고 있다가, 요청이 왔을 때 디스패처 서블릿이 어느 컨트롤러가 이를 처리해야하는지 식별하고 위임한다. 컨트롤러에는 @RequestMapping 관련 어노테이션이 사용되므로 이를 기반으로 요청 매핑 정보를 관리하고, 요청이 왔을 때 이를 처리하는 대상(Handler)를 찾는 클래스가 바로 RequestMappingHandlerMapping이다.

이러한 RequestMappingHandlerMapping은 AbstractHandlerMethodMapping를 상속받고 있으며, AbstractHandlerMethodMapping에서 내부적으로 관리하는 MappingRegistry 클래스를 가지고 있다.

public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMapping implements InitializingBean {

    private final MappingRegistry mappingRegistry = new MappingRegistry();
    
    class MappingRegistry {

        private final Map<T, MappingRegistration<T>> registry = new HashMap<>();

        ...
    }

    ...
    
    static class MappingRegistration<T> {

        private final T mapping;

        private final HandlerMethod handlerMethod;

        private final Set<String> directPaths;

        @Nullable
        private final String mappingName;

        private final boolean corsConfig;
    }
    
    ...
}

public abstract class RequestMappingInfoHandlerMapping extends AbstractHandlerMethodMapping<RequestMappingInfo> {

}

public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMapping
		implements MatchableHandlerMapping, EmbeddedValueResolverAware {

}

 

 

실제로 요청 매핑 정보를 관리하는 곳은 MappingRegistry라는 클래스이다. 그러므로 MappingRegistry에 대해 살펴보도록 하자.

 

 

 

MappingRegistry의 registry 클래스

MapppingRegistry는 매핑 정보를 관리하는 클래스이며, 실제 매핑 정보는 MappingRegistry의 registry 변수에서 관리된다. registry는 HashMap으로써 (Key, Value)로 각각 (요청 정보, 요청을 처리할 대상 정보)을 저장하는데, 스프링 애플리케이션은 실행될 때 모든 컨트롤러를 파싱해서 해당 정보들을 다음과 같이 관리한다.

 

 

그리고 각각의 Key, Value 클래스는 각각 다음과 같은 특징을 지닌다.

  • Key(요청 정보)
    • 클래스: RequestMappingInfo
    • 특징: Http Method와 URI를 포함해 헤더, 파라미터 등의 조건을 가짐
  • Value(요청을 처리할 대상 정보)
    • 클래스: MappingRegistration
    • 특징: RequestMappingInfo와 HandlerMethod 등으로 구성됨

 

 

여기서 우리는 HandlerMethod에 주목해야 한다. HandlerMethod에는 매핑되는 컨트롤러의 메소드와 컨트롤러 빈 정보(컨트롤러 빈 이름 또는 컨트롤러 빈이 될 수 있음) 및 빈 팩토리 등이 저장되어 있다. (왜 이러한 것들을 가지고 있는지는 뒤에서 설명된다.)
디스패처 서블릿은 요청이 오면 요청 정보를 파싱해 RequestMappingInfo 객체를 생성하고, Map의 key로써 요청을 처리할 대상 정보를 꺼낸다. 이를 위해 당연히 RequestMappingInfo의 hashCode는 오버라이딩 되어있다.

(equals와 hashcode에 대해 잘 모르면 여기를 참고해주세요)

 

그리고 Value 값인 MappingRegistration 객체가 갖고 있는 HandlerMethod를 사용해서 컨트롤러에 요청을 위임하는 것이다. 그러므로 우리도 디스패처 서블릿이 동작하는 것처럼 RequestMappingInfo 객체를 생성하고 Value 값을 가져오면 될 것 같은데, 실제로 코드를 작성할 때에는 조금 다르게 처리를 해야 한다. 관련 내용은 실제로 코드를 작성하면서 살펴보도록 하자.

그리고 그 전에 핸들러 어댑터가 필요한 이유를 찾아보도록 하자.

 

 

 

 

[ 핸들러 어댑터(HandlerAdapter) ]

핸들러 어댑터가 필요한 이유

디스패처 서블릿은 컨트롤러로 요청을 직접 위임하는 것이 아니라 HandlerAdapter를 통해 컨트롤러로 위임한다. 이때 어댑터 인터페이스가 필요한 이유는 컨트롤러의 구현 방식이 다양하기 떄문이다. 최근에는 @Controller에 @RequestMapping 관련 어노테이션을 사용해 컨트롤러 클래스를 주로 작성하지만, Controller 인터페이스를 구현하여 컨트롤러 클래스를 작성할 수도 있다. 스프링은 HandlerAdapter라는 어댑터 인터페이스를 통해 어댑터 패턴을 적용함으로써 컨트롤러의 구현 방식에 상관없이 요청을 위임할 수 있는 것이다.

 

 

 

핸들러 어댑터(HandlerAdapter)와 RequestMappingHandlerAdapter

@Controller에 @RequestMapping 관련 어노테이션으로 구현된 컨트롤러가 요청을 처리할 대상이면  RequestMappingHandlerMapping가 찾아진다. 그리고 찾아진 HandlerMapping을 처리할 어댑터를 찾아야 하는데, 이를 처리할 어댑터는 RequestMappingHandlerAdapter이다. 대부분 컨트롤러는 위와 같이 구현되므로 RequestMappingHandlerAdapter에 의해 요청이 컨트롤러로 위임된다고 생각하면 된다.

그러면 이제 API에 매핑되는 컨트롤러와 메소드를 직접 호출해보도록 하자.

 

 

 

 

 

 

 

2. Spring API에 매핑되는 컨트롤러와 메소드 가져와서 직접 호출하기


[ API에 매핑되는 컨트롤러와 메소드 가져와서 직접 호출하기  ]

예를 들어 다음과 같은 ProductController가 있다고 할 때, 이 컨트롤러의 메소드를 직접 호출해보도록 하자.

@RestController
@RequiredArgsConstructor
public class ProductController {

    @GetMapping("/product/test")
    public ResponseEntity<MyResponse> temp() throws MangKyuCustomException {
        final MyResponse response = MyResponse.builder()
                .name("MangKyu")
                .desc("MangKyu's Tistory")
                .age(29)
                .build();
        return ResponseEntity.ok(response);
    }
}

 

 

 

1. 컨트롤러 생성 및 RequestMappingHandlerMapping 주입받기

가장 먼저 해야할 작업은 컨트롤러를 만들고, 요청 정보를 관리는 RequestMappingHandlerMapping를 주입받는 것이다.

스프링에서는 생성자 주입을 권장하고 있으므로 생성자 주입을 받으며, URI는 모든 요청에 매핑되는 API를 추가해주도록 하자. 반환 타입은 Object로 해두는데, 이에 대해서는 뒤에서 다시 한번 살펴보도록 하자.

(생성자 주입을 사용해야 하는 이유에 대해 모르면 여기를 참고해주세요)

@RestController
@RequiredArgsConstructor
public class AdminController {

    private final RequestMappingHandlerMapping mapping;

    @RequestMapping("/**")
    public Object forceCall() {

    }
    
}

 

 

 

2. 요청 매핑 정보 조회

이제 주입받은 RequestMappingHandlerMapping로를 통해 요청 매핑 정보를 조회해야 한다. 그런데 문제는 RequestMappingHandlerMapping가 MappingRegistry를 반환하는 Getter 메소드가 없다는 것인데, 대신 (Key, Value)로 (RequestMappingInfo, HandlerMethod)를 반환하는 getHandlerMethods 메소드를 제공하고 있으므로, 이를 이용하도록 하자.

@RestController
@RequiredArgsConstructor
public class AdminController {

    private final RequestMappingHandlerMapping mapping;

    @GetMapping("/**")
    public Object forceCall() {
        final Map<RequestMappingInfo, HandlerMethod> handlerMethods = mapping.getHandlerMethods();

    }

}

 

 

 

3. 요청 정보에 맞는 HandlerMethod 조회

앞서 설명하였듯 디스패처 서블릿은 equals와 hashcode가 오버라이딩된 RequestMappingInfo 객체를 만들어서 Value 값을 꺼내온다고 하였다. RequestMappingInfo 객체 생성은 HttpServletRequest를 파라미터로 받는 getMatchingCondition를 이용하면 된다.

public class RequestMappingInfo implements RequestCondition<RequestMappingInfo> {

    ...

    public RequestMappingInfo getMatchingCondition(HttpServletRequest request) {
        RequestMethodsRequestCondition methods = this.methodsCondition.getMatchingCondition(request);
        if (methods == null) {
            return null;
        }
        ParamsRequestCondition params = this.paramsCondition.getMatchingCondition(request);
        if (params == null) {
            return null;
        }
        HeadersRequestCondition headers = this.headersCondition.getMatchingCondition(request);
        if (headers == null) {
            return null;
        }
        ConsumesRequestCondition consumes = this.consumesCondition.getMatchingCondition(request);
        if (consumes == null) {
            return null;
        }
        ProducesRequestCondition produces = this.producesCondition.getMatchingCondition(request);
        if (produces == null) {
            return null;
        }
        PathPatternsRequestCondition pathPatterns = null;
        if (this.pathPatternsCondition != null) {
            pathPatterns = this.pathPatternsCondition.getMatchingCondition(request);
            if (pathPatterns == null) {
                return null;
            }
        }
        PatternsRequestCondition patterns = null;
        if (this.patternsCondition != null) {
            patterns = this.patternsCondition.getMatchingCondition(request);
            if (patterns == null) {
                return null;
            }
		}
        RequestConditionHolder custom = this.customConditionHolder.getMatchingCondition(request);
        if (custom == null) {
            return null;
        }
        return new RequestMappingInfo(this.name, pathPatterns, patterns,
            methods, params, headers, consumes, produces, custom, this.options);
    }
    
    ...

}

 

 

getMatchingCondition 메소드는 HttpServletRequest로 해당 RequestMappingInfo 객체와 매칭되면 동일한  RequestMappingInfo를 새로 생성하여 반환하고 그렇지 않으면 null을 반환한다. 이를 활용해서 매칭되는 RequestMappingInfo를 찾을 수 있다.

@RestController
@RequiredArgsConstructor
public class AdminController {

    private final RequestMappingHandlerMapping mapping;

    @GetMapping("/**")
    public Object forceCall(final HttpServletRequest request) throws InvocationTargetException, IllegalAccessException {
        final Map<RequestMappingInfo, HandlerMethod> handlerMethods = mapping.getHandlerMethods();

        final RequestMappingInfo result = handlerMethods.keySet().stream()
                .filter(v -> v.getMatchingCondition(request) != null)
                .findAny()
                .orElseThrow(() -> new IllegalArgumentException("Invalid Argument"));
    }

}

 

 

하지만 우리는 현재 ProductController라는 명확한 타겟을 호출하려고 하므로, RequestMappingInfo를 직접 만들어보도록 하자. RequestMappingInfo는 빌더 패턴(Builder Pattern)을 지원하고 있어서 빌더를 이용하면 된다.

@RestController
@RequiredArgsConstructor
public class AdminController {

    private final RequestMappingHandlerMapping mapping;

    @GetMapping("/**")
    public Object forceCall(final HttpServletRequest request) throws InvocationTargetException, IllegalAccessException {
        final Map<RequestMappingInfo, HandlerMethod> handlerMethods = mapping.getHandlerMethods();

        final RequestMappingInfo.BuilderConfiguration builderConfiguration = new RequestMappingInfo.BuilderConfiguration();
        builderConfiguration.setPatternParser(new PathPatternParser());
        final RequestMappingInfo targetRequestMappingInfo = RequestMappingInfo.paths("/product/test")
                .methods(RequestMethod.GET)
                .options(builderConfiguration).build();

    }

}

 

 

위의 코드에서 주의할 점은 BuilderConfiguration을 만들어 PathPatternParser 객체를 생성한 후에 options까지 넣어주어야 한다는 것이다. 앞서 설명하였듯 RequestMappingInfo는 hashCode가 오버라이딩 되어있으므로, options를 넣어주지 않으면 Spring에서 관리하는 RequestMappingInfo와 우리가 생성한 RequestMappingInfo의 hashcode가 달라져 null이 반환된다.

위와 같이 RequestMappingInfo를 생성하였으면 이제 map에서 HandlerMethod를 조회하면 된다.

@RestController
@RequiredArgsConstructor
public class AdminController {

    private final RequestMappingHandlerMapping mapping;

    @GetMapping("/**")
    public Object forceCall(final HttpServletRequest request) throws InvocationTargetException, IllegalAccessException {
        final Map<RequestMappingInfo, HandlerMethod> handlerMethods = mapping.getHandlerMethods();

        final RequestMappingInfo.BuilderConfiguration builderConfiguration = new RequestMappingInfo.BuilderConfiguration();
        builderConfiguration.setPatternParser(new PathPatternParser());
        final RequestMappingInfo targetRequestMappingInfo = RequestMappingInfo.paths("/product/test")
                .methods(RequestMethod.GET)
                .options(builderConfiguration).build();

        final HandlerMethod result = handlerMethods.get(targetRequestMappingInfo);
    }

}

 

 

 

4. 컨트롤러의 메소드 호출

이제 HandlerMethod에 대해 알아봐야 할 차례이다. 앞서 설명하였듯 HandlerMethod에는 매핑되는 컨트롤러의 메소드와 컨트롤러 빈 이름(또는 컨트롤러 빈) 및 빈 팩토리 등이 저장되어 있다. 각각은 다음의 특징을 지닌다.

  • 매핑되는 컨트롤러의 메소드
    • Reflection 패키지의 Method 객체
    • Method를 호출(Invoke)하기 위해서는 메소드의 주인인 빈 객체를 필요로 함
  • 빈 정보(컨트롤러 빈 이름 또는 컨트롤러 빈)
    • Object 타입으로써 컨트롤러 빈 이름 또는 컨트롤러 빈가 될 수 있음
    • 기본적으로 컨트롤러 빈 이름을 갖고 있으며, 빈 객체 조회를 위해 사용됨
    • HandlerMethod의 createWithResolvedBean를 호출하면 빈 이름이 아닌 실제 빈을 갖는 HandlerMethod 객체를 생성함
  • 빈 팩토리
    • 스프링 부트가 관리하는 빈들을 가지고 있음
    • 컨트롤러의 메소드를 호출하기 위한 빈 객체 조회를 위해 사용됨

 

 

HandlerMethod에 저장된 Reflection 패키지의 Method를 호출(Invoke)하기 위해서는 메소드의 주인인 객체를 넘겨주어야 한다. 그런데 HandlerMethod는 기본적으로 컨트롤러 빈 이름을 갖고 있으므로, 빈 팩토리를 통해 컨트롤러 객체를 찾아서 Method를 호출 시에  넘겨주도록 빈 팩토리를 가지고 있는 것이다.

컨트롤러 빈 이름만 있는 HandlerMethod 객체의 createWithResolvedBean를 호출하면 빈 이름 대신 실제 빈을 갖는 HandlerMethod 객체가 반환되며, 이러한 내용을 코드로 작성하여 코드를 완성하면 다음과 같다.

@RestController
@RequiredArgsConstructor
public class AdminController {

    private final RequestMappingHandlerMapping mapping;

    @GetMapping("/**")
    public Object forceCall(final HttpServletRequest request) throws InvocationTargetException, IllegalAccessException {
        final Map<RequestMappingInfo, HandlerMethod> handlerMethods = mapping.getHandlerMethods();

        final RequestMappingInfo.BuilderConfiguration builderConfiguration = new RequestMappingInfo.BuilderConfiguration();
        builderConfiguration.setPatternParser(new PathPatternParser());
        final RequestMappingInfo targetRequestMappingInfo = RequestMappingInfo.paths("/product/test")
                .methods(RequestMethod.GET)
                .options(builderConfiguration).build();

        final HandlerMethod result = handlerMethods.get(targetRequestMappingInfo);
        final HandlerMethod handlerMethod = result.createWithResolvedBean();
        return handlerMethod.getMethod().invoke(handlerMethod.getBean());
    }

}

 

 

Method 객체는 컨트롤러의 메소드이므로 invoke하면 컨트롤러 메소드가 직접 호출된다. 그런데 메소드 객체의 invoke 반환값은 예측할 수 없으므로 당연히 Method 객체의 invoke 메소드 반환값은 Object이다. 이러한 이유로 컨트롤러의 반환값을 Object로 한 것이다.

실제로 위와 같이 작성된 스프링 애플리케이션을 실행하고 테스트 해보면 원하는 대로 결과가 반환됨의 확인할 수 있다.

 

 

 

 

 

이번에는 디스패처 서블릿의 동작 과정을 참고하여 직접 컨트롤러의 메소드를 호출하는 방법에 대해 알아보았습니다.

디스패처 서블릿을 공부하면서 개인적으로 정리한 내용이라 더 좋은 방법이 있을 수 있습니다. 혹시 더 좋은 방법 아시면 공유 부탁드립니다.

추가로 위의 내용들은 개인적으로 공부를 하면서 작성을 한 내용이라 충분히 틀리거나 잘못된 내용들이 있을 수 있습니다. 혹시 수정 또는 추가할 내용들을 발견하셨다면 댓글 남겨주세요! 반영해서 수정하도록 하겠습니다.

감사합니다:)

 

 

 

 

반응형
댓글
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG more
«   2024/03   »
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 26 27 28 29 30
31
글 보관함