HATEOAS는 REST 아키텍쳐, Uniform Interface 조건 중 하나로 어플리케이션의 상태를 전이시킬 수 있는 링크, 기능을 제공해야한다.
Spring HATEOAS는 HATEOAS를 만족시키기 위해 만들어진 라이브러리로 크게 링크와 리소스를 만드는 기능, 링크를 찾아주는 기능을 제공한다.
여기서 Resource는 Client가 요구하는 데이터(예를 들어 게시판 글, 이벤트 등)를 포함하고 있으며 Links는 어플리케이션의 다른 상태나 리소스에 접근할 수 있는 링크를 제공한다.
링크에는 크게 2가지 종류가 있으며 HREF, REL가 있다. HREF는 하이퍼링크란 뜻으로 어플리케이션 상태를 전이시킬 수 있는 링크 종류이며 REL은 Resource와 관련된 정보와 기능을 제공하는 링크다.
EventAPI에서 제공하는 Link에는 다음과 같다.
@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());
}
Spring HATEOAS에서 제공하는 RepresentationModel을 상속받은 EventResource 구현
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;
}
}
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));
}
}
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);
}
}