개발강의정리/Spring

[스프링 기반 REST API 개발] 2-4. Event 생성 API 구현: 입력값 제한하기

nineDeveloper 2019. 12. 28.
728x90

스프링 기반 REST API 개발

2. 이벤트 생성 API 개발

포스팅 참조 정보

GitHub

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

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

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

https://github.com/freespringlecture/spring-rest-api-study/tree/chap02-04_input_restrict

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

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

실습 환경

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

4. Event 생성 API 구현: 입력값 제한하기

입력값 제한

입력하기로한 값들 이외에는 무시하는 방법

  • id 또는 입력받은 데이터로 계산 해야 하는 값들은 입력을 받지 않아야 한다
  • EventDto 적용
    • 너무 많은 애노테이션으로 코드가 복잡하고 지저분해지므로 분리해서 작업
    • 입력받는 DTO 별도로 복사해서 처리
    • 받아올 객체가 EventDto이기 떄문에 id가 있던 free가 있던 무시

DTO -> 도메인 객체로 값 복사

기존 클래스를 DTO로 손쉽게 변환해주는 라이브러리

ModelMapper

<dependency>
    <groupId>org.modelmapper</groupId>
    <artifactId>modelmapper</artifactId>
    <version>2.3.2</version>
</dependency>

ModelMapper 빈 등록

입력값은 EventDto로 받았지만 계산되어야 되는 필드들은 없으므로 걸러서 받고 걸러진 값들을 대상으로 이벤트 객체를 생성해서 eventRepository에 저장을 함

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

통합 테스트로 전환

Mocking 테스트를 하려고 만든 객체와 새롭게 EventDto로 생성한 객체가 달라서 null을 리턴해줘서 NullPointException이 일어남

  • @WebMvcTest 빼고 다음 애노테이션 추가
    • @SpringBootTest
      • 테스트할때는 @SpringBootTest로 테스트하는게 편함 Mocking 해줘야 될게 너무많아서 관리가 힘듬
      • 애플리케이션을 실행했을때와 가장 근사한 테스트를 만들어 작성할 수 있음
    • @AutoConfigureMockMvc
      • MockMvc를 계속 사용하기 위해 적용
  • Repository @MockBean 코드 제거

테스트 할 것

  • 입력값으로 누가 ideventStatus, offline, free 이런 데이터까지 같이 주면?
    • Bad_Request로 응답 vs받기로 한 값 이외는 무시

입력값 제한하기 로직 작성

1. EvnetDto 추가

package me.freelife.rest.events;

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

import java.time.LocalDateTime;

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

    private String name; //이벤트 네임
    private String description; // 설명
    private LocalDateTime beginEnrollmentDateTime; //등록 시작일시
    private LocalDateTime closeEnrollmentDateTime; //종료일시    private LocalDateTime beginEventDateTime; //이벤트 시작일시
    private LocalDateTime beginEventDateTime; //이벤트 시작일시
    private LocalDateTime endEventDateTime;   //이벤트 종료일시
    private String location; // (optional) 이벤트 위치 이게 없으면 온라인 모임
    private int basePrice; // (optional) 기본 금액
    private int maxPrice; // (optional) 최고 금액
    private int limitOfEnrollment; //등록한도

}

2. Event 도메인에 eventStatus 기본값 DRAFT로 지정


...

public class Event {

    ...

    @Enumerated(EnumType.STRING)
    private EventStatus eventStatus = EventStatus.DRAFT; // 이벤트 상태
}

3. EventController에 modelMapper 사용


...

public class EventController {

    private final EventRepository eventRepository;

    private final ModelMapper modelMapper;

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

    @PostMapping
    public ResponseEntity createEvent(@RequestBody EventDto eventDto) {
        //EventDto에 있는 것을 Event 타입의 인스턴스로 만들어 달라
        Event event = modelMapper.map(eventDto, Event.class);
        Event newEvent = this.eventRepository.save(event);
        //EventController의 id에 해당하는 링크를 만들고 링크를 URI로 변환
        //API에 events에 어떤 특정한 ID 그 ID가 생성된 이벤트에 Location Header에 들어감
        URI createdUri = linkTo(EventController.class).slash(newEvent.getId()).toUri();
        event.setId(10);
        // createdUri 헤더를 가지고 201응답을 만듬
        return ResponseEntity.created(createdUri).body(event);
    }
}

4. Event 생성 API 테스트 코드 수정

  • 테스트를 @WebMvcTest에서 @SpringBootTest로 변경하고 @AutoConfigureMockMvc 추가
  • MockMvc 의존성 주입
  • id, free, offLine, eventStatus 테스트 데이터 추가 및 검증 로직 추가

package me.freelife.rest.events;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.hamcrest.Matchers;
import org.junit.Test;
import org.junit.runner.RunWith;
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 java.time.LocalDateTime;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

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

    @Autowired
    MockMvc mockMvc;

    @Autowired
    ObjectMapper objectMapper;

    @Test
    public void createEvent() throws Exception {
        Event event = Event.builder()
                .id(100)
                .name("Spring")
                .description("REST API Development with Spring")
                .beginEnrollmentDateTime(LocalDateTime.of(2018, 11, 23, 14, 21))
                .closeEnrollmentDateTime(LocalDateTime.of(2018, 11, 24, 14, 21))
                .beginEventDateTime(LocalDateTime.of(2018, 11, 25, 14, 21))
                .endEventDateTime(LocalDateTime.of(2018, 11, 26, 14, 21))
                .basePrice(100)
                .maxPrice(200)
                .limitOfEnrollment(100)
                .location("강남역 D2 스타텁 팩토리")
                .free(true)
                .offline(false)
                .eventStatus(EventStatus.PUBLISHED)
                .build();

        mockMvc.perform(post("/api/events/")
                    .contentType(MediaType.APPLICATION_JSON_UTF8) //요청타입
                    .accept(MediaTypes.HAL_JSON) //받고싶은 타입
                    .content(objectMapper.writeValueAsString(event))) //event를 json을 String으로 맵핑
                .andDo(print())
                .andExpect(status().isCreated()) // 201 상태인지 확인
                .andExpect(jsonPath("id").exists()) //ID가 있는지 확인
                .andExpect(header().exists(HttpHeaders.LOCATION)) // HEADER에 Location 있는지 확인
                .andExpect(header().string(HttpHeaders.CONTENT_TYPE, MediaTypes.HAL_JSON_UTF8_VALUE)) //Content-Type 값 확인
                .andExpect(jsonPath("id").value(Matchers.not(100))) // ID가 100이 아니면
                .andExpect(jsonPath("free").value(Matchers.not(true))) // free가 true가
                .andExpect(jsonPath("eventStatus").value(EventStatus.DRAFT.name()))

        ;
    }
}
728x90

댓글

💲 추천 글