[Spring REST API #7] Bad Request 에 대한 본문 메세지

| Bad Request 응답 본문 만들기

의존성

<dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-hateoas</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
    <optional>true</optional>
    </dependency>
    <dependency>
      <groupId>org.modelmapper</groupId>
      <artifactId>modelmapper</artifactId>
      <version>2.3.1</version>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>com.h2database</groupId>
      <artifactId>h2</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.restdocs</groupId>
      <artifactId>spring-restdocs-mockmvc</artifactId>
      <scope>test</scope>
    </dependency>
</dependencies>

설정

application.yml

spring:
  jackson:
    deserialization:
      fail-on-unknown-properties: true

 

 

errors에 있는 에러 정보들을 응답 메세지에 포함하기 위해서는 body 메서드에 errors 객체를 ResponseEntity.badRequest().body(errors)와 같이 처리하면 된다고 생각하기 쉽습니다. 하지만 이렇게 처리할 경우에는 errors를 직렬화 할 수 있는 serializer가 없기 때문에 런타임 에러를 내게 됩니다. 따라서 아래와 같이 Errors 객체를 직렬화하기 위한 serializer를 추가해야 합니다. (참고로 event 객체는 Bean 스펙을 준수하는 객체이기 때문에 이미 등록되어 있는BeanSerializer로 직렬화 가능)

 

package org.kyhslam.rest.common;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import org.springframework.boot.jackson.JsonComponent;
import org.springframework.validation.Errors;

import java.io.IOException;

@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 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();

    }
}

 

  • JsonSerializer 클래스를 상속받아 serialize 메서드를 오버라이딩합니다. errors 객체에 있는 필드 에러들을 순회하여 그 값들을 JsonGenerator 인스턴스에 담아 Json으로 직렬화를 하게 됩니다. 
  • @JsonComponent 어노테이션을 이용하여 Json Serializer로 손쉽게 등록할 수 있습니다.

 

 

컨트롤러

package org.kyhslam.rest;

import org.modelmapper.ModelMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.hateoas.MediaTypes;
import org.springframework.hateoas.mvc.ControllerLinkBuilder;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.validation.Errors;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;

import javax.validation.Valid;
import java.net.URI;

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

    @Autowired
    EventRepository eventRepository;

    @Autowired
    ModelMapper modelMapper;

    @Autowired
    EventValidator eventValidator;

    public EventController(EventRepository eventRepository, ModelMapper modelMapper, EventValidator eventValidator){
        this.eventRepository = eventRepository;
        this.modelMapper = modelMapper;
        this.eventValidator = eventValidator;
    }


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

        eventValidator.validate(eventDto, errors);
        if(errors.hasErrors()){
            return ResponseEntity.badRequest().body(errors);
        }

        Event event = modelMapper.map(eventDto, Event.class); // Dto를 event로 변환
        Event newEvent = eventRepository.save(event);
        URI createdUri = ControllerLinkBuilder.linkTo(EventController.class).slash(newEvent.getId()).toUri();
        return ResponseEntity.created(createdUri).body(event);
    }

}

errors에 있는 에러 정보들을 응답 메세지에 포함하기 위해서는 body 메서드에 errors 객체를 ResponseEntity.badRequest().body(errors)와 같이 처리하면 된다고 생각하기 쉽습니다. 하지만 이렇게 처리할 경우에는 errors를 직렬화 할 수 있는 serializer가 없기 때문에 런타임 에러를 내게 됩니다. 따라서 아래와 같이 Errors 객체를 직렬화하기 위한 serializer를 추가해야 합니다. (참고로 event 객체는 Bean 스펙을 준수하는 객체이기 때문에 이미 등록되어 있는BeanSerializer로 직렬화 가능)

출처: https://engkimbs.tistory.com/862?category=789178 [새로비]

테스트

package org.kyhslam.rest;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.kyhslam.rest.common.TestDescription;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.hateoas.MediaTypes;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;

import java.time.LocalDateTime;

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class EventControllerTests {

    @Autowired
    MockMvc mockMvc;

    @Autowired
    ObjectMapper objectMapper;

    //들어온 값이 이상할 경우
    @Test
    @TestDescription("입력값이잘못된 경우에 에러가 발생하는 테스트")
    public void createEvent_Bad_Request_Wrong_input() throws Exception {

        EventDto eventDto = EventDto.builder()
                .name("Spring")
                .description("REST API Development")
                .beginEnrollmentDateTime(LocalDateTime.of(2018,11,23,14,21))
                .closeEnrollmentDateTime(LocalDateTime.of(2018,11,23,14,21))
                .beginEventDateTime(LocalDateTime.of(2018,11,25,14,21))
                .endEventDateTime(LocalDateTime.of(2018,11,26,14,21))
                .basePrice(10000)
                .maxPrice(200)
                .limitOfEnrollment(100)
                .location("강남역 d2 팩토리")
                .build();

        this.mockMvc.perform(MockMvcRequestBuilders.post("/api/events")
                .contentType(MediaType.APPLICATION_JSON_UTF8)
                .content(this.objectMapper.writeValueAsString(eventDto)))
                .andDo(MockMvcResultHandlers.print())
                .andExpect(MockMvcResultMatchers.status().isBadRequest())
                .andExpect(MockMvcResultMatchers.jsonPath("$[0].objectName").exists())
                .andExpect(MockMvcResultMatchers.jsonPath("$[0].defaultMessage").exists())
                .andExpect(MockMvcResultMatchers.jsonPath("$[0].code").exists())
                ;

    }

}

 

응답결과

ockHttpServletResponse:
           Status = 400
    Error message = null
          Headers = {Content-Type=[application/hal+json;charset=UTF-8]}
     Content type = application/hal+json;charset=UTF-8
             Body = [{"field":"basePrice","objectName":"eventDto","code":"wrongValue","defaultMessage":"basePrice is wrong.","rejectedValue":"10000"},{"field":"maxPrice","objectName":"eventDto","code":"wrongValue","defaultMessage":"basePrice is wrong.","rejectedValue":"200"}]
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

응답의 body를 json으로 변환하면 아래와 같이 내용이 나온다. (http://json.parser.online.fr/)

[
  {
      "field":"basePrice",
      "objectName":"eventDto",
      "code":"wrongValue",
      "defaultMessage":"basePrice is wrong.",
      "rejectedValue":"10000"
  },
  {
      "field":"maxPrice",
      "objectName":"eventDto",
      "code":"wrongValue",
      "defaultMessage":"basePrice is wrong.",
      "rejectedValue":"200"
  }
]

댓글

Designed by JB FACTORY