Home Custom Method Argument Not Valid Exception
Post
Cancel

Custom Method Argument Not Valid Exception

  • Bean Validation 이용중 request body를 이용하여 요청 파라미터를 검증 할때에
    에러 메세지를 field : error message 형태의 json 문자열로 리턴해주기 위한 설정이 필요 했음.

  • 검증 대상이 될 Dto

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
@Getter
@Setter
@Builder
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class CustomerDto {

  @NotNullEmail
  @IdDuplicateCheck(tableName = "customer", columnName = "customer_id")
  private String customerId;

  @NotBlank
  private String firstName;

  @NotBlank
  private String lastName;

  @NotBlank
  private String address;

  @NotBlank
  private String phone;

}


테스트 컨트롤러

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping(path = "/test")
public class TestController {

  private final CustomerService customerService;

  @PostMapping("/customer-dto-validator")
  public ResponseEntity<String> customDtoValidator(@Valid @RequestBody CustomerDto customerDto) {
    customerService.save(customerDto);
    log.info("customerDto = {}", customerDto);
    return ResponseEntity.ok("valid pass");
  }

}


RequestBody

  • 여기서 customerId는 이미 등록되어있는 id
1
2
3
4
5
6
7
{
    "customerId":"johnDoe@gmail.com",
    "firstName":"john",
    "lastName":"doe",
    "address":"동작대로 xx길 xxx xx",
    "phone":"555-0101"
}


  • 위와 같은 코드를 실행 시키면 customerId에서 아이디 중복이나기 때문에 BindingResult 의 값이 JSON 형태로 반환되어야함
  • 반환 텍스트
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
{
    "timestamp": "2024-02-20T08:05:13.575+00:00",
    "status": 400,
    "error": "Bad Request",
    "trace": "...",
    "message": "Validation failed for object='customerDto'. Error count: 1",
    "errors": [
        {
            "codes": [
                "IdDuplicateCheck.customerDto.customerId",
                "IdDuplicateCheck.customerId",
                "IdDuplicateCheck.java.lang.String",
                "IdDuplicateCheck"
            ],
            "arguments": [
                {
                    "codes": [
                        "customerDto.customerId",
                        "customerId"
                    ],
                    "arguments": null,
                    "defaultMessage": "customerId",
                    "code": "customerId"
                },
                {
                    "arguments": null,
                    "defaultMessage": "customer_id",
                    "codes": [
                        "customer_id"
                    ]
                },
                {
                    "arguments": null,
                    "defaultMessage": "customer",
                    "codes": [
                        "customer"
                    ]
                }
            ],
            "defaultMessage": "존재하는 아이디 입니다.",
            "objectName": "customerDto",
            "field": "customerId",
            "rejectedValue": "johnDoe@gmail.com",
            "bindingFailure": false,
            "code": "IdDuplicateCheck"
        }
    ],
    "path": "/test/customer-dto-validator"
}


  • 반환된 데이터에는 쓸만한 데이터가 모두 있지만 너무 많은게 탈이다. 원하는 메세지만 사용자화 시켜 사용할 수 있어야함.
    • 순서
      • 1 ] BindingResult 를 인자로 받는CustomArgumentNotValidException 생성
      • 2 ] @RestControllerAdvice 생성
      • 3 ] @ExceptionHandler(CustomArgumentNotValidException.class) 생성
      • 4 ] CustomArgumentNotValidException 의 BindingResult 로 부터 메세지 바인딩 후 리턴
      • 5 ] 해당 기능을 사용할 컨트롤러에서 해당 예외를 터트려줘야함


CustomArgumentNotValidException

  • RuntimeException 을 상속 받은후 컨트롤러에서 BindingResult 객체를 인자로 받아서 예외틑 터트린다.
1
2
3
4
5
6
7
8
9
10
11
@Getter
public class CustomArgumentNotValidException extends RuntimeException {

  private final BindingResult bindingResult;

  public CustomArgumentNotValidException(BindingResult bindingResult) {
    this.bindingResult = bindingResult;
  }

}


@RestControllerAdvice

  • 예외를 프로젝트 내에서 전적으로 처리하여 예외 처리를 공통화
1
2
3
4
5
6
7
@Slf4j
@RestControllerAdvice
public class GlobalControllerAdvice {


}


@ExceptionHandler(CustomArgumentNotValidException.class)

  • @RestControllerAdvice 에 예외 핸들러 추가
    • 이제 CustomArgumentNotValidException 발생시 해당 처리기를 통해 예외가 처리된다.
1
2
3
4
5
6
7
8
9
10
11
12
@Slf4j
@RestControllerAdvice
public class GlobalControllerAdvice {

  @ResponseStatus(HttpStatus.BAD_REQUEST)
  @ExceptionHandler(CustomArgumentNotValidException.class)
  public void handleCustomArgumentNotValidExceptions(CustomArgumentNotValidException ex) {

  }

}


CustomArgumentNotValidException 바인딩

  • 예외가 발생하고 해당 예외가 포함하고 있는 BindingResult를 통해 에러 메세지를 커스텀화 한다.
    • 간단하게 여기에서는 Map 형태로 key = field, value = error message 설정.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Slf4j
@RestControllerAdvice
public class GlobalControllerAdvice {

  @ResponseStatus(HttpStatus.BAD_REQUEST)
  @ExceptionHandler(CustomArgumentNotValidException.class)
  public Map<String, String> handleCustomArgumentNotValidExceptions(CustomArgumentNotValidException ex) {
    Map<String, String> errors = new HashMap<>();
    List<FieldError> fieldErrors = ex.getBindingResult().getFieldErrors();
    for (FieldError fieldError : fieldErrors) {
      errors.put(fieldError.getField(), fieldError.getDefaultMessage());
    }
    log.info("errors = {}", errors);
    return errors;
  }

}


테스트 컨트롤러

  • 글 맨 처음에 설명했던 테스트 컨트롤러를 이용해서 값을 검증할 때 BindingResult 처리부분 추가.

  • 수정된 컨트롤러

    • BindingResult 를 인자로 받고, 에러 검증을 하는 코드, 예외를 발생시키는 코드가 추가됨.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping(path = "/test")
public class TestController {

    private final CustomerService customerService;

    @PostMapping("/customer-dto-validator")
    public ResponseEntity<String> customDtoValidator(@Valid @RequestBody CustomerDto customerDto, BindingResult bindingResult) {

        if (bindingResult.hasErrors()) {
            throw new CustomArgumentNotValidException(bindingResult);
        } else {
            customerService.save(customerDto);
            log.info("customerDto = {}", customerDto);
            return ResponseEntity.ok("valid pass");
        }

    }

}

요청 결과

postman_test


테스트 코드

  • 설명 생략
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
@Transactional
@SpringBootTest
@AutoConfigureMockMvc
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class ValidatorTest {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private MessageSource messageSource;

    @Autowired
    private CustomerRepository customerRepository;

    @Autowired
    private CustomerService customerService;

    @Autowired
    private LocalValidatorFactoryBean validator;

    private CustomerDto customerDto;

    @BeforeEach
    void beforeEach() {
        customerDto =
                CustomerDto.builder()
                        .customerId("johnDoe@gmail.com")
                        .firstName("john")
                        .lastName("doe")
                        .address("동작대로 xx길 xxx xx")
                        .phone("555-0101")
                        .build();
    }

    @BeforeEach
    void afterEach() {
        customerDto =
                CustomerDto.builder()
                        .customerId("johnDoe@gmail.com")
                        .firstName("john")
                        .lastName("doe")
                        .address("동작대로 xx길 xxx xx")
                        .phone("555-0101")
                        .build();

        customerRepository.deleteCustomerByCustomerId(customerDto.getCustomerId());

    }

    @Test
    @Order(1)
    void id_duplicated_validator() {
        Set<ConstraintViolation<CustomerDto>> validated = validator.validate(customerDto);
        Assertions.assertEquals(0, validated.size());
        customerService.save(customerDto);
        validated = validator.validate(customerDto);
        Assertions.assertEquals(1, validated.size());
        ConstraintViolation<CustomerDto> violation = validated.iterator().next();
        String errorMessage = violation.getMessage();
        Assertions.assertEquals(messageSource.getMessage("validation.duplicated.id", null, Locale.KOREA), errorMessage);
    }

    @Test
    @Order(2)
    void validator_integrated_test_valid_pass() throws Exception {
        ResultActions resultActions = customerDtoValidatorRequest(customerDto);
        resultActions.andExpect(MockMvcResultMatchers.status().isOk());
        String responseBody = resultActions.andReturn().getResponse().getContentAsString();
        Assertions.assertEquals("valid pass", responseBody);
    }

    @Test
    @Order(3)
    void validator_integrated_test_valid_fail_case1_emptyId() throws Exception {
        String notNullEmail = messageSource.getMessage("validation.not.null.email", null, Locale.getDefault());
        customerDto.setCustomerId("");
        ResultActions resultActions = customerDtoValidatorRequest(customerDto);
        resultActions.andExpect(MockMvcResultMatchers.status().isBadRequest());
        resultActions.andExpect(MockMvcResultMatchers.jsonPath("$.customerId", Is.is(notNullEmail)));
    }

    @Test
    @Order(4)
    void validator_integrated_test_valid_fail_case2_duplicateId() throws Exception {
        CustomerDto save = customerService.save(customerDto);
        Assertions.assertNotNull(save);
        String duplicateId = messageSource.getMessage("validation.duplicated.id", null, Locale.getDefault());
        ResultActions resultActions = customerDtoValidatorRequest(customerDto);
        resultActions.andExpect(MockMvcResultMatchers.status().isBadRequest());
        resultActions.andExpect(MockMvcResultMatchers.jsonPath("$.customerId", Is.is(duplicateId)));
    }

    private ResultActions customerDtoValidatorRequest(CustomerDto customerDto) throws Exception {
        String req = objectMapper.writeValueAsString(customerDto);
        return mockMvc.perform(
                MockMvcRequestBuilders
                        .post("/test/customer-dto-validator")
                        .content(req)
                        .contentType(MediaType.APPLICATION_JSON_VALUE)
        ).andDo(MockMvcResultHandlers.print());
    }

}

결과

test_result

This post is licensed under CC BY 4.0 by the author.