컨트롤러는 어떻게 값을 바인딩할까?

주노·2023년 4월 23일
7
post-thumbnail

서론

자바의 Annotaion은 단순히 주석의 역할만 한다.
우리는 Spring Framework를 사용하면서 이 주석들을 종류별로 달아주고 그에 따른 역할이 수행됨을 알 수 있었다.

@PostMapping("/plays")
public ResponseEntity<GameResponse> plays(@Valid @RequestBody CarGameRequest request) {
    GameResponse result = racingGameService.play(request);
    return ResponseEntity.ok(result);
}

위 코드를 보자.

@Valid 라는 어노테이션을 붙이기만 했는데 해당 객체에 대한 검증이 이뤄지고 있다.

도대체 내부적으로 어떻게 등록되고 관리되기에 위와같은 과정이 이뤄질 수 있을까?

@Valid 어노테이션은 spring-boot-starter-validation 의존성을 추가하여 사용할 수 있다.

ArgumentResolver

Spring은 기본적으로 HandlerMethodArgumentResolver 인터페이스를 이용하여 메소드 인자에 선언되어있는 어노테이션을 찾고, 특정 동작을 수행할 수 있다.

인터페이스를 한번 살펴보자

public interface HandlerMethodArgumentResolver {

	// 메소드 인자에 선언되어있는 어노테이션을 찾는다.
	boolean supportsParameter(MethodParameter parameter);

	// 특정 로직을 수행할 수 있다.
	@Nullable
	Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
			NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;

}

크게 두가지 메소드가 존재하는 것을 알 수 있다.

백문이 불여일타... 직접 CustomArgumentResolver를 구현해보면서 동작 과정을 알아보자

시나리오

@MyValid 어노테이션을 이용해서 Client으로부터 들어오는 요청에 검증해보자.

@RestController
public class MyController {

    @GetMapping("/validTest")
    public String argumentTest(@MyValid String value) {
        return value;
    }
}

클라이언트의 participants라는 Header에 juno가 들어있다면 예외를 던지는 프로그램을 만들어본다.

@MyValid

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.*;

@Target({METHOD, FIELD, CONSTRUCTOR, PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyValid {
}

위와 같이 아무 기능도 없는 MyValid라는 어노테이션을 만들었다.

상단에 붙어있는 @Target, @Retention에 대해서 짧게 설명하고 넘어가자

@Retention : 어노테이션의 지속 시간을 정한다. 위 설정의 경우 Runtime시 까지 해당 어노테이션의 주기가 지속된다.
@Target : 어노테이션의 선언 위치를 지정할 수 있다. 위 설정의 경우 메소드, 필드, 생성자, 파라미터에 붙일 수 있다.

CustomArgumentResolver

이제 HandlerMethodArgumentResolver를 상속받는 CustomArgumentResolver를 만들어보자

@Component
public class CustomArgumentResolver implements HandlerMethodArgumentResolver {
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(MyValid.class);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, 
    							  ModelAndViewContainer mavContainer, 
    							  NativeWebRequest webRequest, 
                                  WebDataBinderFactory binderFactory) throws Exception {       
        // TODO : validate
        return null;
    }
}

우선 Spring의 Bean에 등록되어 Spring이 관리하는 객체로 만들기 위해 @Component어노테이션을 붙여줬다.

이후 supportsParameter() 메소드를 구현했다.
이를 통해 MethodParameterhasParameterAnnotation() 메소드를 이용해 해당 파라미터가 @MyValid 어노테이션을 가지고 있는지 확인한다.

아래 resolveArgument() 메소드를 구현하기 전에 ResolveArgument의 각 인자가 어떤 역할을 하는지 알아보자.

  • MethodParameter
    • 요청 핸들러 메서드가 반환하는 값을 저장하는 컨테이너
    • 파라미터 타입, 어노테이션 등의 정보를 가지고 있다.
  • ModelAndViewContainer
    • 요청 핸들러 메서드가 반환하는 값을 저장하는 컨테이너
  • NativeWebRequest
    • 현재 HTTP 요청에 대한 정보를 제공하는 인터페이스
    • 객체를 사용하여 HTTP 요청의 Header, QueryParameter 등의 정보를 얻을 수 있다.
  • WebDataBinderFactory
    • 요청 핸들러 메서드의 Parameter를 Binding할 데이터 바인더를 생성하는 팩토리이다.

따라서 구현한 CustomArgumentResolver는 다음과 같다.

 @Component
public class CustomArgumentResolver implements HandlerMethodArgumentResolver {
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(MyValid.class);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        HttpServletRequest nativeRequest = webRequest.getNativeRequest(HttpServletRequest.class);
        System.out.println(nativeRequest);
        String request = nativeRequest.getHeader("participants");

        if (request != null) {
            if ("juno".equals(request)) {
                throw new IllegalArgumentException("juno는 초대받지 못했습니다...");
            }
            return request;
        }

        throw new IllegalArgumentException("participants 헤더가 존재하지 않습니다.");
    }
}

WebMvcConfigurer

@Configuration
public class CustomWebConfig implements WebMvcConfigurer {

    private final CustomArgumentResolver customArgumentResolver;

    public CustomWebConfig(CustomArgumentResolver customArgumentResolver) {
        this.customArgumentResolver = customArgumentResolver;
    }

    @Override
    public void addArgumentResolvers(final List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(customArgumentResolver);
    }
}

WebMvcConfigurer를 상속받는 CustomWebConfig를 만들었다.

해당 클래스는 Spring이 해당 ArgumentResolver를 사용하도록 등록하는 과정을 포함한다.

실행해보기

포비는 잘 동작한다.

정말 슬프게도 juno는 초대받지 못해 예외가 발생한다.

participants 헤더가 존재하지 않을 때도 예외가 발생한다.

결론

간단하게 ArgumentResolver를 직접 구현하고 HandlerMethodArgumentResolver에 등록하여 실행해봤다.

많이 사용되는 JWT를 예시로 들고 싶었으나 아직 JWT에 대한 개념이 익숙하지 않은 사람에게도 설명할 수 있게끔 글을 구성해봤다.

그런데 혹시 @RequestBody가 붙으면 내가 만든 ArgumentResolver가 안먹힌다는 거 알고 있는가?

➡️ 1depth 더 들어가서 RequestBodyAdvice에 대해 알아보기!

Reference

스프링에서 Argument Resolver 사용하기

Spring Annotation의 원리와 Custom Annotation 만들어보기

[Spring MVC - ArgumentResolver] ArgumentResolver를 이용해서 컨트롤러 메서드의 파라미터 받기

profile
안녕하세요 😆

1개의 댓글

comment-user-thumbnail
2023년 4월 23일

👍

답글 달기