본문 바로가기
Spring Framework

@Notnull , @Min 으로 검증 할 수 없는 입력 값 검증하기 - Validator 클래스 만들기

by 자유코딩 2019. 1. 18.

이전 글에서는 @Notnull @Min 을 사용해서 입력 값을 검증했다.

 

그런데 @Notnull 과 @Min 만으로는 검증 할 수 없는 경우가 있다.

 

아래 코드 예제를 한번 보자.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Builder @NoArgsConstructor @AllArgsConstructor
@Getter @Setter
public class EventDto {
    @NotEmpty
    private String name;
    @NotEmpty
    private String description;
    @NotNull
    private LocalDateTime beginEnrollmentDateTime;
    @NotNull
    private LocalDateTime closeEnrollmentDateTime;
    @NotNull
    private LocalDateTime beginEventDateTime;
    @NotNull
    private LocalDateTime endEventDateTime;
    private String location; // location 값이 없다면 온라인 모임
    @Min(0)
    private int basePrice; // optional
    @Min(0)
    private int maxPrice; // optional
    @Min(0)
    private int limitOfEnrollment;
}
cs

 

코드를 보면 beginEnrollmentDateTime , closeEnrollmentDateTime , beginEventDateTime, endEventDateTime 이 있다.

 

예를 들어서 beginEventDateTime 이 endEventDateTime 보다 값이 크면 문제가 있다.

 

이벤트의 시작 시간이 종료시간보다 늦는 다는 것은 말이 되지 않는다.

 

이럴 경우 Bad Request( 400 ) 을 발생시켜야 한다.

 

Validator 클래스를 만들어서 사용하면 된다.

 

Validator 클래스를 아래와 같이 작성했다.

 

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
@Component
public class EventValidator {
 
    public void validate(EventDto eventDto, Errors errors){
        if (eventDto.getBasePrice() > eventDto.getMaxPrice() && eventDto.getMaxPrice() > 0) {
            errors.rejectValue("basePrice""wrongValue""BasePrice is wrong");
            errors.rejectValue("maxPrice""wrongValue""maxPrice is wrong");
            //errors.reject("wrongPrices","Values fo Prices are wrong"); // reject 는 global error
        }
 
        LocalDateTime endEventDateTime = eventDto.getEndEventDateTime();
        if (endEventDateTime.isBefore(eventDto.getBeginEventDateTime()) ||
        endEventDateTime.isBefore(eventDto.getCloseEnrollmentDateTime()) ||
        endEventDateTime.isBefore(eventDto.getBeginEnrollmentDateTime())) {
            errors.rejectValue("endEventDateTime""wrongValue""endEventDateTime is wrong"); // reject value 는 field error
        }
 
        LocalDateTime beginEvnetDateTime = eventDto.getBeginEventDateTime();
        if (beginEvnetDateTime.isAfter(eventDto.getEndEventDateTime()) ||
        beginEvnetDateTime.isBefore(eventDto.getCloseEnrollmentDateTime()) ||
        beginEvnetDateTime.isBefore(eventDto.getBeginEnrollmentDateTime())){
            errors.rejectValue("beginEventDateTime""wrongValue""beginEventDateTime is wrong");
        }
 
        LocalDateTime closeEnrollmentDateTime = eventDto.getCloseEnrollmentDateTime();
        if (closeEnrollmentDateTime.isBefore(eventDto.getBeginEnrollmentDateTime()) ||
        closeEnrollmentDateTime.isAfter(eventDto.getBeginEventDateTime()) ||
        closeEnrollmentDateTime.isAfter(eventDto.getEndEventDateTime())){
            errors.rejectValue("closeEnrollmentDateTime""wrongValue""closeEnrollmentDateTime is wrong");
        }
    }
 
}
cs

 

컨트롤러는 아래와 같이 작성했다.

 

조만간 영상으로 코드리뷰를 해야 할 것 같다.

 

글로 이렇게 작성하면 작성자인 나는 보기가 좋은데 읽는 사람은 잘 이해가 안될수도 있을 것 같다.

 

컨트롤러는 아래와 같다.

@PostMapping
public ResponseEntity createEvent(@RequestBody @Valid EventDto eventDto, Errors errors){// 모델매퍼를 활용해서 EventDTO 를 Event 로 바꾼다.
if (errors.hasErrors()) { // @Valid 애너테이션에 의해서 입력값이 걸러지면 여기에 걸린다. , 처음부터 @Valid만 써봐도 값이 잘못 된 경우
return ResponseEntity.badRequest().body(errors);
}

eventValidator.validate(eventDto, errors); //validate 함수를 호출했을때 잘못된 값이 있는 경우
if (errors.hasErrors()) {
return ResponseEntity.badRequest().body(errors);
}

// Event event = Event.builder() 1. ModelMapper 를 사용하지 않는 방법
// .name(eventDto.getName())
// .description(eventDto.getDescription())
//ModelMapper 를 사용하는 방법
Event event = modelMapper.map(eventDto , Event.class); // 위에 사용하지 않는 방법은 많은 값을 입력한다. //ModelMapper 를 사용하면 이 1줄로 들어온 모든 값을 1세팅 할 수 있다.
event.update();
Event newEvent = this.eventRepository.save(event);
URI createUri = linkTo(EventController.class).slash(newEvent.getId()).toUri();
return ResponseEntity.created(createUri).body(event);
}

ResponseEntity 의 body 에 errors 객체를 담았다.

 

원래는 이렇게 하면 에러가 발생한다.

 

event 객체는 자동으로 serialize 할 수 있다. 그러나 Errors 객체는 그렇지 않다.

 

-   EventDto 같은 객체를 json 문서로 변환하는 것을 Serialize 라고 한다.

-   반대로 json 을 객체로 바꾸는 것은 Deserialize 라고 한다.

 

그래서 Errors 객체를 Serialize 할 Serializer 클래스도 만든다.

 

아래와 같다.

 

@JsonComponent
public class ErrorsSerializer extends JsonSerializer<Errors> { // 이게 필요한 이유는 Event 객체는 json 으로 Serialize 해서 리턴 할 수 있다.
//하지만 Errors 객체는 Serialize 가 안되기 때문에 이렇게 Serializer 클래스를 만들어서 Serialize 하고 리턴한다.
//Json Serializer의 제네릭에 <Errors>를 명시했다.
@Override
public void serialize(Errors errors, JsonGenerator gen, SerializerProvider serializers) throws IOException { // serialize 함수를 override 한다.
gen.writeStartArray(); // errors 안에는 에러가 여러개 있는데 이것을 배열로 담기 위해서 이렇게 작성한다.
errors.getFieldErrors().forEach( e -> { // EventValidator 클래스에서 rejectValue 라고 작성되어 있다면 이 부분이 사용된다.
try {
gen.writeStartObject();
gen.writeStringField("objectName", e.getObjectName());
gen.writeStringField("field" , e.getField());
gen.writeStringField("defaultMessage", e.getDefaultMessage());
gen.writeStringField("code", e.getCode());
Object rejectedValue = e.getRejectedValue();
if (rejectedValue != null){
gen.writeStringField("rejectedValue", rejectedValue.toString());
}
gen.writeEndObject();
}
catch (Exception e1) {
e1.printStackTrace();
}
});
errors.getGlobalErrors().forEach(e -> { // EventValidator 클래스에서 reject 라고 작성되어 있다면 이 부분이 사용된다.
try{
gen.writeStartObject();
gen.writeStringField("objectName", e.getObjectName());
gen.writeStringField("code", e.getCode());
gen.writeStringField("defaultMessage", e.getDefaultMessage());
gen.writeEndObject();
}
catch (Exception e1){
e1.printStackTrace();
}
});
gen.writeEndArray();// errors 안에는 에러가 여러개 있는데 이것을 배열로 담기 위해서 이렇게 작성한다.
}
}

주석으로도 설명을 적었다.

 

JsonSerializer 클래스를 상속하고 serialize 함수를 override 하면 된다.

 

errors에서 field 에러와 global 에러에 따라 JsonGenerator 객체에 String field를 작성한다.

 

자 이제 테스트코드를 사용해서 요청을 보내본다.

 

beginEventDateTime과 basePrice 값에 일부러 잘못된 값을 담았다.

@Test
@TestDescription("입력 값이 잘못된 경우에 에러가 발생하는 테스트")
public void createEvent_Bad_Request_Wrong_Input() throws Exception {
EventDto eventDto = EventDto.builder()
.name("Spring")
.description("REST API Development with Spring")
.beginEnrollmentDateTime(LocalDateTime.of(2018,11,20,10,0))
.closeEnrollmentDateTime(LocalDateTime.of(2018,11,21,10,0))
.beginEventDateTime(LocalDateTime.of(2018,11,25,10,0))
.endEventDateTime(LocalDateTime.of(2018,11,24,10,0))
.basePrice(10000)
.maxPrice(200)
.limitOfEnrollment(100)
.location("로케이션")
.build();

mockMvc.perform(post("/api/events/") //perform 안에는 요청 uri 를 적는다.
.contentType(MediaType.APPLICATION_JSON_UTF8) //요청 content type
.content(objectMapper.writeValueAsString(eventDto))) // HAL = Hypertext Application Language , 응답 내용을 ObjectMapper 로 작성한다.(JSON)
.andDo(print()) // 요청 정보를 모두 출력한다.
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$[0].objectName").exists())
.andExpect(jsonPath("$[0].field").exists())
.andExpect(jsonPath("$[0].defaultMessage").exists())
.andExpect(jsonPath("$[0].code").exists())
.andExpect(jsonPath("$[0].rejectedValue").exists()); // 응답이 어떤지 확인한다. - andExpect()

}

 

콘솔에 나타난 요청 정보이다.

 

MockHttpServletRequest:
      HTTP Method = POST
      Request URI = /api/events/
       Parameters = {}
          Headers = {Content-Type=[application/json;charset=UTF-8]}
             Body = {"name":"Spring","description":"REST API Development with Spring","beginEnrollmentDateTime":"2018-11-20T10:00:00","closeEnrollmentDateTime":"2018-11-21T10:00:00","beginEventDateTime":"2018-11-25T10:00:00","endEventDateTime":"2018-11-24T10:00:00","location":"로케이션","basePrice":10000,"maxPrice":200,"limitOfEnrollment":100}
    Session Attrs = {}


 

응답 정보는 이렇다.

 


MockHttpServletResponse:
           Status = 400
    Error message = null
          Headers = {Content-Type=[application/hal+json;charset=UTF-8]}
     Content type = application/hal+json;charset=UTF-8
             Body = [{"objectName":"eventDto","field":"basePrice","defaultMessage":"BasePrice is wrong","code":"wrongValue","rejectedValue":"10000"},{"objectName":"eventDto","field":"maxPrice","defaultMessage":"maxPrice is wrong","code":"wrongValue","rejectedValue":"200"},{"objectName":"eventDto","field":"endEventDateTime","defaultMessage":"endEventDateTime is wrong","code":"wrongValue","rejectedValue":"2018-11-24T10:00"},{"objectName":"eventDto","field":"beginEventDateTime","defaultMessage":"beginEventDateTime is wrong","code":"wrongValue","rejectedValue":"2018-11-25T10:00"}]
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

 

테스트 코드가 아닌 Postman 으로도 확인해본다.

 

요청 값이 정상인 경우엔 아래와 같이 나온다.

 

 

에러를 발생시키면 아래와 같이 나온다.

 

 

beginEnrollmentDateTime에 잘못된 값을 작성했다.

 

에러 메세지를 잘 리턴하고 있다.

 

댓글