반응형

[Spring REST API #5] Spring REST API 입력값 제한하기 및 에러 발생 처리

반응형

Spring REST API 입력값 제한하기 및 Bad Request 처리


REST API를 설계할 시 Client 측에서 잘못된 데이터를 요청하거나 전송할 시 그것을 처리하는 로직을 만들어야 합니다. 여기서는 DTO(Data Transfer Object)와 스프링 부트에서 제공하는 설정 정보를 이용해 손쉽게 해당 로직을 적용할 것입니다.


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


프로젝트 구조

+---src
| +---main
| | +---java
| | | \---com
| | | \---example
| | | \---springrestapi
| | | | SpringRestApiApplication.java
| | | |
| | | \---events
| | | Event.java
| | | EventController.java
| | | EventDto.java
| | | EventRepository.java
| | | EventStatus.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
  • deserialization.fail-on-unkown-properties 옵션은 HTTP Body의 데이터를 자바 객체로 역직렬화할 때 객체의 프로퍼티에 매핑되는 값이 없다면 직렬화 작업을 fail시키는 옵션입니다. 이것을 통해 이상한 요청이 왔을 때 Bad Request를 반환합니다.


테스트 코드

@Test
public void createEvent_Bad_Request() throws Exception {
Event event = Event.builder()
.name("Spring")
.description("REST API Development")
.beginEnrollmentDateTime(LocalDateTime.of(2010, 11, 23, 14, 23))
.closeEnrollmentDateTime(LocalDateTime.of(2018, 11, 30, 14, 23))
.beginEventDateTime(LocalDateTime.of(2018, 12, 5, 14, 30))
.endEventDateTime(LocalDateTime.of(2018, 12, 6, 14, 30))
.basePrice(100)
.maxPrice(200)
.limitOfEnrollment(100)
.location("D Start up Factory")
.free(true)
.offline(false)
.eventStatus(EventStatus.PUBLISHED)
.build();

mockMvc.perform(post("/api/events/")
.contentType(MediaType.APPLICATION_JSON_UTF8)
.accept(MediaTypes.HAL_JSON_UTF8)
.content(objectMapper.writeValueAsString(event)))
.andDo(print())
.andExpect(status().isBadRequest())
;
}
  • 잘못된 요청이 왔을 때를 테스트 하기 위해 작성된 코드입니다. 

소스 코드

package com.example.springrestapi.events;

import lombok.*;

import javax.persistence.*;
import java.time.LocalDateTime;


/**
* In case of referencing between entities, using default @EqualsAndHashCode stack overflow.
* Therefore, redefine the way of checking equality is the best practice as below.
* Also, you shouldn't use @Data annotation because it uses default @EqualsAndHashCode
*/
@Entity
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
@EqualsAndHashCode(of = "id")
public class Event {

@Id @GeneratedValue
private Integer id;
private String name;
private String description;
private LocalDateTime beginEnrollmentDateTime;
private LocalDateTime closeEnrollmentDateTime;
private LocalDateTime beginEventDateTime;
private LocalDateTime endEventDateTime;
private String location; // (optional)
private int basePrice; // (optional)
private int maxPrice; // (optional)
private int limitOfEnrollment;
private boolean offline;
private boolean free;
@Enumerated(EnumType.STRING)
private EventStatus eventStatus = EventStatus.DRAFT;

}
package com.example.springrestapi.events;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
public class EventDto {

private String name;
private String description;
private LocalDateTime beginEnrollmentDateTime;
private LocalDateTime closeEnrollmentDateTime;
private LocalDateTime beginEventDateTime;
private LocalDateTime endEventDateTime;
private String location; // (optional)
private int basePrice; // (optional)
private int maxPrice; // (optional)
private int limitOfEnrollment;
}
  • HTTP Body에 있는 데이터를 역직렬화 시, 해당 데이터를 EventDto 클래스의 프로퍼티에 매핑시킵니다. 
  • 위 설정 파일에 있는 fail-on-unkown-properties = false 옵션이 없다면 만약 HTTP Body에 있는 데이터는 클래스에 존재하는 프로퍼티에 대한 정보만 매핑시키고 나머지 다른 요청값들은 무시할 것입니다.


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

@Autowired
EventRepository eventRepository;

@Autowired
ModelMapper modelMapper;

@PostMapping
public ResponseEntity createEvent(@RequestBody EventDto eventDto) {
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);
}
}
  • ModelMapper 객체를 통해서 자동적으로 eventDto 인스턴스를 Event 클래스의 인스턴스에 매핑시킬 수 있습니다. (Event event = modelMapper.map(eventDto, Event.class))
  • ModelMapper 객체를 빈(bean)으로 등록하기 위해서는 아래와 같이 @Bean 어노테이션을 붙인 코드를 작성해야합니다.


import org.modelmapper.ModelMapper;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class SpringRestApiApplication {

public static void main(String[] args) {
SpringApplication.run(SpringRestApiApplication.class, args);
}

@Bean
public ModelMapper modelMapper() {
return new ModelMapper();
}

}


결과 화면


요청

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


응답

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


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

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




반응형

이 글을 공유하기

댓글

Designed by JB FACTORY