개발 기록

> [Spring] Request Value 조작 - 1. HandlerMethodArgumentResolver 를 직접 구현해보자! 본문

Spring

> [Spring] Request Value 조작 - 1. HandlerMethodArgumentResolver 를 직접 구현해보자!

1z 2023. 12. 7. 17:27

 

 

 

 

환경

java 17
spring version 6.0.11 
spring boot version 3.1.3

 

 

1. 개념  

HandlerMethodArgumentResolver 인터페이스의 역할은 Controller로 들어온 파라미터를 가공하거나 수정, 바인딩 하는 역할이다.

 

Interceptor vs Filter vs Resolver Argument vs AOP "

ArgumentResolver 에서 처리하는 중복 코드는 interceptor, AOP, Filter 로도 구현이 가능하지만, 각 호출 되는 시점과 return type 이 다르기 때문에 해당로직을 어디 구간에서 실행할지 고민해봐야 한다. 

 

1. Fitler : filter 는 DispatcherServlet 요청이 있기 전 호출 (ex. token 검사)

2. Interceptor : 클라이언트가 요청한 API 를 찾은 뒤 호출 (ex. role 검사)

3. Resolver Argument : interceptor 요청 뒤 호출

4. AOP : pointcut 으로 호출 지정 (어노테이션, 메서드, 경로)

 

 사용 예시

ex. 클라이언트로부터 암호화된 데이터를 받을 때 복호화 하는 작업을 AgumentResolver 를 이용하여 처리하고 실제 사용할 수 있는 값은 Controller 로 넘겨 줄 수 있다.

 

 

2. 실습  

목표
1. Ip, 브라우저 정보, 회원 정보를 UserDTO 담아 Controller 로 넘겨주기 
2. 인증로직 => heaer 에서 토큰을 가져와 회원 ID 만 Controller 로 넘겨주기 

 

(1) WebConfig

 

: addArgument 함수를 재정의하여  arguementResolver 리스트에  사용자 정의 ArgumentResovler 을 추가한다.

@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {

    private final AuthCustomArgumentResolver authCustomArgumentResolver;

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

 

(2) Annotataion  

: 필수적인 것은 아니지만, 기본적으로 같이 많이 사용한다.  특정한 부분에서만 자동으로 객체가 Argument Resolver를 통해 바인딩되도록 만들고 싶은 경우 커스텀 어노테이션을 사용하여 해결한다.

// 파라미터에 해당 애노테이션 사용
@Target(ElementType.PARAMETER) 
 // 라이프 사이프 설정 : 런타임까지 애노테이션 메타 정보 보유(리플렉션 등을 활용할 수 있다.)
@Retention(RetentionPolicy.RUNTIME)
public @interface AuthUser {
}

 

(3) HandlerMethodArgumentResolver 구현체 작성

1. supportsParameter() : Argument Resolver 적용 대상을 제한하고 resolveArgument() 실행여부를 판단한다.

ⓛ getParameterType() :

: Controller method parameter type 이 UserDTO 타입인지 확인한다.

hasParameterAnnotaion()

:  파라미터의 어노테이션으로 Argument Resolver 적용 대상을 제한한다.  

2. resolveArgument() : 반복되는 로직, 바인딩 시 필요한 로직등을 넣어준다. 그리고 최종적으로 UserDto 를 생성해서 반환한다.

  supportsParameter() return 값이 true 일 때 실행된다. 

/** HandlerMethodArgumentResolver
 *  컨트롤러 메서드에서 특정 조건에 맞는 파라미터가 있을 때 요청에 들어온 값을 이용해 원하는 값을 바인딩 한다.
 * **/

@Slf4j
public class AuthCustomArgumentResolver implements HandlerMethodArgumentResolver {

    /** 호출되는 Controller의 파라미터 값을 검사하는 콜백 함수
     * supportsParameter: 들어온 파라미터에 대해 resolveArgument 메서드 실행여부 판단
     * ex. 리턴 값이 true => resolveArgument 메서드 실행
     */
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return
            // getParameterType() : 컨트롤러 메소드의 파라미터가 UserDTO 타입인지 검사
            parameter.getParameterType().equals(UserDTO.class) ||
                // hasParameterAnnotation() : 컨트롤러 메소드의 파라미터가  @AuthUser 어노테이션을 가지고 있는지 확인한다.
                parameter.hasParameterAnnotation(AuthUser.class);

    }

    /* 파라미터를 가공하는 역할: 반환값이 대상이 되는 메소드의 파라미터에 바인딩 */
    @Override
    public Object resolveArgument(
        MethodParameter parameter,
        ModelAndViewContainer mavContainer,
        NativeWebRequest webRequest,
        WebDataBinderFactory binderFactory) throws Exception {

        HttpServletRequest request = (HttpServletRequest)webRequest.getNativeRequest();

        // RequestParameter value(=name) 로 사용자 PK 를 조회하여 바인딩한다.
        String name = (String) webRequest.getAttribute("name", WebRequest.SCOPE_REQUEST);
        // TODO select * from tbl_user where name = :name

       // 예제 2
        if(parameter.hasParameterAnnotation(AuthUser.class)){
        	String authorization = request.getHeader("Authorization");
            // .. 인증 로직 진행
            return 1; // 회원 ID return
        }
        
        // 예제 1
        return new RequestInfo(1L, getClientIP(request), request.getHeader("User-Agent"));
    }

    public static String getClientIP(HttpServletRequest request) {
        String ip = request.getHeader("X-Forwarded-For");

        if (ip == null) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null) {
            ip = request.getHeader("HTTP_CLIENT_IP");
        }
        if (ip == null) {
            ip = request.getHeader("HTTP_X_FORWARDED_FOR");
        }
        if (ip == null) {
            ip = request.getRemoteAddr();
        }

        return ip;
    }
}

 

 

(3) Controller - 파라미터 적용

▶예제 1. 어노테이션

파리미터에  @AuthUser 존재를 인식하고 인증처리를 위한 ArgumentResolver가 실행된다.

 

Spring Security vs ArgumentResolver

Spring Security 도 ArgumentResolver 메카니즘 등을 이용한 인증관련 통합 라이브러리라서 Spring security 를 사용하여 인증로직을 처리해도 되고, 간단한 인증은 Spring Filter(or Interceptor), ArgumentResolver같은 객체를 이용해서 직접 구현해도 된다.  당연히 둘다 사용해도 된다.

 

 예제 2. Class Type

파리미터 타입이 UserDTO 인 것을 확인하고 요청정보(ip 등) 을 추가해주기 위한  ArgumentResolver가 실행된다.

 public class ArgumentTestController {

    // 매핑 일치 기준 : 어노테이션
    @GetMapping("/all")
    public void findItems(@AuthUser int requestId) {
        System.out.println(requestId);
    }

    // 매핑 일치 기준 : Class Ttype
    @GetMapping("/all")
    public void findItems(UserDTO request) {
        System.out.println(request.toString());
    }

}