2020-8-12 스프링 Rest(2) 예시

Event Rest

이벤트 등록, 조회 API

GET /api/events

이벤트 목록조회 REST API(로그인 안 한상태)

  • 응답데이터
    • 이벤트목록
    • 링크
      • self
      • profile : 이벤트 목록 조회 API 문서로 이동하는 링크
      • get-an-event: 이벤트 하나를 조회하는 API 링크
      • Next: 다음 페이지(optional)
      • prev : 이전페이지(optional)
  • 문서 : spring rest doc활용

이벤트 목록 조회(로그인 한 상태) => Stateless를 유지하기 위해 Bearer 헤더에 유효한 Access Token이 들어있는 경우.

  • 응답데이터
    • 이벤트목록
    • 링크
      • self
      • profile : 이벤트 목록 조회 API 문서로 이동하는 링크
      • get-an-event: 이벤트 하나를 조회하는 API 링크
      • Create-new-event : 이벤트를 생성할 수 있는 API링크
      • Next: 다음 페이지(optional)
      • prev : 이전페이지(optional)

POST /api/events

  • 이벤트 생성

GET /api/events/{id}

  • 이벤트 하나 조회

PUT /api/events/{id}

  • 이벤트 수정

Event생성 API

  • 빌더를 쓰게 되면 파라미터에 값을 명확하게 알 수 있다. Default생성자가 생성이 안된다. 따라서 public이 아니다. 자바빈 규약에 어긋난다.

    • @Builder @NoArgsConstructor @AllArgsConstructor
      @Getter @Setter @EqualsAndHashCode(of = "id")
      public class Event {
          private Integer id;
          private String name;
          private String description;
          private LocalDateTime beginEnrollmentDateTime;
          private LocalDateTime closeEnrollmentDateTime;
          private LocalDateTime beginEventDateTime;
          private LocalDateTime closeEventDateTime;
          private String location;
          private int basePrice;
          private int maxPrice;
          private int limitOfEntrollment;
          private boolean offLline;
          private boolean free;
          private EventStatus eventStatus;
      }
      
      public class EventTest {
        @Test
        public void builderTest() {
          Event event = Event.builder()
            .name("rest api")
            .description("hello builder")
            .build();
          assertThat(event).isNotNull();
        }
      
        @Test
        public void javaBean() {
          // Given
          String name = "Event name";
          String description = "event description";
      
          // When
          Event event = new Event();
          event.setName(name);
          event.setDescription(description);
      
          // Then
          assertThat(event.getName()).isEqualTo(name);
          assertThat(event.getDescription()).isEqualTo(description);
        }
      }
      
    • @EqualsAndHashCode(of=”id”)를 사용한 이유 :

      • 모든 필드를 이용해서 해쉬코드를 만들 수 있다. 엔티티 안에서 연관관계가 있을때, stackoverflow가 발생할 수 있다.
      • 몇가지 필드를 더 추가할 수 있다. 다른 엔티티와 연관관계에 있는 객체들은 넣으면 안좋다.
    • 롬복은 메타어노테이션으로 잘 동작을 안하기 때문에 커스텀 어노테이션으로 합치지를 못한다. @Data도 @EqualsAndHashCode도 지원하지만 모든 객체를 사용하기때문에 이전에 설명한 이유로 안쓰는게 좋다.

    • @WebMvcTest : MockMvc빈을 자동설정해준다. 웹 관련 빈만 등록한다.(“슬라이싱”이라고도 한다)

      • MockMvc : 스프링 MVC테스트 핵심 클래스, 웹서버를 띄우지 않고 스프링MVC(DispatcherServlet) 요청을 처리하는 과정을 확인할 수 있다. 주로 컨트롤러 테스트용으로 자주 활용

201 응답 받기

  • @RestController : @ResponseBody를 모든 메서드에 적용한 것과 동일

  • ResponseEntity를 사용한 이유 : 응답 코드, 헤더, 본문 모두 다루기 편한 API

  • Location URI 만들기 : HATEOAS가 제공하는 linkTo(), methodOn()사용

  • 객체를 JSON으로 변환 : ObjectMapper사용

    • @Controller
      @RequestMapping(value = "/api/events", produces = MediaTypes.HAL_JSON_VALUE)
      public class EventController {
      
          @Autowired
          private EventRepository eventRepository;
      
          @PostMapping
          public ResponseEntity createEvent(@RequestBody Event event) {
              Event newEvent = this.eventRepository.save(event);
              URI createdUri = linkTo(EventController.class).slash(newEvent.getId()).toUri();
              event.setId(10);
              return ResponseEntity.created(createdUri).body(event);
          }
      }
      
      public interface EventRepository extends JpaRepository<Event, Integer> {
      }
      
      
      
      @RunWith(SpringRunner.class)
      @WebMvcTest
      public class EventControllerTest {
          @Autowired
          private MockMvc mockMvc;
      
          @MockBean
          private EventRepository eventRepository;
      
          @Autowired
          private ObjectMapper objectMapper;
      
          @Test
          public void createEvent() throws Exception {
              Event event = Event.builder()
                      .name("spring")
                      .description("hello world")
                      .beginEnrollmentDateTime(LocalDateTime.of(2020, 1, 1, 11, 11))
                      .closeEnrollmentDateTime(LocalDateTime.of(2020, 1, 1, 11, 11))
                      .beginEventDateTime(LocalDateTime.of(2020, 1, 1, 11, 11))
                      .closeEventDateTime(LocalDateTime.of(2020, 1, 1, 11, 11))
                      .basePrice(100)
                      .maxPrice(200)
                      .limitOfEnrollment(100)
                      .location("강남")
                      .build();
              event.setId(10);
            	// Mockito에서 기대값 설정
              Mockito.when(eventRepository.save(event)).thenReturn(event);
             mockMvc.perform(
                      post("/api/events/")
                  .contentType(MediaType.APPLICATION_JSON)
                      .accept(MediaTypes.HAL_JSON_VALUE)
                      .content(objectMapper.writeValueAsString(event))
              )
                      .andDo(print())
                      .andExpect(status().isCreated())
                      .andExpect(jsonPath("id").exists())
                      .andExpect(header().exists(HttpHeaders.LOCATION))
                      .andExpect(header().stringValues(HttpHeaders.CONTENT_TYPE, "application/hal+json"));
          }
      }
      
    • @WebMvcTest가 슬라이싱 테스트이기때문에 웹과 관련된 빈들만 등록한다. 따라서 EventRepository를 주입받아야한다.

    • @MockBean을 이용해서 주입받고. Mockito를 이용해서, eventRepository.save(event)에 대한 기대값을 설정해야한다.

입력값 제한

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

  • EventDto적용

  • Dto->도메인 객체로 값 복사

    • Domain객체에 너무 많은 어노테이션이 분산될 수 있어서. Dto에 데이터 처리를 하고, validation과 관련된 로직들을 추가하는게 좋다.

    • 단점은 중복이 생긴다.

    • Dto=> 도메인으로 바꾸기 위해 ModelMapper를 쓴다.

    • @PostMapping
      public ResponseEntity createEvent(@RequestBody EventDto eventDto) {
        // ModelMapper를 안쓸때.
        Event event = Event.builder()
          .name(eventDto.getName())
          .maxPrice(eventDto.getMaxPrice())
          .build();
        ...
          // ModelMapper를 쓸때
          Event event = modelMapper.map(eventDto, Event.class);
        	Event newEvent = this.eventRepository.save(event);
          return ResponseEntity.created(createdUri).body(event);
      }
      
      @Test
      public void createEvent() throws Exception {
        Event event = Event.builder()
          .id(100)
          .name("spring")
          .description("hello world")
          .beginEnrollmentDateTime(LocalDateTime.of(2020, 1, 1, 11, 11))
          .closeEnrollmentDateTime(LocalDateTime.of(2020, 1, 1, 11, 11))
          .beginEventDateTime(LocalDateTime.of(2020, 1, 1, 11, 11))
          .closeEventDateTime(LocalDateTime.of(2020, 1, 1, 11, 11))
          .basePrice(100)
          .maxPrice(200)
          .limitOfEnrollment(100)
          .location("강남")
          .free(true)
          .offLine(false)
          .eventStatus(EventStatus.PUBLISHED)
          .build();
      
        mockMvc.perform(
          post("/api/events/")
          .contentType(MediaType.APPLICATION_JSON)
          .accept(MediaTypes.HAL_JSON_VALUE)
          .content(objectMapper.writeValueAsString(event))
        )
          .andDo(print())
          .andExpect(status().isCreated())
          .andExpect(jsonPath("id").value(Matchers.not(100)))
          .andExpect(jsonPath("free").value(Matchers.not(true)))
          .andExpect(jsonPath("eventStatus").value("DRAFT"))
          .andExpect(header().exists(HttpHeaders.LOCATION))
          .andExpect(header().stringValues(HttpHeaders.CONTENT_TYPE, "application/hal+json"));
      }
      
    • 직접해야되는걸 ModelMapper를 이용해서 변환한다. 리플렉션을 이용하기 때문에 직접 변환하는거에 비해서는 느리다. 성능적인 부분은 잘 모르겠다. ModelMapper를 공통으로 쓰기 위해 빈으로 생성

    • Mockito.perform에서 전달한 event와 실제 event가 다르기때문에 @SpringBootTest를 이용해서 실제 Repository를 써야된다.

    • 테스트코드에서 Event를 전달하더라도 실제 요청에서는 Dto를 통해, 제한된 값들만 전달이 된다. Dto에 정의되지 않은 필드들은 받지 못하고 ModelMapper를 통해 Dto to Domain을 변환하더라도 Dto에 있는 값들만 전달된다.

입력값 이외 예외

  • 입력값 이외의 값들이 들어오면 에러발생 및 예외처리 vs 그냥 무시

  • 예외발생

    • spring.jackson.deserialization.fail-on-unknown-properties=true
      
    • string to object를 deserialization이라고 한다. unknow-property에 대해서 fail하게 설정을 한다.

    • 스프링MVC는 에러가 나면 기본적으로 Bad Request를 처리하게 한다.

    느낀점

    • restapi의 형태에 맞춰서 api를 설계하려고 하다보니 복잡하다. 특히 강좌에서 들었던 메서드들이 deprecated된 부분들이 있어서 HATEOAS에서 공식문서가서 찾아서 권장하는 메서드로 변경을 했다. 아직까지는 뭐가 좋은지 잘 모르겠다. 우선은 prev, next, self이런 url을 만드는게 생각보다 쉽지 않다.
Written on August 12, 2020