Java&Web

[Spring] Argument Resolver를 이용하여 Client IP 바인딩 하기

프로그래민 2021. 7. 15. 23:25
반응형

이번 포스팅에선 SpringBoot 환경에서 Argument Resolver를 이용하여 요청 파라미터를 바인딩하는 실습을 해보았다.

 

Argument Resolver란?

Argument Resolver(아규먼트 리졸버)는 Spring 환경에서 Controller로 들어온 파라미터를 가공하고나, 수정, 바인딩 기능을 제공할때 사용하는 객체이다. HandlerMethodArgumentResolver 인터페이스를 상속하여 Class를 만들어 사용한다. Body(@Request Body)에 담겨 들어오거나 @PathVariable을 이용하는 데이터들은 Controller에서 바로 파라미터로 받을 수 있지만 세션, 쿠키, 헤더 등에서 제공받는 데이터들을 파라미터로 받는 경우 Argument Resolver를 활용하여 바인딩할 수 있다. 또한, Argument Resolver를 사용함으로써 반복코드를 확실히 줄여줄 수 있는 효과를 얻을 수 있다. 

 

실습

SpringBoot 환경에서 Argument Resolver와 Custom Annotation을 이용하여 Client IP를 Controller에서 파라미터로 받도록 하는 실습을 진행해보았다. 

ClientIp Annotation 생성

1
2
3
4
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface ClientIp {
}
                                                              

Controller에서 Argument Resolver와 같이 활용할 수 있는 @ClientIp 어노테이션을 생성하였다. Client IP의 바인딩이 필요한 경우 이 어노테이션을 요청 파라미터 앞에 붙이게 되고, Argment Resolver는 이 어노테이션의 유무의 따라 동작을 하게 된다.

UserInfo 클래스 생성

1
2
3
4
5
6
7
8
@Getter
@Setter
@Builder
public class UserInfo {
    private String name;
    private int age;
    private String ip;
}
                                                                        

요청으로 받을 파라미터인 name, age와 Argument Resolver를 통해 바인딩할 UserInfo 클래스를 생성하였다.

ClientIpArgumentResolver 클래스 생성

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
32
33
34
35
@Component
public class ClientIpArgumentResolver implements HandlerMethodArgumentResolver {
    @Override
    public boolean supportsParameter(MethodParameter methodParameter) {
        if (methodParameter.hasParameterAnnotation(ClientIp.class)) {
            return true;
        }
        return false;
    }
 
    @Override
    public UserInfo resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer,
        NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {
        HttpServletRequest request = (HttpServletRequest)nativeWebRequest.getNativeRequest();
 
        return UserInfo.builder()
            .name(request.getParameter("name"))
            .age(Integer.parseInt(request.getParameter("age")))
            .ip(getClientIp(request))
            .build();
    }
 
    private String getClientIp(HttpServletRequest request) {
        return Stream.of(
            request.getHeader("X-Forwarded-For"),
            request.getHeader("Proxy-Client-IP"),
            request.getHeader("WL-Proxy-Client-IP"),
            request.getHeader("HTTP_CLIENT_IP"),
            request.getHeader("HTTP_X_FORWARDED_FOR")
        )
            .filter(Objects::nonNull)
            .findFirst()
            .orElse(request.getRemoteAddr());
    }
}
        

가장 핵심이 되는 ClientIpArgumentResolver 클래스를 생성하였다. HandlerMethodArgumentResolver를 구현하게 되고, supportsParameter 메소드를 통해 동작여부를 결정하게 된다. 위에선 ClientIp 어노테이션의 포함여부를 통해 동작여부를 결정하게 구현하였다. 만일 Controller에 메소드 중 파라미터에 @ClientIp를 가지게 된다면 이 ClientIpArgumentResolver가 동작이 된다.
동작하는 경우 우선 resolveArgument 메소드를 거치게 된다. 메소드에 있는 nativeWebRequest 파라미터를 이용하여 HttpServlertReuqest 형의 request를 얻게 되고, 이것은 실제로 호출된 Controller의 메소드로 향하던 request이다. 여기서 이제 파라미터 바인딩을 해주게 되는데, resolve Argument 메소드는 원래 리턴형이 Object 이기에 원하는 타입으로 설계를 할 수가 있다. 여기선 미리 정의한 UserInfo 클래스를 반환하도록 설계하였다.
UserInfo를 생성하기 위해 queryString으로 받은 name과 age를 request에서 뽑아주고, Client IP 또한 request를 활용해주었다. Client IP는  Web Server에서 프록시나 로드 밸런서를 통해 WAS에 요청하기 때문에 프록시나 로드 밸런서의 IP 주소만을 담고 있어서 원래의 IP를 못가져오는 현상이 있다. 따라서 헤더에서 벤더마다 다른 key값으로 가져와야 하는데 getClientIp 메소드에 사용된 key값들을 사용할 수 있다(링크).

ClientIpArgumenrResolver를 WebMvcConfig 클래스에 등록

1
2
3
4
5
6
7
8
9
@RequiredArgsConstructor
public class WebMvcConfig implements WebMvcConfigurer {
    private final ClientIpArgumentResolver clientIpArgumentResolver;
 
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(clientIpArgumentResolver);
    }
}
           

Config 설정 클래스인 WebMvcConfig 클래스에 Argument Resolver로 CleintIpArgumentResolver를 등록하였다.

ClientIpArgumentResolver 테스트용 API 생성

1
2
3
4
5
6
7
@RestController
public class TestApiController {
    @GetMapping("/api/ip")
    public UserInfo getIp(@ClientIp UserInfo userInfo) {
        return userInfo;
    }
}
                                       

RestController인 TestApiController를 생성후 /api/ip를 생성해주었다. 파라미터로 UserInfo를 받으면서 @ClientIp 어노테이션을 붙여주어 ClientIpArgumentResolver가 동작을 할 수 있도록 해주었다.

 

결과

/api/ip 라는 API를 호출했을때 위와 같은 결과를 얻을 수 있었다. ClientIpArgumentResolver를 거쳐서 UserInfo의 바인딩된 name, age, ip를 얻었다. name 과 age는 query String에 들어온 값이 기대대로 나왔고, ip의 경우 local환경이기에 127.0.0.1을 확인 할 수 있었다. 

IP의 경우 IPv4의 형태는 jvm 옵션에 -Djava.net.preferIPv4Stack=true 사용

 

출처
https://tlatmsrud.tistory.com/48
https://velog.io/@tigger/Argument-Resolver
https://linked2ev.github.io/java/2019/05/22/JAVA-1.-java-get-clientIP/
반응형