개발강의정리/Spring

[스프링 기반 REST API 개발] 2-6. Event 생성 API 구현: Bad Request 처리하기

nineDeveloper 2019. 12. 28.
728x90

스프링 기반 REST API 개발

2. 이벤트 생성 API 개발

포스팅 참조 정보

GitHub

공부한 내용은 GitHub에 공부용 Organizations에 정리 하고 있습니다

해당 포스팅에 대한 내용의 GitHub 주소

실습 내용이나 자세한 소스코드는 GitHub에 있습니다
포스팅 내용은 간략하게 추린 핵심 내용만 포스팅되어 있습니다

https://github.com/freespringlecture/spring-rest-api-study/tree/chap02-06_bad_request_handle

해당 포스팅 참고 인프런 강의

https://www.inflearn.com/course/spring_rest-api/dashboard

실습 환경

  • Java Version: Java 11
  • SpringBoot Version: 2.1.2.RELEASE

6. Event 생성 API 구현: Bad Request 처리하기

입력값이 이상한 경우에 Bad Request를 보내는 방법

@Valid와 BindingResult (또는 Errors)

스프링 MVC에 해당하는 내용 JS303 애노테이션을 사용해 확인할 수 있음

  • @Valid 라는 애노테이션을 붙이면 Entity에 바인딩을 할때 애노테이션들에 대한 정보를 참고해서 검증을 수행함

    • 검증을 수행한 결과를 객체 오른쪽에 있는 Errors 객체에 에러값들을 넣어줌
    • 받은 에러를 확인 해서 Bad Request를 발생시킴
  • BindingResult는 항상 @Valid 바로 다음 인자로 사용해야 함 (스프링 MVC)

  • @NotNull, @NotEmpty, @Min, @Max, ... 사용해서 입력값 바인딩할 때 에러 확인할 수 있음

도메인 Validator 만들기

Validator​ 인터페이스 없이 만들어도 상관없음

Junit5로 테스트하면 테스트 설명이 나옴

테스트 설명 용 애노테이션 만들기

@Target, @Retention

common 패키지를 생성하고 TestDescription 설명 용 어노테이션 생성

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface TestDescription {

    String value();
}

테스트 할 것

  • 입력 데이터가 이상한 경우 Bad_Request로 응답
    • 입력값이 이상한 경우 에러
    • 비즈니스 로직으로 검사할 수 있는 에러
    • 에러 응답 메시지에 에러에 대한 정보가 있어야 한다

Bad Request 처리 로직 작성

1. EventDto에 validation 어노테이션 추가

package me.freelife.rest.events;

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

import javax.validation.constraints.Min;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.time.LocalDateTime;

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

    @NotEmpty
    private String name; //이벤트 네임
    @NotEmpty
    private String description; // 설명
    @NotNull
    private LocalDateTime beginEnrollmentDateTime; //등록 시작일시
    @NotNull
    private LocalDateTime closeEnrollmentDateTime; //종료일시    private LocalDateTime beginEventDateTime; //이벤트 시작일시
    @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; //등록한도

}

2. EventValidator 추가


package me.freelife.rest.events;

import org.springframework.stereotype.Component;
import org.springframework.validation.Errors;

import java.time.LocalDateTime;

@Component
public class EventValidator {
    public void validate(EventDto eventDto, Errors errors) {
        String wrongValue = "wrongValue";
        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, "endEventDateTime is wrong");
        }

        // TODO beginEventDateTime
        // TODO CloseEnrollmentDateTime
    }
}

3. Event 생성 API에 validation 처리 로직 추가

package me.freelife.rest.events;

import org.modelmapper.ModelMapper;
import org.springframework.hateoas.MediaTypes;
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;

import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo;

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

    private final EventRepository eventRepository;

    private final ModelMapper modelMapper;

    private final EventValidator eventValidator;

    public EventController(EventRepository eventRepository, ModelMapper modelMapper, EventValidator eventValidator) {

        ...

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

        ...

    }
}

4. Event 생성 API 테스트 코드에 @TestDescription 적용 및 Bad Request 응답 테스트 코드 추가


...

import me.freelife.rest.common.TestDescription;

...

public class EventControllerTests {

    @Autowired
    MockMvc mockMvc;

    @Autowired
    ObjectMapper objectMapper;

    @Test
    @TestDescription("정상적으로 이벤트를 생성하는 테스트")
    public void createEvent() throws Exception {
        ...
    }

    @Test
    @TestDescription("입력 받을 수 없는 값을 사용한 경우에 에러가 발생하는 테스트")
    public void createEvent_Bad_Request() throws Exception {
        ...
    }

    @Test
    @TestDescription("입력 값이 비어있는 경우에 에러가 발생하는 테스트")
    public void createEvent_Bad_Request_Empty_Input() throws Exception {
        ...
    }

    @Test
    @TestDescription("입력 값이 잘못된 경우에 에러가 발생하는 테스트")
    public void createEvent_Bad_Request_Wrong_Input() throws Exception {
        EventDto eventDto = EventDto.builder()
                .name("Spring")
                .description("REST API Development with Spring")
                .beginEnrollmentDateTime(LocalDateTime.of(2018, 11, 26, 14, 21))
                .closeEnrollmentDateTime(LocalDateTime.of(2018, 11, 25, 14, 21))
                .beginEventDateTime(LocalDateTime.of(2018, 11, 24, 14, 21))
                .endEventDateTime(LocalDateTime.of(2018, 11, 23, 14, 21))
                .basePrice(10000)
                .maxPrice(200)
                .limitOfEnrollment(100)
                .location("강남역 D2 스타텁 팩토리")
                .build();

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

}
728x90

댓글

💲 추천 글