Junit
                
              Bad Request 처리하기
                채마스
                 2022. 2. 27. 01:38
              
                          
            @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
- 백기선님의 rest api