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을 만드는게 생각보다 쉽지 않다.
-