본문 바로가기
Spring

[Spring] @RequestBody와 ArgumentResolver의 충돌 해결

by 호강하는 지해 2024. 11. 3.
728x90

 

 

📍 목표

전 시간에 만들었던 @UserIp 애노테이션을 컨트롤러(핸들러)의 파라미터에서 일일이 쓰지 않고,
@RequestBody로 User 이런식으로 객체를 JSON 형식으로 받아 바인딩할 때,  어디선가 User(UserLotto) 객체 안 ip(userIp) 필드에 사용자 IP 값을 알아서 넣어주도록 해보고 싶었다.

 

 

 

문제점

내가 생각한 방안은 HandlerMethodArgumentResolver을 구현한 커스텀 ArgumentResolver 클래스에서, 파라미터에 User 객체가 들어오면 HttpServletRequest에서 사용자의 IP를 받아 값을 바인딩해주는 방식이었다.

 

그러나 커스텀 애너테이션과 @RequestBody는 함께 사용할 수 있지만, Spring MVC는 각 파라미터에 대해 하나의 리졸버만 적용하기 때문에 동시에 두 리졸버가 하나의 파라미터에 적용되지는 않는다.
ArgumentResolver도 적용 우선 순위가 있는데, 먼저 하나가 적용되면 나머지 리졸버는 적용되지 않는 것이다. (우선 순위는 설정 가능)

결국 파라미터에서 @UserIp를 생략하고 싶으면 HandlerMethodArgumentResolver를 구현하면 안 됐다.

 

 

❓ 그럼 한 객체에 @RequestBody와 추가 ArgumentResolver를 함께 처리해야 한다면 어떻게 해? 

RequestBodyAdvice RequestBodyAdviceAdapter를 사용하여, @RequestBody로 변환된 객체에 추가 데이터를 설정해야한다.

나는 원하는 메서드만 오버라이딩해서 사용할 수 있는 RequestBodyAdviceAdapter를 상속 받아서 구현했다 !

 

 

 

RequestBodyAdvice

public abstract class RequestBodyAdviceAdapter implements RequestBodyAdvice {
    public RequestBodyAdviceAdapter() {
    }

    public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
        return inputMessage;
    }

    public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        return body;
    }

    @Nullable
    public Object handleEmptyBody(@Nullable Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        return body;
    }
}

RequestBodyAdviceAdapter 클래스의 정의다.
다 정의되어 있으니 나에게 필요한 메서드만 오버라이딩해서 쓸 수 있다는 점에서 더 편리해서 나는 이걸 선택해서 사용했다.

afterBodyRead() 메서드는 @RequestBody로 바인딩된 객체가 생성된 후에 호출된다. 여기에서 IP 주소를 가져와 UserLotto 객체의 crtIp 필드에 설정한다.

밑에서 실제로 구현해보자 !

 

package com.lotto.argumentResolver;

import com.lotto.userLotto.dto.UserLotto;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdviceAdapter;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Type;


// WebConfig 파일에 따로 등록하지 않아도 됨
@ControllerAdvice
public class UserIpResolver extends RequestBodyAdviceAdapter {

    private final HttpServletRequest request;

    public UserIpResolver(HttpServletRequest request) {
        this.request = request;
    }

    @Override
    public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        return UserLotto.class.isAssignableFrom(methodParameter.getParameterType());
    }

    // @RequestBody로 바인딩 된 후 타는 메서드
    @Override
    public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {

        if (body instanceof UserLotto) {
            UserLotto userLotto = (UserLotto) body;
            String ip = request.getRemoteAddr();
            userLotto.setCrtIp(ip);  // IP 주소 설정
        }
        return body;
    }
}
  • @ControllerAdvice를 쓰면 WebMvcConfigurer에 등록하지 않아도 된다.
    • RequestBodyAdvice는 @ControllerAdvice와 함께 사용되어 전역적으로 등록된다. @ControllerAdvice이 붙으면 Spring이 해당 클래스를 스캔하고 모든 @RequestBody가 있는 컨트롤러 요청에 대해 RequestBodyAdvice를 자동 적용한다.
  • HttpServletRequest을 의존성 주입 받으면 현재 요청의 정보를 담은 HttpServletRequest 인스턴스가 주입된다.
    Spring은 기본적으로 요청 스코프를 지원하므로 각 요청마다 독립된 HttpServletRequest가 생성된다.
  • supports() 메서드는 RequestBodyAdvice가 특정 객체 타입에 대해 동작할지 여부를 결정한다. 여기서는 UserLotto 타입에 대해서만 동작하도록 설정했다.
  • methodParameter.getParameterType()를 통해 해당 메서드 파라미터의 구체적인 타입을 확인할 수 있다.

  • afterBodyRead()@RequestBody로 바인딩 된 후 타는 메서드다. 그러니 여기서 바인딩 후 사용자의 IP를 UserLotto 객체의 crtIp 필드에 저장하면 정상적으로 저장할 수 있다.

 

 

 

이렇게 해주면

  1. 애노테이션을 만들지 않아도 될 것
  2. 컨트롤러에 일일히 애노테이션 쓰지않고 사용할 것
  3. WebMvcConfiguerer에 등록하지 않아도 될 것
  4. 컨트롤러 파라미터에 @RequestBody UserLotto userLotto 만 써줘도 사용자의 ip가 들어올 것

다 만족하게 된다 !!

 


참조
https://mangkyu.tistory.com/250

 

https://velog.io/@junho5336/RequsetBody%EC%97%90-ArgumentResolver%EA%B0%80-%EC%95%88%EB%A8%B9%ED%9E%8C%EB%8B%A4

728x90