모르지 않다는 것은 아는것과 다르다.

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

'Junit' 카테고리의 다른 글

Mockito  (0) 2022.02.27
Junit5 애노테이션 비교  (0) 2022.02.27
# Junit 5 기본 애노테이션  (0) 2022.02.27
Controller 단위테스트  (0) 2022.02.27
Assertion  (0) 2022.02.27