공부/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"
}
}
}