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