1 minute read

Argument Resolver란?

Body에 담아 들어오거나 @PathVariable를 이용하는 데이터는 Controller에서 바로 받을 수 있지만 세션, 쿠키, 헤더 등에서 제공하는 데이터들은 파라미터로 받는 경우 Argument Resolver를 활용해서 바인딩할 수 있다.

개인적으로 세션, 쿠키, 헤더 말고 활용하면 안되는것인지, 코드 가독성이 떨어지는것인지 의문점이 생긴다. 🤔 (참 좋은 방법인거 같은데…)

ClientIp Annotation 생성

Custom Annotation을 생성하였다. 이게 왜 필요하냐면 파라미터 앞에 붙여줘서 Resolver에서 동작여부를 판단하게 된다.

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface ClientIp {
}

UserInfo 클래스 생성

Http Response Object라고 생각하면 된다.

@Getter
@Setter
@Builder
@@AllArgsConstructor
public class UserInfo {
    private String name;
    private int age;
    private String ip;
}

ClientIpArgumentResolver 클래스 생성

이 코드는 복잡해보이나 대강 보면

supportsParameter : @ClientIp 어노테이션이 포함되었는지 동작여부를 결정한다.

resolveArgument : Controller로 향하는 request를 전달받아서 파라미터에 바인딩하게 된다. 원하는 형태의 Object를 설계해서 리턴할 수 있다. 미리 정의한 UserInfo를 반환한다.

getClientIp : 당연한 거지만 Client 에서 request한것은 로드밸런싱되서 WAS에 전달되기 때문에 ClientIp는 내 아이피가 아니라 로드밸런서의 아이피가 될 것이다. 그래서 헤더에서 벤더마다 다른 Key값을 가져와야 하는데 getClientIp 메서드에서 사용된 key값을 사용한다. 이글

@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());
    }
}

ClientIpArgumenrResolver를 WebMvcConfig 클래스에 등록

Argument Resolver로 ClientIpArgumentResolver를 등록한다.

@RequiredArgsConstructor
public class WebMvcConfig implements WebMvcConfigurer {
    private final ClientIpArgumentResolver clientIpArgumentResolver;
 
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(clientIpArgumentResolver);
    }
}

ClientIpArgumentResolver 테스트용 API 생성

마지막 API를 테스트해본다.

@RestController
public class TestApiController {
    @GetMapping("/api/ip")
    public UserInfo getIp(@ClientIp UserInfo userInfo) {
        return userInfo;
    }
}

Result

IP의 경우 IPv4의 형태는 jvm 옵션에 -Djava.net.preferIPv4Stack=true 사용해야 한다. (현재는 null)

참고

Categories:

Updated: