@Valid와 BindingResult (또는 Errors)
- 스프링 MVC에 해당하는 내용 JS303 애노테이션을 사용해 확인할 수 있다.
- @Valid 라는 애노테이션을 붙이면 Entity에 바인딩을 할때 애노테이션들에 대한 정보를 참고해서 검증을 수행할 수 있다.
- 검증을 수행한 결과를 객체 오른쪽에 있는 Errors 객체에 에러값들을 넣어준다.
- 받은 에러를 확인 해서 Bad Request를 발생시킨다.
- BindingResult는 항상 @Valid 바로 다음 인자로 사용해야 한다. (스프링 MVC)
- @NotNull, @NotEmpty, @Min, @Max, ... 사용해서 입력값 바인딩할 때 에러 확인할 수 있다.
Errors
- rejectValue: 필드 에러
- reject: 글로벌 에러
BindingError
- FieldError 와 GlobalError (ObjectError)가 있음
- objectName
- defaultMessage
- code
- field
- rejectedValue
코드 예시
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class EventDto {
@NotEmpty
private String name; //이벤트 네임
@NotEmpty
private String description; // 설명
@NotNull
private LocalDateTime beginEnrollmentDateTime; //등록 시작일시
@NotNull
private LocalDateTime closeEnrollmentDateTime; //종료일시
private LocalDateTime beginEventDateTime; //이벤트 시작일시
@NotNull
private LocalDateTime beginEventDateTime; //이벤트 시작일시
@NotNull
private LocalDateTime endEventDateTime; //이벤트 종료일시
private String location; // (optional) 이벤트 위치 이게 없으면 온라인 모임
@Min(0)
private int basePrice; // (optional) 기본 금액
@Min(0)
private int maxPrice; // (optional) 최고 금액
@Min(0)
private int limitOfEnrollment; //등록한도
}
- 다음과 같이 dto를 만들면 validation 처리를 할 수 있다.
- 또한 아래처럼 validation 클래스를 만들 수 있다.
@Component
public class EventValidator {
public void validate(EventDto eventDto, Errors errors) {
String wrongValue = "wrongValue";
if(eventDto.getBasePrice() > eventDto.getMaxPrice() && eventDto.getMaxPrice() > 0) {
errors.rejectValue("basePrice", wrongValue, "BasePrice is wrong");
errors.reject("wrongPrices", "Values fo prices are wrong");
}
LocalDateTime endEventDateTime = eventDto.getEndEventDateTime();
if(endEventDateTime.isBefore(eventDto.getBeginEventDateTime()) ||
endEventDateTime.isBefore(eventDto.getCloseEnrollmentDateTime()) ||
endEventDateTime.isBefore(eventDto.getBeginEnrollmentDateTime())) {
errors.rejectValue("endEventDateTime", wrongValue, "endEventDateTime is wrong");
}
}
}
- @Component 로 빈에 등록했기 때문에 통합테스트에 경우 가져다가 쓸 수 있다.
- rejectValue 를 통해서 필드 에러를 저장할 수 있다.
- reject 를 통해서 글로벌 에러로 저장할 수도 있다.
@PostMapping
public ResponseEntity createEvent(@RequestBody @Valid EventDto eventDto, Errors errors) {
if(errors.hasErrors())
return ResponseEntity.badRequest().build();
eventValidator.validate(eventDto, errors);
if(errors.hasErrors()) {
return ResponseEntity.badRequest().build();
}
...
}
- if(errors.hasErrors()) 를 통해서 @Valid 를 통해서 발생한 에러나 혹은 기본적으로 발생할 수 있는 에러에 대해서 검증한다.
- eventValidator.validate(eventDto, errors) 를 통해서 직접만든 validate 클래스를 통해서 에러를 검증한다.
Errors JSON 변환의 문제점
- BeanSerializer을 사용해서 Java Bean Spec을 준수하는 객체를 JSON으로 변환할 수 있다.
- ObjectMapper에 여러가지 Serializer가 등록이 되어있다.
- Errors는 Java Bean Spec을 준수하는 객체가 아니라 JSON객체로 변환할 수가 없다.
- produces = MediaTypes.HAL_JSON_UTF8_VALUE 이 지정되어있어 응답 처리시 JSON으로 변환하는 처리를 하는데 변환 처리를 할 수 없어서 에러가 발생한다.
- 그렇기 때문에 ErrorsSerializer를 구현해 줘야한다.
@JsonComponent
public class ErrorsSerializer extends JsonSerializer<Errors> {
@Override
public void serialize(Errors errors, JsonGenerator gen, SerializerProvider serializers) throws IOException {
gen.writeStartArray();
errors.getFieldErrors().forEach(e -> {
try {
gen.writeStartObject();
gen.writeStringField("field", e.getField());
gen.writeStringField("objectName", e.getObjectName());
gen.writeStringField("code", e.getCode());
gen.writeStringField("defaultMessage", e.getDefaultMessage());
Object rejectedValue = e.getRejectedValue();
if (rejectedValue != null) {
gen.writeStringField("rejectedValue", rejectedValue.toString());
}
gen.writeEndObject();
} catch (IOException e1) {
e1.printStackTrace();
}
});
errors.getGlobalErrors().forEach(e -> {
try {
gen.writeStartObject();
gen.writeStringField("objectName", e.getObjectName());
gen.writeStringField("code", e.getCode());
gen.writeStringField("defaultMessage", e.getDefaultMessage());
gen.writeEndObject();
} catch (IOException e1) {
e1.printStackTrace();
}
});
gen.writeEndArray();
}
}
public class EventControllerTests {
...
@Test
@TestDescription("입력 값이 잘못된 경우에 에러가 발생하는 테스트")
public void createEvent_Bad_Request_Wrong_Input() throws Exception {
...
this.mockMvc.perform(post("/api/events")
.contentType(MediaType.APPLICATION_JSON_UTF8)
.content(this.objectMapper.writeValueAsString(eventDto)))
.andDo(print())
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$[0].objectName").exists())
.andExpect(jsonPath("$[0].defaultMessage").exists())
.andExpect(jsonPath("$[0].code").exists())
;
}
}
- 이제 다음과 같이 에러를 검증할 수 있게 되었다.
REFERENCES