[이벤트 생성 API 개발]_04_입력값 제한하기

3 minute read

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

입력값 제한

  • id 또는 입력 받은 데이터로 계산해야 하는 값들은 입력을 받지 않아야 한다.
  • EventDto 적용

DTO → 도메인 객체로 값 복사

  • ModelMapper
<dependency>
            <groupId>org.modelmapper</groupId>
            <artifactId>modelmapper</artifactId>
            <version>2.3.1</version>
</dependency>

통합 테스트로 전환

  • @WebMvcTest 빼고 다음 애노테이션 추가
    • @SpringBootTest
    • @AutoConfigureMockMvc
  • Repository @MockBean 코드 제거

테스트 할 것

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

입력값 제한 테스트

  • id 또는 입력 받은 데이터로 계산해야 하는 값들은 입력을 받지 않아야 한다.
  • 입력값으로 누가 id나 eventStatus, offline, free 이런 데이터까지 같이 주면?
    • Bad_Request로 응답 vs 받기로 한 값 이외는 무시
    @Test
    public void createEvent() throws Exception {
        Event event = Event.builder()
                .id(100) //셋팅되어서는 안되는 값
                //...
                .free(true) //계산되어야 하는 값
                .offline(false) //계산되어야 하는 값
                .build();

        mockMvc.perform(post("/api/events/") // 요청
                //...
        .andExpect(jsonPath("id").value(Matchers.not(100)))
        .andExpect(jsonPath("free").value(Matchers.not(true)));
    }
}

⇒ 입력값을 제한하는 여러가지 방법들이 있다.

  1. Jackson Json이 제공하는 여러가지 애노테이션 사용 @JsonIgnoreProperties등…

    단점 - 도메인 클래스에 애노테이션이 많아질 우려가 있다.

    Validation과 관련된 애노테이션도 추가해야한다. 그런 경우에 도메인 클래스에 롬복관련 애노테이션, JPA 관련 애노테이션, JSON, JSON Validation 등 너무 많아져서 헷갈릴 수 있기 때문에 어느정도 분산을 시켜서 입력값을 받는 DTO를 분리해낼 수 있다.

  2. 입력값을 받는 DTO를 분리

    장점 - 애노테이션 분리

    단점 - 중복이 발생

EventDto 적용

1. EventDto 생성

@Data @Builder @NoArgsConstructor @AllArgsConstructor
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;
}

2. 컨트롤러에서 EventDto로 받아오도록 수정

EventDto로 받아오기 때문에 입력값에 id, free, offline 값이 있더라도 무시하게 된다. (DTO필드에 없으므로)

@PostMapping()
public ResponseEntity createEvent(@RequestBody EventDto eventDto) {

3. DTO → 도메인 객체로 값 복사

  • 방법 1. ModelMapper 라이브러리 사용

    직접 EventDto의 값을 getter로 가져와서 Event를 생성하는 과정을 생략할 수 있다.

    ModelMapper를 사용하면 입력해서 옮기는 것보다 시간이 좀 더 걸리지만 Reflection도 자바 버전이 올라갈수록 성능이 좋아졌기 때문에 크게 우려할 정도는 아님

  • 방법 2. 직접 EventDto의 값을 getter로 가져와서 Event를 생성

    성능이 우려된다면 직접 적어주는 방법을 사용하자

    @PostMapping()
    public ResponseEntity createEvent(@RequestBody EventDto eventDto) {
      	// 직접 변환하는 코드
        Event event = Event.builder()
                .name(eventDto.getName())
                .description(eventDto.getDescription())
                .build();
        Event newEvent = this.eventRepository.save(event);
    
  1. 의존성 추가
<dependency>
    <groupId>org.modelmapper</groupId>
    <artifactId>modelmapper</artifactId>
    <version>2.3.9</version>
</dependency>
  1. 공용으로 사용할 수 있으므로 빈으로 등록
@SpringBootApplication
public class DemoApplication {

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

  // 빈 등록
    @Bean
    public ModelMapper modelMapper() {
        return new ModelMapper();
    }
}
  1. 등록한 ModelMapper빈을 컨트롤러에서 가져와서 사용
public class EventController {
    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);
      // ...
    }
}

테스트 결과 - 실패 : NPE 발생

Caused by: java.lang.NullPointerException
	at dev.solar.demoinflearnrestapi.events.EventController.createEvent(EventController.java:32)

newEvent가 없어서 NPE가 발생한다. why? 목킹하고 stubbing 해줬는데 ??

eventRepository.save()에 우리가 만든 event가 주어졌을 때, event가 리턴되도록 했는데,

save()시에 우리가 전달한 event가 받아진 것이 아니라 실제로 save()에 전달된 객체는 EventDto로 부터 새로 만든 Event이기 때문에 Stubbing되지 않은 것이다.

따라서 Stubbing시 조건이 맞지않기 때문에 (save되는 event 객체가 다름) 목킹되지 않는다.

newEvent는 기본적으로 목객체가 리턴하는 null이 되고, null에서 getId()를 요청했기때문에 NPE가 발생한다.

// stubbing
// evnetRepository의 save()가 호출될 때 할 행동을 정의
Mockito.when(eventRepository.save(event)).thenReturn(event);
@PostMapping()
public ResponseEntity createEvent(@RequestBody EventDto eventDto) {
  // eventDto -> Event
    Event event = modelmapper.map(eventDto, Event.class);
    Event newEvent = this.eventRepository.save(event);
  // ...
}

통합 테스트로 전환

목킹하지 않고, 실제 Repository에 저장해서 테스트하도록 수정

  • @WebMvcTest 빼고 다음 애노테이션 추가

    • @SpringBootTest

      webEnvironment()의 기본값이 MOCK으로 되어있어서 MockMvc를 사용할 수 있다.

      WebEnvironment webEnvironment() default WebEnvironment.MOCK;
      
    • @AutoConfigureMockMvc

  • Repository @MockBean 코드 제거

⇒ 실제 Repository를 사용해서 테스트가 동작한다.

테스트 성공

MockHttpServletResponse:
           Status = 201
    Error message = null
          Headers = [Location:"http://localhost/api/events/1", Content-Type:"application/hal+json;charset=UTF-8"]
     Content type = application/hal+json;charset=UTF-8
             Body = {"id":1,"name":"Spring","description":"REST API Development with Spring","beginEnrollmentDateTime":"2021-01-08T13:02:21","closeEnrollmentDateTime":"2021-01-09T13:02:21","beginEventDateTime":"2021-01-10T13:02:21","endEventDateTime":"2021-01-11T13:02:21","location":"강남역 D2 스타텁 팩토리","basePrice":100,"maxPrice":200,"limitOfEnrollment":100,"offline":false,"free":false,"eventStatus":null}
    Forwarded URL = null
   Redirected URL = http://localhost/api/events/1
          Cookies = []

웹 관련 테스트는 @SpringBootTest로 작성하는 것이 편하다.

컨트롤러 테스트등 테스트를 위해 목킹할 것이 많고, 그로인해 테스트코드를 짜기 힘들어진다.

또한 코드가 바뀔 때마다 테스트가 더 자주 깨져서 관리도 힘들다.

그 자체로 나쁜 것은 아니지만 테스트를 작성하는 것이 불편한 것 자체는 문제이다.

통합 테스트를 작성하면 모든 빈들이 등록된다. @SpringBootTest를 붙이면 @SpringBootApplication 애노테이션을 찾아서 여기서 부터 모든 빈들을 다 등록해준다.

애플리케이션을 실제 실행했을 때와 가장 근사한 형태로 테스트를 작성할 수 있다.

⇒ JPA 테스트를 만들 때 이렇게 통합테스트로 만드는 것을 선호