본문 바로가기
Java, Kotlin, Spring/Spring REST API

Spring REST API - BadRequest

by Wordbe 2021. 1. 23.
728x90

Spring REST API - Bad Request

빈 입력값 처리

JSR 303 라이브러리에 있는 Bean Validation 을 사용할 수 있다.

pom.xml

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

각 필드마다 필드의 속성들을 어노테이션으로 달아준다.

@Builder @AllArgsConstructor @NoArgsConstructor
@Data
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; // null 이면 온라인 모임
    @Min(0)
    private int basePrice;
    @Min(0)
    private int maxPrice;
    @Min(0)
    private int limitOfEnrollment;
}

컨트롤러에서 메소드의 파라미터로 @Valid 를 붙여서 Dto 를 받고, 이것에 대한 에러가 있다면 Errors 객체에 담아지게 된다.

@PostMapping
public ResponseEntity createEvent(@RequestBody @Valid EventDto eventDto, Errors errors) {
    // 에러값이 있으면 badRequest 를 리턴한다.
  if (errors.hasErrors()) return ResponseEntity.badRequest().build();
  ...
}

테스트코드는 아래와 같다.

@SpringBootTest
@AutoConfigureMockMvc
public class EventControllerTest {

    @Autowired
    MockMvc mockMvc;

    @Autowired
    ObjectMapper objectMapper;
      ...

    @Test
    @TestDescription("입력값이 비어있는 경우 에러가 발생하는 이벤트 생성 테스트")
    public void createEvent_badRequest_emptyInput() throws Exception {
        EventDto eventDto = EventDto.builder().build();

        this.mockMvc.perform(post("/api/events")
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(this.objectMapper.writeValueAsString(eventDto)))
                .andDo(print())
                .andExpect(status().isBadRequest());
    }
}

테스트가 여러개인 경우, 무슨 테스트인지 서술해주기 위해 주석을 달아서 표현하면 좋다. 주석 대신에 커스텀 어노테이션을 사용해서 안에 서술할 수 도 있다. 만드는 방법은 간단하다.

패키지/common 폴더(이름은 자유)를 만들고 아래 클래스를 생성한다.

@Target(ElementType.METHOD) // 메소드 위에 붙이는 주석(annotation)
@Retention(RetentionPolicy.SOURCE) // 소스 단계까지 유지
public @interface TestDescription {
    String value();
}

RetentionPolicy 는 어노테이션의 유효기간을 나타낸다.

  • SOURCE 는 컴파일 전이다. 즉 주석처럼 사용하는 것이다.
  • CLASS 는 컴파일까지만 메모리에 들어있고 런타임에서는 사라진다. 리플렉션으로 선언된 어노테이션 데이터를 가져올 수 없게 된다.
  • RUNTIME 은 런타임까지 사용할 수 있다. JVM이 자바 바이트코트가 담긴 class 파일에서 런타임환경을 구성하고, 런타임을 종료할 때까지 메모리가 살아있다.

 


잘못된 입력값 처리

어노테이션만으로 NotEmpty, NonNull, Min 등등은 가능하지만 구체적인 검증로직을 구현하는 것은 한계가 있다.

validator 를 별도로 구현해보자.

@Component
public class EventValidator {

    public void validate(EventDto eventDto, Errors errors) {
        if (eventDto.getBasePrice() > eventDto.getMaxPrice()) {
            // 필드 에러
              errors.rejectValue("basePrice", "wrongValue", "BasePrice 가 잘못되었습니다.");

              // 글로벌 에러
            errors.reject("wrongValue", "price 가 잘못되었습니다.");
        }

        if (eventDto.getCloseEnrollmentDateTime().isBefore(eventDto.getBeginEnrollmentDateTime())) {
            errors.rejectValue("closeEnrollmentDateTime", "wrongValue", "closeEnrollmentDateTime 가 잘못되었습니다.");
        }

    }
}

위 예시와 같이 비즈니스 로직에 위반된 조건을 제시하고, 에러가 발생하면 Errors.rejectValue() 를 발생시켜준다.

컨트롤러 수정

@Controller
@RequestMapping(value = "/api/events", produces = MediaTypes.HAL_JSON_VALUE)
@RequiredArgsConstructor
public class EventController {

    private final EventRepository eventRepository;
    private final ModelMapper modelMapper;
    private final EventValidator eventValidator;

    @PostMapping
    public ResponseEntity createEvent(@RequestBody @Valid EventDto eventDto, Errors errors) {
        if (errors.hasErrors()) return ResponseEntity.badRequest().build();

	// Validator 추가
        eventValidator.validate(eventDto, errors);
        if (errors.hasErrors()) {
            errors.getAllErrors().forEach(System.out::println);
            return ResponseEntity.badRequest().build();
        }
		...
    }
}

Validator 를 추가하고 에러가 발생하면 BadRequest 응답을 반환한다.

 

@SpringBootTest
@AutoConfigureMockMvc
public class EventControllerTest {

    @Autowired
    MockMvc mockMvc;

    @Autowired
    ObjectMapper objectMapper;

    @Test
    @TestDescription("입력 값이 잘못된 경우 에러가 발생하는 이벤트 생성 테스트")
    public void createEvent_badRequest_wrongInput() throws Exception {
        EventDto eventDto = EventDto.builder()
                                ...
                      // 고의로 위반 로직 설정: 종료등록시간이 시작등록시간보다 이전일수 없음.
                .beginEnrollmentDateTime(LocalDateTime.of(2021,01,31,12,00,00)) 
                .closeEnrollmentDateTime(LocalDateTime.of(2021,01,30,12,00,00))
                                ...
                      // 고의로 위반 로직 설정: maxPrice >= basePrice 여야함.
                .basePrice(1000)
                .maxPrice(200)
                ...
                .build();

        this.mockMvc.perform(post("/api/events")
                .contentType(MediaType.APPLICATION_JSON)
                .content(this.objectMapper.writeValueAsString(eventDto)))
                .andDo(print())
                .andExpect(status().isBadRequest());
    }
}





Custom Validator, 에러 Serializer 구현

시리얼라이즈 라는것은 모델 객체를 json 타입으로 변환해주는 것을 말한다.

 

modelMapper의 BeanSerializer 는 빈 객체를 json 으로 변환시킨다. (자바 빈 스펙을 준수한다.)

하지만 Errors 객체는 빈으로 등록되어 있지 않기 때문에 json으로 변환되지 못한다.

따라서 Errors 를 핸들링할 수 있는 커스텀 에러 시리얼라이저를 만들어보자.

@JsonComponent
public class ErrorsSerializer extends JsonSerializer<Errors> {

    @Override
    public void serialize(Errors errors, JsonGenerator gen, SerializerProvider serializerProvider) 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 ioException) {
                ioException.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 ioException) {
                ioException.printStackTrace();
            }
        });

        gen.writeEndArray();
    }
}

스프링부트가 제공하는 @JsonComponent 를 이용하면 JsonSerializerObjectMapper 에 등록할 수 있다.

serialze 메소드를 오버라이딩하고, 필드에러와, (필요하면) 글로벌 에러 json 을 만들면 된다.

 

테스트 코드를 보자.

@Test
@TestDescription("입력 값이 잘못된 경우 에러가 발생하는 이벤트 생성 테스트")
public void createEvent_badRequest_wrongInput() throws Exception {
  EventDto eventDto = EventDto.builder()
    .name("Spring")
    .description("REST API Development")
    .beginEnrollmentDateTime(LocalDateTime.of(2021,01,31,12,00,00))
    .closeEnrollmentDateTime(LocalDateTime.of(2021,01,30,12,00,00))
    .beginEventDateTime(LocalDateTime.of(2021,01,29,12,00,00))
    .endEventDateTime(LocalDateTime.of(2021,01,23,12,00,00))
    .basePrice(1000)
    .maxPrice(200)
    .limitOfEnrollment(100)
    .location("D2 스타트업 팩토리")
    .build();

  this.mockMvc.perform(post("/api/events")
                       .contentType(MediaType.APPLICATION_JSON)
                       .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())
    ;
}

json 경로에 배열이 들어있고, 그 필드값으로 위 3개의 값이 담긴 것을 확인할 수 있다.

728x90

'Java, Kotlin, Spring > Spring REST API' 카테고리의 다른 글

Spring REST API - HATEOAS  (0) 2021.01.24
Spring REST API - 테스트코드 파라미터사용  (0) 2021.01.23
Spring REST API - 입력값 처리  (0) 2021.01.23
Spring REST API TEST  (0) 2021.01.22
Spring REST API  (0) 2021.01.22

댓글