티스토리 뷰

Spring

[Spring] @RequestBody에 ArgumentResolver(아규먼트 리졸버)가 동작하지 않는 이유, RequestBodyAdvice로 @RequestBody에 부가 기능 구현하기

망나니개발자 2022. 5. 12. 10:00
반응형

이번에는 @RequestBody에 ArgumentResolver(아규먼트 리졸버)가 동작하지 않는 이유를 알아보고 @RequestBody의 동작을 커스터마이징하여 부가 기능을 적용하는 방법에 대해 알아보도록 하겠습니다.

 

 

 

 

1. @RequestBody에 ArgumentResolver(아규먼트 리졸버)가 동작하지 않는 이유


[ ArgumentResolver(아규먼트 리졸버)란? ]

스프링의 디스패처 서블릿은 컨트롤러로 요청을 전달한다. 그때 컨트롤러에서 필요로 하는 객체를 만들고 값을 바인딩하여 전달하기 위해 사용되는 것이 ArgumentResolver이다. 스프링이 제공하는 다음과 같은 어노테이션들은 모두 ArgumentResolver로 동작한다.

  • @RequestParam: 쿼리 파라미터 값 바인딩
  • @ModelAttribute: 쿼리 파라미터 및 폼 데이터 바인딩
  • @CookieValue: 쿠키값 바인딩
  • @RequestHeader: 헤더값 바인딩
  • @RequestBody: 바디값 바인딩

 

ArgumentResolver가 동작하면 컨트롤러로 전달할 객체가 만들어지고, 컨트롤러에게 전달된다.

 

 

 

[ @RequestBody에 ArgumentResolver가 동작하지 않는 이유 ]

@RequestBody는 ArgumentResolver의 구현체인 RequestResponseBodyMethodProcessor에 의해 처리된다. json 형태의 메세지는 객체로 변환되어 컨트롤러로 전달되는데, 그렇다면 @RequestBody가 있는 파라미터에 새로운 ArgumentResolver를 추가하면 어떻게될까? 즉, 1개의 파라미터에 객체 생성을 위한 2가지 ArgumentResolver 처리 어노테이션이 붙어 있는 것이다.

@RestController
public class UserController {

    @PostMapping("/users")
    public ResponseEntity<User> users(@RequestBody @DefaultUser User user) {
        return ResponseEntity.ok(user);
    }

}

 

 

위와 같은 상황에서는 @RequestBody만 동작하고 우리가 추가한 ArgumentResolver는 동작하지 않는다. 왜냐하면 ArgumentResolver는 객체를 만드는 동작을 하는데, @RequestBody가 우선순위를 갖고 먼저 동작하여 컨트롤러로 전달할 객체가 만들어졌기 때문이다. 그래서 두 번째 ArgumentResolver는 동작하지 않고 무시된다.

그렇다면 @RequestBody의 동작 이후 부가 작업을 해주고 싶은 경우에는 어떻게 처리하는게 좋을까? 예를 들어 위의 User 객체에 @RequestBody가 동작하고 나서 User에 특별한 부가 작업을 해주고 싶은 경우이다.

 

 

 

 

 

2. RequestBodyAdvice로 @RequestBody에 부가 기능 구현하기


[ RequestBodyAdvice ]

RequestBodyAdvice란?

RequestBodyAdvice는 요청 body를 커스터마이징할 수 있는 기능을 제공하는 인터페이스이다. RequestBodyAdvice를 이용하면 Http 메세지를 객체로 변환하기 전/후 또는 body가 비어있을 때 등을 처리할 수 있다. 이 인터페이스의 구현체를 빈으로 등록하기 위해서는 RequestMappingHandlerAdapter에 직접 등록해주거나, @ControllerAdvice 어노테이션을 붙여주면 된다.

 

 

 

RequestBodyAdvice의 메소드

RequestBodyAdvice는 다음과 같은 메소드들을 제공한다. 해당 메소드들을 구현하면 RequestBody의 동작을 커스터마이징할 수 있다.

  • supports: 해당 RequestBodyAdvice를 적용할지 여부를 결정함
  • beforeBodyRead: body를 읽어 객체로 변환되기 전에 호출됨
  • afterBodyRead: body를 읽어 객체로 변환된 후에 호출됨
  • handleEmptyBody: body가 비어있을때 호출됨

 

 

 

[ RequestBodyAdvice 사용해보기  ]

문제 상황 가정하기

예를 들어 클라이언트가 보내는 json 요청을 위한 User 클래스가 있다고 하자. 우리가 하고 싶은 동작은 ArgumentResolver를 이용하여 @RequestBody로 만들어진 user 객체의 값 중에서 desc만 바꿔주는 것이다.

@Getter
@Setter
public class User {

    private String name;
    private String desc;

}

 

 

위의 동작을 위한 컨트롤러 역시 다음과 같이 구현되어 있다고 가정하자.

@RestController
public class UserController {

    @PostMapping("/users")
    public ResponseEntity<User> users(@RequestBody @Valid User user) {
        return ResponseEntity.ok(user);
    }

}

 

 

위와 같은 문제 상황 해결을 위해서 인터셉터나 AOP 등을 이용할수도 있지만 RequestBodyAdvice가 가장 적합하고 구현이 편리하다. 그러므로 RequestBodyAdvice를 통해 해결해보도록 하자.

 

 

 

RequestBodyAdvice 구현체 만들기

RequestBodyAdvice는 요청이 UserController인 경우와 body의 타입이 User일 경우에만 적용되도록 구현해보도록 하자. 또한 body의 데이터가 읽어진 후에 데이터를 변경해주는 로직 역시 작업해주도록 하자.

@RestControllerAdvice
public class RequestBodyControllerAdvice implements RequestBodyAdvice {

    @Override
    public boolean supports(final MethodParameter methodParameter, final Type targetType, final Class<? extends HttpMessageConverter<?>> converterType) {
        return methodParameter.getContainingClass() == UserController.class && targetType.getTypeName().equals(User.class.getTypeName());
    }

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

    @Override
    public Object afterBodyRead(final Object body, final HttpInputMessage inputMessage, final MethodParameter parameter, final Type targetType, final Class<? extends HttpMessageConverter<?>> converterType) {
        final User user = (User) body;
        user.setDesc("desc");
        return user;
    }

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

}

 

 

위의 코드에서 잊지 말아야 하는 부분은 클래스에 @RestControllerAdvice를 선언해주었다는 것이다. @RestControllerAdvice를 붙여줘야만 스프링이 해당 빈을 자동으로 탐색하여 동작가능하게 한다. 

 

 

 

 

 

테스트 코드 작성해서 동작 확인 및 검증하기

이를 검증하기 위한 테스트 코드는 다음과 같이 작성할 수 잇다.

@SpringBootTest
@AutoConfigureMockMvc
class CustomRequestbodyApplicationTests {

    @Autowired
    private Gson gson;

    @Autowired
    private MockMvc mockMvc;

    @Test
    void 호출성공() throws Exception {
        // given
        final String url = "/users";

        final User user = new User();
        user.setName("test-name");
        user.setDesc("test-desc");

        // when
        final ResultActions result = mockMvc.perform(
                MockMvcRequestBuilders.post(url)
                        .content(new Gson().toJson(user))
                        .contentType(MediaType.APPLICATION_JSON)
        );

        // then
        result.andExpect(status().isOk())
                        .andExpect(jsonPath("name").value("test-name"))
                        .andExpect(jsonPath("desc").value("desc"));
    }

    @Test
    void 호출실패_유효성에러() throws Exception {
        // given
        final String url = "/users";

        final User user = new User();
        user.setName(null);
        user.setDesc(null);

        // when
        final ResultActions result = mockMvc.perform(
                MockMvcRequestBuilders.post(url)
                        .content(new Gson().toJson(user))
                        .contentType(MediaType.APPLICATION_JSON)
        );

        // then
        result.andExpect(status().isBadRequest());
    }
    
}

 

 

 

그리고 테스트를 실행해보면 다음과 같이 성공함을 확인할 수 있다.

 

 

 

 

 

 

위의 내용은 왜 @RequestBody에 ArgumentResolver가 동작하지 않는지 친구의 삽질로부터 같이 보게 되었습니다. 그리고 이후에 @RequestBody의 동작 후에 어떻게 부가기능을 구현하는 방법을 고민하다가 같이 스터디하는 분으로부터 좋은 아이디어를 얻을 수 있었습니다ㅎㅎ 조금 복잡한 시스템을 구현하다보면 충분히 사용될 수 있을 것 같은데, 다른 누군가에게도 도움이 되기를 바라겠습니다!

실제 코드는 깃허브 참고해주세요! 감사합니다:)

 

 

 

 

 

 

반응형
댓글
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG more
«   2024/04   »
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
글 보관함