반응형

[Spring REST API #6] Spring REST API Bad Request 처리

반응형

| Spring REST API Bad Request 처리


이번 시간은 지난 시간에 이어서 HTTP 요청에 대해 잘못된 입력값이 보내진 경우, 어떻게 처리를 할 것 인지에 대해 알아보겠습니다. 한 경우는 입력값이 없는 상태, 또 하나는 비즈니스 로직에 위배되는 값이 보내왔을 때입니다.


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


프로젝트 구조

+---src
| +---main
| | +---java
| | | \---com
| | | \---example
| | | \---springrestapi
| | | | SpringRestApiApplication.java
| | | |
| | | +---common
| | | | TestDescription.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>


설정 파일

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


테스트 코드

@Test
@TestDescription("입력값이 비어있을 때")
public void createEvent_Bad_Request_Empty_Input() throws Exception {
EventDto eventDto = EventDto.builder().build();

this.mockMvc.perform(post("/api/events")
.contentType(MediaType.APPLICATION_JSON_UTF8)
.content(this.objectMapper.writeValueAsString(eventDto)))
.andExpect(status().isBadRequest())
;
}

@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())
;
}
  • 첫 번째 테스트 코드는 입력값이 없을 때를 가정한 코드입니다. 두 번째 테스트 코드는 비즈니스 로직에 위배된 (등록시작날짜가 등록종료날짜보다 크던지 아니면 이벤트 시작날짜가 등록시작날짜보다 적은지 등) 경우를 테스트 하는 코드입니다.


소스 코드

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

@NotEmpty
private String name;
@NotEmpty
private String description;
@NotNull
private LocalDateTime beginEnrollmentDateTime;
@NotNull
private LocalDateTime closeEnrollmentDateTime;
@NotNull
private LocalDateTime beginEventDateTime;
@NotNull
private LocalDateTime endEventDateTime;
private String location; // (optional)
@Min(0)
private int basePrice; // (optional)
@Min(0)
private int maxPrice; // (optional)
@Min(0)
private int limitOfEnrollment;
}
  • 입력값이 없을 경우를 체크할 경우, @NotEmpty, @NotNull 어노테이션을 붙여서 이 프로퍼티가 값을 꼭 가져야하는 것을 명시합니다. 또한 basePrice, maxPrice 같은 정수값을 산정할 때 최소값 제한을 두어( @Min(0) ) 해당 프로퍼티에 잘못된 값이 들어오는 것을 방지할 수 있습니다.


@Component
public class EventValidator {

public void validate(EventDto eventDto, Errors errors) {
if(eventDto.getBasePrice() > eventDto.getMaxPrice() && eventDto.getMaxPrice() != 0) {
errors.rejectValue("basePrice", "wrongValue", "BasePrice is wrong.");
errors.rejectValue("maxPrice", "wrongValue", "MaxPrice is wrong.");
}

LocalDateTime endEventDateTime = eventDto.getEndEventDateTime();
if(endEventDateTime.isBefore(eventDto.getBeginEventDateTime()) ||
endEventDateTime.isBefore(eventDto.getCloseEnrollmentDateTime()) ||
endEventDateTime.isBefore(eventDto.getBeginEnrollmentDateTime())) {
errors.rejectValue("endEventDateTime", "wrongValue", "Wrong Date Time");
}
}
}
  • 요청값을 받는 EventDto의 프로퍼티들이 유효한 값을 가지고 있는 지 체크하는 EventValidator 클래스를 작성했습니다. 이 클래스의 인스턴스를 통해 비즈니스 로직에 위배되는 요청값을 걸러내고, 만약 잘못된 요청값이 있을 경우 BadRequest 로 클라이언트에 응답하는 로직을 구성할 것입니다.

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

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

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);
}
}
  • 컨트롤러에서의 메서드에 위와 같이 @Valid 어노테이션을 붙이면 스프링에서 자동적으로 EventDto에 들어갈 값들을 체크하여 만약 에러가 발생했을 경우 Errors 객체에 집어넣습니다. @Valid 어노테이션은 위에 EventDto 클래스의 프로퍼티에 붙인 @NotNull, @NotEmpty 등의 어노테이션 정보를 토대로 해당 객체에 대한 유효성 여부를 검사합니다.
  • eventValidator는 validate 메서드를 통해서 객체의 유효성 여부를 검사하며 만약 에러가 발생할 경우 Errors 객체에 집어넣습니다. 
  • errors에 에러에 대한 정보가 있는 지 여부는 hasErrors 메서드를 통해 판별할 수 있습니다. 만약 에러가 있을 경우 badRequest를 반환하도록 로직을 구성했습니다.


결과 화면






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

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

반응형

이 글을 공유하기

댓글

Designed by JB FACTORY