[Spring REST API #7] Spring REST API Bad Request 처리 및 에러 응답 메세지 보내기

2021. 3. 26. 02:42 Spring Data/Spring Data REST

 

Spring REST API Bad Request 처리 및 응답하기

 

이번 시간에는 서버에서 Bad Request 응답시, 응답 메세지에 Error 정보를 담아서 보내는 법을 알아보겠습니다.

 

모든 소스 코드는 여기에서 보실 수 있습니다.

 

프로젝트 구조

+---src
|   +---main
|   |   +---java
|   |   |   \---com
|   |   |       \---example
|   |   |           \---springrestapi
|   |   |               |   SpringRestApiApplication.java
|   |   |               |
|   |   |               +---common
|   |   |               |       TestDescription.java
|   |   |               |       ErrorsSerializer.java
|   |   |               |
|   |   |               \---events
|   |   |                       Event.java
|   |   |                       EventController.java
|   |   |                       EventDto.java
|   |   |                       EventRepository.java
|   |   |                       EventStatus.java
|   |   |                       EventValidator.java
|   |   |
|   |   \---resources
|   |       |   application.yml
|   |       |
|   |       +---static
|   |       \---templates
|   \---test
|       \---java
|           \---com
|               \---example
|                   \---springrestapi
|                       |   SpringRestApiApplicationTests.java
|                       |
|                       \---events
|                               EventControllerTests.java
|                               EventTest.java

 

의존성 관리

<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

 

테스트 코드

@Test
@TestDescription("잘못된 입력값이 입력됬을 때")
public void createEvent_Bad_Request_Wrong_Input() throws Exception {
    EventDto eventDto = EventDto.builder()
            .name("Spring")
            .description("REST API Development")
            .beginEnrollmentDateTime(LocalDateTime.of(2010, 11, 23, 14, 23))
            .closeEnrollmentDateTime(LocalDateTime.of(2018, 11, 21, 14, 23))
            .beginEventDateTime(LocalDateTime.of(2018, 12, 24, 14, 30))
            .endEventDateTime(LocalDateTime.of(2018, 12, 6, 14, 30))
            .basePrice(10000)
            .maxPrice(200)
            .limitOfEnrollment(100)
            .location("D Start up Factory")
            .build();

    this.mockMvc.perform(post("/api/events")
            .contentType(MediaType.APPLICATION_JSON_UTF8)
            .content(this.objectMapper.writeValueAsString(eventDto)))
            .andExpect(status().isBadRequest())
            .andExpect(jsonPath("$[0].objectName").exists())
            .andExpect(jsonPath("$[0].field").exists())
            .andExpect(jsonPath("$[0].defaultMessage").exists())
            .andExpect(jsonPath("$[0].rejectValue").exists())
    ;
}
  • 위 테스트 코드는 잘못된 객체를 POST 형식으로 보낼 때, 서버에서 Bad Request 응답 메세지를 보내며 또한 응답 메세지 안에 에러에 관련된 정보를 포함하는 것을 체크하는 코드입니다.

 

소스 코드

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

    @Autowired
    EventRepository eventRepository;

    @Autowired
    ModelMapper modelMapper;

    @Autowired
    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);
        Event newEvent = this.eventRepository.save(event);
        URI createdURI = linkTo(EventController.class).slash(newEvent.getId()).toUri();
        return ResponseEntity.created(createdURI).body(newEvent);
    }
}
  • errors에 있는 에러 정보들을 응답 메세지에 포함하기 위해서는 body 메서드에 errors 객체를 ResponseEntity.badRequest().body(errors)와 같이 처리하면 된다고 생각하기 쉽습니다. 하지만 이렇게 처리할 경우에는 errors를 직렬화 할 수 있는 serializer가 없기 때문에 런타임 에러를 내게 됩니다. 따라서 아래와 같이 Errors 객체를 직렬화하기 위한 serializer를 추가해야 합니다. (참고로 event 객체는 Bean 스펙을 준수하는 객체이기 때문에 이미 등록되어 있는BeanSerializer로 직렬화 가능)

 

package com.example.springrestapi.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 serializers) throws IOException {
        gen.writeStartArray();
        errors.getFieldErrors().stream().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로 손쉽게 등록할 수 있습니다.

 

결과 화면

 

요청

MockHttpServletRequest:
HTTP Method = POST
Request URI = /api/events
Parameters = {}
Headers = [Content-Type:"application/json;charset=UTF-8"]
Body = {"name":"Spring","description":"REST API Development","beginEnrollmentDateTime":"2010-11-23T14:23:00","closeEnrollmentDateTime":"2018-11-21T14:23:00","beginEventDateTime":"2018-12-24T14:30:00","endEventDateTime":"2018-12-06T14:30:00","location":"D Start up Factory","basePrice":10000,"maxPrice":200,"limitOfEnrollment":100}
Session Attrs = {}

 

응답

MockHttpServletResponse:
Status = 400
Error message = null
Headers = [Content-Type:"application/hal+json;charset=UTF-8"]
Content type = application/hal+json;charset=UTF-8
Body = [{"field":"endEventDateTime","objectName":"eventDto","code":"wrongValue","defaultMessage":"Wrong Date Time","rejectedValue":"2018-12-06T14:30"},{"objectName":"eventDto","code":"wrongPrices","defaultMessage":"wrong price error"}]
Forwarded URL = null
Redirected URL = null
Cookies = []

 

참조: https://www.inflearn.com/course/spring_rest-api/#

소스 코드 : https://github.com/engkimbs/spring-rest-api



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