RestApi 만들기 - badRequest (4)

반응형
반응형

만약에 검색 조건을 잘못 입력한 경우는 어떻게 될까?
최대한 간단하게 작성해봤다.

만약에, 주문 날짜가 배달 완료 날짜보다 늦은 경우라면 어떻게 해야할까?
솔직히 말이 되지 않는다.
이럴때는 어떻게 처리를 해야할까?

@Test
    void badRequest() throws Exception {
        DeliveryDto delivery = DeliveryDto.builder()
            .item("book")
            .user("klom")
            .deliveryTime(LocalDateTime.now().plusDays(10))
            .deliveryEndTime(LocalDateTime.now())
			.itemPrice(0)
            .build();
        mockMvc.perform(post("/api/delivery/")
            .accept(MediaTypes.HAL_JSON_VALUE)
            .contentType(MediaType.APPLICATION_JSON_VALUE)
            .content(objectMapper.writeValueAsString(delivery))
        )
            .andDo(print())
            .andExpect(status().isBadRequest())
        ;
    }

물론 지금은 상태코드가 201이 나온다.
왜냐하면 http입장에서는 이 코드도 틀린 코드는 아니기 때문이다.

결과는 예상과 다르게 나왔다.
어떻게 하면 좋을까?

생각해보니 테스트 코드도 수정해야 될것 같다.
왜냐하면 아무것도 입력하지 않는상태일때도 상태코드400이 나와야 된다.

@Test
void badRequest_empty_entity() throws Exception {
     DeliveryDto delivery = DeliveryDto.builder()
        .build();
     mockMvc.perform(post("/api/delivery/")
        .accept(MediaTypes.HAL_JSON_VALUE)
        .contentType(MediaType.APPLICATION_JSON_VALUE)
        .content(objectMapper.writeValueAsString(delivery))
      )
       .andDo(print())
       .andExpect(status().isBadRequest())
     ;
}

하지만 이것도 상태코드가 201이 나오게 된다. 왜냐하면 소스는 틀린것이 없기 때문이다.
바로 Validation을 하는 방법이 존재한다.
만약 Validation이 정상적으로 되어있지 않는다면,
400에러가 나오게 하면 된다.

가장 먼저 dto에 Validation을 추가해보자.
하지만 Validation이 존재하지 않는다. 스프링부트2.3부터인가 Validation이 web에서 독립되었기 때문이다.

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

이것을 추가하면 정상적으로 나온다.
아, 이것을 추가하지 않아도 @NotNull은 나오긴 하지만 Validation이 되는 어노테이션이 아니다.

import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;

public class DeliveryDto {
    @NotNull
    private String item;
    @NotNull
    private String user;
    @NotNull
    private LocalDateTime deliveryTime;
    @NotNull
    private LocalDateTime deliveryEndTime;
    @Min(0)
    private Integer itemPrice;
}

그리고 컨트롤러에 가서

import org.springframework.validation.Errors;
import javax.validation.Valid;
public ResponseEntity<?> createDelivery(@RequestBody @Valid DeliveryDto deliveryDto, Errors errors) {

이렇게 수정한다.

그리고

if(errors.hasErrors()) {
    return ResponseEntity.badRequest().build();
}

이것을 입력하게 되면,

ockHttpServletResponse:
           Status = 400
    Error message = null
          Headers = []
     Content type = null
             Body = 
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

상태코드가 400이 나온다는 걸 알 수 있다.

그러면 초기에 보여줬던,

@Test
    void badRequest() throws Exception {
        DeliveryDto delivery = DeliveryDto.builder()
            .item("book")
            .user("klom")
            .deliveryTime(LocalDateTime.now().plusDays(10))
            .deliveryEndTime(LocalDateTime.now())
    	    .itemPrice(0)
            .build();
        mockMvc.perform(post("/api/delivery/")
            .accept(MediaTypes.HAL_JSON_VALUE)
            .contentType(MediaType.APPLICATION_JSON_VALUE)
            .content(objectMapper.writeValueAsString(delivery))
        )
            .andDo(print())
            .andExpect(status().isBadRequest())
        ;
    }

 

이건 성공이다.

java.lang.AssertionError: Status expected:<400> but was:<201>
Expected :400
Actual   :201

왜 그럴까?
애초에 Validation에는 시간 체크하는 것이 없기 때문이다.
그러면 Validation를 해주는 클래스를 만드는방법이 존재한다.

@Component
public class DeliveryValidation {
  
  public void validate(DeliveryDto deliveryDto, Errors errors) {
    if(deliveryDto.getDeliveryEndTime().isAfter(deliveryDto.getDeliveryTime())) {
      errors.rejectValue("DeliveryTime", "wrong time");
    }
  }

}

이런식으로 에러를 추가할 수 있다.
그리고 컨트롤러에

validation.validate(deliveryDto, errors);
if (errors.hasErrors()) {
   return ResponseEntity.badRequest().build();
}

이 코드를 추가하게 되면,

MockHttpServletResponse:
           Status = 400
    Error message = null
          Headers = []
     Content type = null
             Body = 
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

응답이 정상적으로 동작된다는 것을 알 수 있다.

근데 우리는 rest-api를 만들고 있다. 그렇기 때문에 에러 정보도 필요할지도 모른다.
그러면

return ResponseEntity.created(createUri).body(deliver);

이것 처럼 body에 error를 넣어 보내면 되는것일까?
이건 애초에 잘못된 생각이다.

Caused by: com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class org.springframework.validation.DefaultMessageCodesResolver and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS) (through reference chain: org.springframework.validation.BeanPropertyBindingResult["messageCodesResolver"])

이런 에러가 나오는것을 확인 할 수 있는데,
그 이유는 애초에 직렬화가 정상적으로 되지 않았기 때문이다.
참고로 delivery같은 경우에는 적절히 자바빈 스펙을 준수했기 때문에 직렬화가 되는 것이라고 한다.

그러면 어떻게 해야할까?
json을 만들면된다.
다음은  json직렬화를 만드는 코드다.

@JsonComponent
public class DeliverySerializer extends JsonSerializer<Errors> {

<>에는 직렬화를 만드는 클래스를 작성하면 된다.

그리고 메소드를 오버라이드 시키면...
그러면

  @Override
  public void serialize(Errors errors, JsonGenerator jsonGenerator,
      SerializerProvider serializerProvider) throws IOException {
    
  }

 이런게 나오는데,
errors는 주체이며,
jsonGenerator으로 json을 만들 수 있다.
serializerProvider은 잘 모르겠지만, 직렬화를 도와주는 그런 것 같다.

그러면 어떤식으로 json을 만드는지 확인해보자.
사용되는 건 이렇게 4(+1)인데, 하나는 궁금해서 일단 작성해봤다.

jsonGenerator.writeStartObject();
jsonGenerator.writeStartArray();
jsonGenerator.writeStringField("filedName","name");
jsonGenerator.writeFieldName("name");
jsonGenerator.writeString("???");

실제 json을 만든다고 생각하면 만들기가 어렵지가 않다.

근데 뭘 근거로 json을 만들까?
사실 아무이름으로 해도 상관은 없다. 하지만 적어도 뭐가 있는지 안다면 더 좋지 않을까?

디버거를 통해 어떤것이 존재하는지 확인 해보자.
그러면 다음과 같은 결과를 얻을 수 있다.

자 본격적으로 만들어보자.
error에는 총 2가지가 있는데, globalErrors와 FieldErrors이렇게 2가지가 존재한다.

현재 나는 FiledErrors를 사용하기 때문에,

errors.getFieldErrors().forEach(e -> {
        
});

코드를 이렇게 작성했다.

여기는 배열로 만드는것이 좋을까? 아니면 객체(JSON으)로 만드는것이 좋을까?
리스트로 되어있기 때문에 배열로 만드는것이 좋다고 생각한다.

jsonGenerator.writeStartArray();
jsonGenerator.writeEndArray();

이렇게 배열을 만들 수 있다.
이 안에 필요한 필드를 넣어주면 된다.

그러면 이런 코드가 완성이 된다.

jsonGenerator.writeStartArray();
    errors.getFieldErrors().forEach(e -> {
      try {
        jsonGenerator.writeStartObject();
        jsonGenerator.writeStringField("field", e.getField());
        jsonGenerator.writeStringField("objectName", e.getObjectName());
        jsonGenerator.writeStringField("code", e.getCode());
        String result = e.getRejectedValue().toString();
        if (result != null) {
          jsonGenerator.writeStringField("rejectedValue", result);
        }
        jsonGenerator.writeEndObject();
      } catch (IOException ioException) {
        ioException.printStackTrace();
      }
    });
    jsonGenerator.writeEndArray();

rejectedValue는 반환값이기 때문에 반환이 되지 않을 수 있기 때문에 null check가 필수다.
자 이것을 실행해보자.
그러면 다음과 같은 결과를 얻을 수 있다.

Body = [{"field":"DeliveryTime","objectName":"deliveryDto","code":"wrong time","rejectedValue":"2021-02-13T22:07:37.794013"}]

*객체로 만들지 않으면
Can not write a field name, expecting a value가 등장한다.

참고로 globalErrors는

@Component
public class DeliveryValidation {
  
  public void validate(DeliveryDto deliveryDto, Errors errors) {
    if(deliveryDto.getDeliveryEndTime().isAfter(deliveryDto.getDeliveryTime())) {
      errors.reject("DeliveryTime", "wrong time");
    }
  }

}

이렇게 하면 된다. 여기에는 filed값이 존재하지 않는다.

반응형

댓글

Designed by JB FACTORY