☝🏻 목표
나는 사용자 DTO에 사용자의 IP를 조회해서 값을 넣고싶었다.
그래서 커스텀 애노테이션을 만들어서 사용하고자 했다. 그럴려면 ArgumentResolver도 정의해줘야 하는데,
CustomArgumentResolver를 구현해보면서 동작 과정을 알아보자.
ArgumentResolver란?
간단히 말하면, Spring MVC에서 컨트롤러 메서드의 파라미터를 다루기 위해 사용하는 인터페이스다.
ArgumentResolver를 만들기 위해서는 클래스가 HandlerMethodArgumentResolver 인터페이스를 구현해야 한다.
HandlerMethodArgumentResolver를 이용하여 컨트롤러(핸들러)가 필요로 하는 다양한 파라미터의 값(객체)을 생성하고, 파라미터의 값이 모두 준비되면 컨트롤러를 호출하면서 값을 넘겨준다.
그럼 ArgumentResolver는 어떻게 호출이 될까?
핸들러 어댑터가 ArgumentResolver를 호출해서 파라미터 값을 만들어 컨트롤러로 보내주는 것이다.
백문이 불여일타 직접 만들어보자
@ResponseBody
@PostMapping("/save")
public String saveLottoNumbers(@UserIp String crtIp, @RequestBody UserLotto userLotto) {
userLottoService.saveLottoNumbers(userLotto);
return "성공";
}
컨트롤러(핸들러) 파라미터에 정의된 @UserIp가 사용될 수 있도록 해보겠다.
package com.lotto.argumentResolver;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface UserIp {
}
@UserIp 정의해주기
package com.lotto.argumentResolver;
import org.springframework.core.MethodParameter;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
import javax.servlet.http.HttpServletRequest;
@Component
public class UserIpResolver implements HandlerMethodArgumentResolver {
private static final String[] IP_HEADER_CANDIDATES = {
"X-Forwarded-For", "Proxy-Client-IP", "WL-Proxy-Client-IP",
"HTTP_X_FORWARDED_FOR", "HTTP_X_FORWARDED", "HTTP_X_CLUSTER_CLIENT_IP",
"HTTP_CLIENT_IP", "HTTP_FORWARDED_FOR", "HTTP_FORWARDED", "HTTP_VIA",
"REMOTE_ADDR"
};
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(UserIp.class);
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
for (String header : IP_HEADER_CANDIDATES) {
String ip = request.getHeader(header);
if (ip != null && !ip.isEmpty() && !"unknown".equalsIgnoreCase(ip)) {
return ip;
}
}
return request.getRemoteAddr();
}
}
HandlerMethodArgumentResolver 인터페이스 커스텀해서 구현하기
1. Bean으로 등록하기 위해 @Component어노테이션을 붙여야한다.
2. supportsParameter() 메서드를 구현한다. MethodParameter의 hasParameterAnnotation() 메서드를 이용하여 해당 파라미터가 내가 만든 어노테이션을 가지고 있는지 확인한다.
3. resolveArgument() 메서드를 구현한다.
각 파라미터의 역할
- MethodParameter
- 요청 핸들러 메서드가 반환하는 값을 저장하는 컨테이너
- 파라미터 타입, 애노테이션 등의 정보를 가지고 있다.
- ModelAndViewContainer
- 요청 핸들러 메서드가 반환하는 값을 저장하는 컨테이너
- NativeWebRequest
- 현재 HTTP 요청에 대한 정보를 제공하는 인터페이스
- 객체를 사용하여 HTTP 요청의 Header, QueryParameter 등의 정보를 얻을 수 있다.
- WebDataBinderFactory
- 요청 핸들러 메서드의 Parameter를 Binding할 데이터 바인더를 생성하는 팩토리
package com.lotto;
import com.lotto.argumentResolver.UserIpResolver;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private UserIpResolver userIpResolver;
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(userIpResolver);
}
}
WebMvcConfigurer을 구현한 WebConfig에 만든 ArgumentResolver 추가해주기
그럼 완성이다 !!
☝🏻 하지만 나는 사용자 IP가 필요한 컨트롤러 마다 일일이 @UserIp를 쓰자니 너무 불편하다는 걸 느꼈다.
@RequestBody User 이런식으로 객체를 JSON 형식으로 받아 바인딩할 때, 어디선가 User 객체 안 userIp 필드에 값을 넣어주도록 해보고 싶었다.
그래서 ArgumentResolver를 수정해보는 도중 문제를 발견했다.
커스텀 애노테이션과 @RequestBody는 함께 사용할 수 있지만, Spring MVC는 각 파라미터에 대해 하나의 리졸버만 적용한다는 것이었다. 동시에 두 리졸버가 하나의 파라미터에 적용되지는 않는다.
한 객체에 @RequestBody와 추가 ArgumentResolver를 함께 처리해야 한다면?
RequestBodyAdvice나 RequestBodyAdviceAdapter를 사용하여, @RequestBody로 변환된 객체에 추가 데이터를 설정하기 !!
나는 최종적으로 이 방법을 선택했다. (위에 눌러서 확인하기 !)
참조
'Spring' 카테고리의 다른 글
[Spring] 스케줄링 사용하기 (0) | 2024.11.05 |
---|---|
[Spring] 스코프 (0) | 2024.11.04 |
[Spring] @RequestBody와 ArgumentResolver의 충돌 해결 (0) | 2024.11.03 |
[Spring] MySQL 타임존과 9시간 차이날 때 (느릴 때) (0) | 2024.10.30 |
[스프링] JSON 변환 시 날짜 타입 포맷팅하는 법 - @JsonFormat (8) | 2024.10.29 |