[Spring] put 요청 구현

노유성·2023년 7월 26일
0
post-thumbnail

들어가며

지금까지 spring에서 RESTful API를 구현하는 방법에 대해서 공부를 해보았기에 실제로 사용자가 수정 요청을 하면은 요청을 받아서 처리를 하는 로직을 구현해보겠다.

테스트 코드 작성

테스트 코드를 작성하기에 앞서서 기존에 존재하는 데이터를 만들어내는 제너레이터 함수를 만들었다.

private Event generateEvent(int i) {
    Event event = Event.builder()
            .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)
            .location("강남역")
            .free(false)
            .offline(true)
            .build();

    return this.eventRepository.save(event);
}

기존에는

private Event generateEvent(int i) {
    Event event = Event.builder()
            .name("Spring")
            .description("REST API Development with Spring")
            .build();
    return this.eventRepository.save(event);

이 정도로 간략하게 정보만을 넣어서 테스트 코드를 작성을 했었고 아무런 문제가 없었는데 로직을 전부 구현하고 나서 자꾸 400에러를 받아서 왜일까... 고심을 해보니 제너레이터 함수를 사용하면 유효성 검사를 거치지 않고 DB에 등록이 되니까 에러가 발생하지 않지만 test로 요청을 하면 유효성 검사를 하기 때문이었다.

정상 수정

@Test
@Description("이벤트 수정하기")
void updateEvent() throws Exception {
    // Given
    Event event = this.generateEvent(200);
    EventDto eventDto = this.modelMapper.map(event, EventDto.class);
    String eventName = "update name";
    eventDto.setName(eventName);

    // When
    ResultActions perform = this.mockMvc.perform(put("/api/events/{id}", event.getId())
            .contentType(MediaType.APPLICATION_JSON)
            .content(this.objectMapper.writeValueAsString(eventDto)));

    // Then
    perform.andDo(print())
            .andExpect(status().isOk())
            .andExpect(jsonPath("name").value(eventName));
}

제너레이터를 이용해서 event를 만든 후 modelmapper를 이용해 event를 eventDto에 덮어씌웠다. 이렇게 하면 event에만 있는 필드들은 날아가게 되고 eventDto로 정상적으로 요청을 보낼 수 있다. 이렇게 해야하는 이유는 우리가

spring.jackson.deserialization.fail-on-unknown-properties=true

이 설정을 해두었기 때문이다.

올바르지 못한 요청

여러가지 케이스가 있겠지만 하나의 케이스만 다루겠다.

@Test
@Description("존재하지 않는 이벤트 수정 실패")
void updateEvent404() throws Exception {
    // Given
    Event event = this.generateEvent(200);
    EventDto eventDto = this.modelMapper.map(event, EventDto.class);
    eventDto.setMaxPrice(10000);


    // When
    ResultActions perform = this.mockMvc.perform(put("/api/events/123123")
            .contentType(MediaType.APPLICATION_JSON)
            .content(this.objectMapper.writeValueAsString(eventDto)));

    // Then
    perform.andDo(print())
            .andExpect(status().isNotFound());
}

존재하지 않은 id로 요청을 보낸 경우에는 not found 응답을 받도록 테스트 케이슬들 작성했다.

로직

@PutMapping("/{id}")
public ResponseEntity modifyEvent(@PathVariable Integer id,  @RequestBody @Valid EventDto eventDto, Errors errors) {
    Optional<Event> optionalEvent = this.eventRepository.findById(id);
    if(optionalEvent.isEmpty()) {
        return ResponseEntity.notFound().build();
    }
    if(errors.hasErrors()) {
        return badRequest(errors);
    }
    eventValidator.validate(eventDto, errors);
    if(errors.hasErrors()) {
        return badRequest(errors);
    }
    Event newEvent = modelMapper.map(eventDto, Event.class);

    Event event = optionalEvent.get();
    event.setName(newEvent.getName());

    Event savedEvent = eventRepository.save(event);
    EventResource eventResource = new EventResource(savedEvent); // hateoas로 self-descriptive하게 event를 만들어서 내보냄
    eventResource.add(Link.of("/docs/index.html#resources-events-update").withRel("update"));

    return ResponseEntity.ok(eventResource);
}

@PutMapping annotation을 이용해서 id로 put요청을 하는 경우를 다루고 있다. 총 4가지 유효성 검사를 하는데 하나는 위에서 언급했던 알려지지 않은 속성이 있는 겨웅, 하나는 entity의 필드 조건을 만족하지 않는 경우, 하나는 없는 데이터를 수정하려고 하는 경우, 마지막으로 비즈니스 로직을 어긋나는 경우이다. 글을 쓰면서 느끼는 건데 공통적으로 적용되는 유효성 검사를 정규화해놓고 가져다가 쓰니까 참 편리하다. 또 그 부분들이 annotation이나 properties로 구현이 되어있으니 아는 만큼 멋있게 코드를 작성할 수 있다는 생각을 했다.

그래서 유효성 검사를 마치고 나서는 새롭게 받은 데이터에서 name필드를 갈아끼운 다음 DB에 저장하고 저장한 데이터를 client에게 응답으로 보내준다. 여기서 나는 savedEvent만을 body에 담아서 보냈는데 그렇게 하면은 self-descriptive 하지 않고 hateoas를 만족하지 않는다.

public class EventResource extends EntityModel<Event> {
    public EventResource(Event event) {
        super(event);
        add(linkTo(EventController.class).slash(event.getId()).withSelfRel());
    }
}

self-descriptive를 만족하기 위해 위처럼 hateoas 패키지를 이용해서 데이터를 매핑하고 관련 내용에 대한 문서를 add()로 걸어주면 된다. 여기서 더해서 test 코드에서 문서화 작업까지 해주면 완벽하다.

profile
풀스택개발자가되고싶습니다:)

0개의 댓글