Bepoz
파즈의 공부 일기
Bepoz
전체 방문자
오늘
어제
  • 분류 전체보기 (232)
    • 공부 기록들 (85)
      • 우테코 (17)
    • Spring (85)
    • Java (43)
    • Infra (17)
    • 책 정리 (0)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
Bepoz

파즈의 공부 일기

Spring

[Spring] Validator 생성 시 주의해야 할 점, Invalid target 오류

2020. 12. 2. 02:25

Validator 생성 시 주의해야 할 점, Invalid target 오류

Custom 한 Validator 를 많이들 생성할 것이다. 이번에 나는 프로젝트를 수행 도중에 크게 막히는 부분이 있었다.

Invalid target for Validator [com.ticket.captain.festival.validator.FestivalCreateValidator@5d035ab6]: com.ticket.captain.festival.dto.FestivalUpdateDto@3407ded1

바로 다음과 같은 오류였다. 이 오류가 발생할 당시에 코드는 다음과 같았다.

// validate 메서드는 생략
@Component
@RequiredArgsConstructor
public class FestivalCreateValidator implements Validator {

    private final FestivalService festivalService;

    @Override
    public boolean supports(Class<?> clazz) {
        return clazz.isAssignableFrom(FestivalCreateDto.class);
    }
    @PutMapping("update/{festivalId}")
    public ApiResponseDto update(@PathVariable Long festivalId,
                                 @RequestBody FestivalUpdateDto festivalUpdateDto,
                                 Errors errors) {
        if (errors.hasErrors()) {
            String field = errors.getFieldError().getDefaultMessage();
            ExceptionDto exceptionDto = ExceptionDto.builder().message(field).build();
            return ApiResponseDto.VALIDATION_ERROR(exceptionDto);
        }
        return ApiResponseDto.createOK(festivalService.update(festivalId, festivalUpdateDto));
    }
    @Test
    @WithMockUser(value = "mock-manager", roles = "MANAGER")
    void updateFestival() throws Exception {

        FestivalUpdateDto updateDto = FestivalUpdateDto.builder()
                .title("Charity Concert")
                .content("Enjoy And Donate")
                .salesEndDate(LocalDateTime.now())
                .festivalCategory(FestivalCategory.CHARITY.toString())
                .build();

        mockMvc.perform(put(API_MANAGER_URL + "/update/" + festival.getId())
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(updateDto))
                .with(csrf()))
                .andExpect(status().isOk())
                .andExpect(jsonPath("data.title").value("Charity Concert"))
                .andExpect(jsonPath("data.content").value("Enjoy And Donate"))
                .andExpect(jsonPath("data.festivalCategory").value("CHARITY"))
                .andDo(print());

    }

메서드를 만들고 test를 하는 과정에 있었다. 그런데 저렇게 오류가 나는 것이다. 그래서 로그를 확인 해보았다.

REQUEST : com.ticket.captain.festival.FestivalManagerController(initBinder) =  [_csrf -> (e1c3b860-7701-4bbd-abac-7d64bdd0f25f)]
RESPONSE : com.ticket.captain.festival.FestivalManagerController(initBinder) = null (9ms)
REQUEST : com.ticket.captain.festival.FestivalManagerController(initBinder) =  [_csrf -> (e1c3b860-7701-4bbd-abac-7d64bdd0f25f)]

나는 이때 어 나는 @Valid를 붙여주지도 않았는데 도대체 왜 initBinder가 실행이 됐고 그게 왜 또 2번이나 실행이 되었고, 2 번째 호출에서 Invalid Target 에러가 대체 왜 난거지??? 정말 멘붕 그 자체였다.

알고보니 initBinder는 Controller에 들어오는 모든 요청에 따라 다 처리를 해주는 것이었다. 그래서 이 update 메서드도 처리가 되었던 것이다. @Valid를 쓰지 않았음에도 불구하고 말이다. 그런데 여기서 문제가 생긴다.

    @Override
    public boolean supports(Class<?> clazz) {
        return clazz.isAssignableFrom(FestivalCreateDto.class);
    }

validator 의 supports 메서드이다. 내부의 클래스가 FestivalCreateDto.class 이다. 이 코드는 팀프로젝트여서 직접 짠 validator가 아니다. 하지만, 나도 처음에 custom validator를 배울 때에 특정 클래스를 저렇게 넣는 식으로 배웠었다. 왜냐하면 해당 예제에서는 넣은 그 특정 클래스에 대해 validate 을 시작했기 때문이다.

update 메서드에서 @RequestBody 로 FestivalUpdateDto 를 들여온다. 이제 이 상황에서 FestivalCreateDto와 맞지 않기 때문에 Invalid Target 에러가 발생한 것이다. 그래서 저 부분을 clazz로 넣어서 해결해주었다.

어... 그렇다면 대체 왜 initBinder가 2번이 호출이 된거고 위의 말처럼 클래스가 맞지않아 에러가 발생한거면 처음 호출된 initBinder는 어떻게 통과한거죠 ??

그것은 바로 모든 파라미터에 대해서 검사를 해서 그런 것 같다(추측). 추측이지만 99% 맞는 것 같다.
근거로 해당 메서드에 @PathVariable를 추가하거나 없애 주었을 때와 비례해서 initBinder가 호출된 것을 알 수가 있었다.
update 메서드의 파라미터 개수를 보면 2개이기 때문에 2번이 호출된 것이다. Long festivalId 값이 먼저 있으니 이 값은 통과되고 그 다음 파라미터에서 오류가 난 것이다. 이를 증명하기 위해 파라미터의 위치를 바꿔보았다.

@RequestBody FestivalUpdateDto festivalUpdateDto,@PathVariable Long festivalId 다음과 같이 바꾸고 진행하니깐

REQUEST : com.ticket.captain.festival.FestivalManagerController(initBinder) = [_csrf -> (a311ae8f-175c-4b9f-990c-fcb9990c28c0)]

이게 딱 1번 나오고 바로 에러가 난 것을 알 수가 있었다. 그리고 클래스가 아닌 기본타입이 들어오면 통과해주는 것 같다.

그러면 모든 값에 대해서 initBinder를 건다면 @Valid는 왜 하는거죠??
이 어노테이션을 붙이게 되면 다음과 같이 흘러간다.

REQUEST : com.ticket.captain.festival.FestivalManagerController(initBinder) = [_csrf

RESPONSE : com.ticket.captain.festival.FestivalManagerController(initBinder) = null (10ms)

REQUEST : com.ticket.captain.festival.FestivalManagerController(initBinder) = [_csrf

RESPONSE : com.ticket.captain.festival.FestivalManagerController(initBinder) = null (1ms)REQUEST : com.ticket.captain.festival.FestivalService(findByTitle) = [_csrf

initBinder 후에 findByTitle 를 실행하는 것을 볼 수가 있다. 이 메서드는 내가 validate 메서드에 적어둔 메서드이다.

즉, @Valid를 붙이게 되면 initBinder 이후에 validate 메서드를 적용한다는 것을 알 수가 있다.


정말 시간을 많이 잡아먹은 오류였다... 하지만, 큰거 하나 배우고 간다라는 생각이 들어서 해결하고 나니깐 기분은 좋다!

'Spring' 카테고리의 다른 글

[Spring] @Transactional 에 대해  (0) 2021.05.09
[Spring] @BeforeEach @BeforeAll @AfterEach @AfterAll 에 대해  (0) 2020.12.02
[Spring] @NotNull, @NotEmpty, @NotBlank 에 대해  (0) 2020.11.17
[Spring] @Builder에 대해  (0) 2020.11.16
[Spring] 클래스의 ToString에 대해  (0) 2020.11.04
    'Spring' 카테고리의 다른 글
    • [Spring] @Transactional 에 대해
    • [Spring] @BeforeEach @BeforeAll @AfterEach @AfterAll 에 대해
    • [Spring] @NotNull, @NotEmpty, @NotBlank 에 대해
    • [Spring] @Builder에 대해
    Bepoz
    Bepoz
    https://github.com/Be-poz/TIL

    티스토리툴바