이전 글에서는 @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에 잘못된 값을 작성했다.
에러 메세지를 잘 리턴하고 있다.
'Spring Framework' 카테고리의 다른 글
@Parameters 애너테이션으로 테스트 하기 (0) | 2019.02.06 |
---|---|
Spring boot / Spring HATEOAS 간단 정리 (0) | 2019.02.01 |
@Valid 애너테이션 사용해서 값 검증하기 (2) | 2019.01.17 |
BadRequest 로 응답해서 입력 값 제한하기 (0) | 2019.01.16 |
Spring boot REST API 개발 - Dto 클래스로 입력 값 제한하기 (0) | 2019.01.11 |
댓글