가자공부하러!

Spring REST Docs(2) - 링크(HATEOAS) 본문

공부/Spring Boot

Spring REST Docs(2) - 링크(HATEOAS)

오피스엑소더스 2019. 12. 26. 10:51

1. HATEOAS?

1.1. HATEOAS

 - Hypermedia as the Engine of Application State

 - RESTful 아키텍쳐의 구성 요소

 - 클라이언트가 서버와 하이퍼미디어를 통해 동적으로 정보를 제공하는 네트워크 어플리케이션과 상호작용 할 수 있게끔 해줌

  > 서버가 응답을 할 때, 연관있는 URI를 응답에 포함시켜 반환

1.2. 사용목적

 - 서비스가 제공하는 자원에 접근하기 위해 아무런 사전 지식도 요구하지 않는 API 수준을 달성하기 위함

 - Spring REST Docs를 통해 제공하는 API가 RESTful 조건을 충족시키기 위함

  > 제공된 API와 연관된 URL을 알려줘서 Self-Descriptive Message 가 되게끔 API를 작성하기 위함

1.3. 장점

 - 요청 URI가 변경되어도 클라이언트에서 동적으로 생성된 URI를 사용함으로써, 클라이언트가 URI 수정에 따른 코드를 변경하지 않아도 되는 편리성 제공

 - URI 정보를 통해 들어오는 요청을 예측 가능

 - Resource가 포함된 URI를 보여주므로 Resource에 대한 신뢰도 확보

 

2. 간단예제

@Entity
@Table(name = "market")
@Getter
@Setter
@ToString
@Builder
public class Market {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String marketName;
  private String location;

  @OneToMany(mappedBy = "market", cascade = CascadeType.ALL)
  private Set<Employee> employees = new LinkedHashSet();;

  @OneToMany(mappedBy = "market", cascade = CascadeType.ALL)
  private Set<Item> items = new LinkedHashSet();
}
import org.springframework.hateoas.RepresentationModel;

public class MarketResource extends RepresentationModel {
  private Market market;

  public MarketResource(Market market) {
    this.market = market;
  }

  public Market getMarket() {
    return market;
  }
}
package com.exam.restdocs.controller;

import com.exam.restdocs.domain.Market;
import com.exam.restdocs.domain.MarketResource;
import com.exam.restdocs.vo.MarketReqVO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import java.net.URI;
import static org.springframework.hateoas.server.mvc.ControllerLinkBuilder.linkTo;

@Slf4j
@RestController
@RequestMapping(value = "/market")
public class MarketController {

  @RequestMapping(value ="/create/entity", method = RequestMethod.POST)
  public ResponseEntity createNewMarketEntity(@RequestBody Market market) {
    URI uri = linkTo(MarketController.class).slash("{id}").toUri();
    log.info("market : {}", market.toString());
    
    //테스트 용도이므로 아이디 임의 설정
    market.setId(10L);
    
    MarketResource marketResource = new MarketResource(market);
    //withSelfRel() => "self" : { "href" : uri }
    //withRel("update-market") => "update-market" : { "href" : uri }
    marketResource.add(linkTo(MarketController.class).withSelfRel());

    return ResponseEntity.created(uri).body(marketResource);
  }

}
package com.exam.restdocs.market;

import com.exam.restdocs.common.RestDocsConfiguration;
import com.exam.restdocs.domain.Employee;
import com.exam.restdocs.domain.Item;
import com.exam.restdocs.domain.Market;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.io.FileUtils;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
import org.springframework.hateoas.MediaTypes;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;

import java.io.File;
import java.util.LinkedHashSet;

import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName;
import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders;
import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel;
import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.restdocs.payload.PayloadDocumentation.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
@AutoConfigureRestDocs
@Import(RestDocsConfiguration.class)
public class MarketTest {

  @Autowired
  private MockMvc mockMvc;

  @Autowired
  private ObjectMapper objectMapper;

  @Test
  public void createMarketEntity() throws Exception{
    LinkedHashSet<Employee> employees = new LinkedHashSet();
    Employee employee = new Employee();
    employee.setName("emp1");
    employee.setAge(20);
    employees.add(employee);

    LinkedHashSet<Item> items = new LinkedHashSet();
    Item item = new Item();
    item.setCategory("grocery");
    item.setName("corn");
    item.setQuantity(50);
    items.add(item);

    Market market = Market.builder()
        .marketName("marketName")
        .location("norvrant")
        .employees(employees)
        .items(items)
        .build();

    mockMvc.perform(post("/market/create/entity")
        .contentType(MediaType.APPLICATION_JSON)//요청 타입은 JSON이다
        .accept(MediaTypes.HAL_JSON)//HAL JSON을 돌려달라
        .content(objectMapper.writeValueAsString(market))//objectMapper가 json string으로 바꿔줌
    )
        .andDo(print())
        .andExpect(status().isCreated())
        .andExpect(jsonPath("market.id").exists())//id가 있는지 확인
        .andExpect(jsonPath("_links.self").exists())
        .andDo(document("create-market",
              links(linkWithRel("self").description("link to self"))
            )
        )
    ;
  }
}
//테스트 응답
{
  "market":{
    "id":10,
    "marketName":"marketName",
    "location":"norvrant",
    "employees":[
      {
      "id":null,
      "name":"emp1",
      "age":20,
      "market":null
      }
    ],
    "items":[
      {
      "id":null,
      "name":"corn",
      "category":"grocery",
      "quantity":50,
      "market":null
      }
    ]
  },
  "_links":{
    "self":{
    "href":"http://localhost:8080/api/market"
    }
  }
}

만들어진 snippets
Spring REST Docs를 통해 만들어진 문서(http://localhost:18081/docs/index.html)

 

Comments