[사이드프로젝트] 그저 그런 REST API로 괜찮은가? - 진정한 REST API 구현해보기 - Spring HATEOAS 적용

Spring HATEOAS


HATEOAS는 REST 아키텍쳐, Uniform Interface 조건 중 하나로 어플리케이션의 상태를 전이시킬 수 있는 링크, 기능을 제공해야한다.

Spring HATEOAS는 HATEOAS를 만족시키기 위해 만들어진 라이브러리로 크게 링크와 리소스를 만드는 기능, 링크를 찾아주는 기능을 제공한다.

여기서 Resource는 Client가 요구하는 데이터(예를 들어 게시판 글, 이벤트 등)를 포함하고 있으며 Links는 어플리케이션의 다른 상태나 리소스에 접근할 수 있는 링크를 제공한다.

링크에는 크게 2가지 종류가 있으며 HREF, REL가 있다. HREF는 하이퍼링크란 뜻으로 어플리케이션 상태를 전이시킬 수 있는 링크 종류이며 REL은 Resource와 관련된 정보와 기능을 제공하는 링크다.

포함되어야할 Link


EventAPI에서 제공하는 Link에는 다음과 같다.

  • profile : API 기술 문서를 나타내는 링크다.
    - 지금 API 기술 문서가 없기 때문에 현재는 구현하지 않는다.
  • self : 현재 리소스의 출처를 나타내는 링크다.
  • query-events : 리소스(이벤트)를 검색할 수 있는 링크다.
  • update-event : 리소스(이벤트)를 업데이트 할 수 있는 링크다.

EventController Test 수정


    @Test
    public void create_event_success() throws Exception {
        //Given
        EventDto eventDto = createEventDto();

        //When
        //Then
        mockMvc.perform(post("/api/events")
                        .contentType(MediaType.APPLICATION_JSON)
                        .accept(MediaTypes.HAL_JSON)
                        .content(objectMapper.writeValueAsString(eventDto)))
                .andDo(print())
                .andExpect(status().isCreated())
                .andExpect(jsonPath("event").exists())
                .andExpect(header().exists(HttpHeaders.LOCATION))
                .andExpect(header().string(HttpHeaders.CONTENT_TYPE,MediaTypes.HAL_JSON_VALUE))
                //_link 추가
                .andExpect(jsonPath("_links.self").exists())
                .andExpect(jsonPath("_links.query-events").exists())
                .andExpect(jsonPath("_links.update-event").exists());
    }

EventResourcs 구현


Spring HATEOAS에서 제공하는 RepresentationModel을 상속받은 EventResource 구현

  • RepresentationModel 내부의 add 함수를 통해 link를 추가할 수 있음
package com.carrykim.restapi.event.model.dto;

import com.carrykim.restapi.event.model.Event;
import org.springframework.hateoas.RepresentationModel;

public class EventResource extends RepresentationModel{

    private Event event;

    public EventResource(Event event){
        this.event = event;
    }

    public Event getEvent() {
        return event;
    }
}

EventService 수정


Service layer에서 EventResource를 반환하도록 합니다.

package com.carrykim.restapi.event.service;

import com.carrykim.restapi.event.infra.EventRepository;
import com.carrykim.restapi.event.model.Event;
import com.carrykim.restapi.event.model.dto.EventDto;
import com.carrykim.restapi.event.model.dto.EventResource;
import org.springframework.stereotype.Service;

@Service
public class EventService {

    private final EventRepository eventRepository;

    public EventService(EventRepository eventRepository) {
        this.eventRepository = eventRepository;
    }

    public EventResource create(EventDto eventDto){
        Event newEvent = eventDto.toModel();
        return new EventResource(eventRepository.save(newEvent));
    }

}

EventController 수정


package com.carrykim.restapi.event.controller;

import com.carrykim.restapi.event.model.Event;
import com.carrykim.restapi.event.model.dto.EventDto;
import com.carrykim.restapi.event.model.dto.EventResource;
import com.carrykim.restapi.event.service.EventService;
import org.springframework.hateoas.MediaTypes;
import org.springframework.hateoas.server.mvc.WebMvcLinkBuilder;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.validation.Valid;
import java.net.URI;

import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn;

@RestController()
@RequestMapping(value = "/api/events", produces = MediaTypes.HAL_JSON_VALUE)
public class EventController {

    private final EventService eventService;

    public EventController(EventService eventService) {
        this.eventService = eventService;
    }

    @PostMapping("")
    public ResponseEntity create(@RequestBody @Valid EventDto eventDto) {
        EventResource eventResource = this.eventService.create(eventDto);
        WebMvcLinkBuilder selfAndUpdateLink =  linkTo(methodOn(EventController.class)
                .create(new EventDto()))
                .slash(eventResource.getEvent().getId());
        WebMvcLinkBuilder queryLink =  linkTo(methodOn(EventController.class));
        eventResource.add(queryLink.withRel("query-events"));
        eventResource.add(selfAndUpdateLink.withRel("update-event"));
        eventResource.add(selfAndUpdateLink.withSelfRel());
        URI uri = selfAndUpdateLink.toUri();
        return ResponseEntity.created(uri).body(eventResource);
    }
}

Test 확인


좋은 웹페이지 즐겨찾기