RestApi 만들기 - HATEOAS (5)

반응형
반응형

HATEOAS란 무엇일까?
Hypermedia as the Engine of Application State의
약자로 요청에 대한 응답값에 사용자가 사이트를 네비게이션 할 수 있는 링크들을 만들어 포함시킬 수 있도록 해주는 라이브러리라고 한다.

이러한 기능을 스프링에서 제공하고 있다. 그래서 스프링 헤이티오스

그럼 헤이티오스는 어떤 것을 해줄까?

크게 2가지 부분으로 나눌 수 있다.
첫 번째는 링크를 만들 수 있는 기능을 제공한다.

링크 만드는 방법 :

linkTo(DeliveryController.class).slash(newDelivery.getId()).toUri();

링크를 만드는 방법은 이것말고도 많은데, 대표적으로 linkTo로 만드는 방법이 존재한다.
lonkTo로 uri를 만들 수 있게 되는데,
이 uri는 Controller위에 작성된 

@RequestMapping(value = "/api/delivery", produces = MediaTypes.HAL_JSON_VALUE)

과 같은 말이다.

이렇게 만든 link는 ResponseEntity에 넣어서 보낼 수도 있다.

두 번째는 리소스를 만드는 기능이다.
리소스라하면 자원을 뜻하는데,
이것을 이용해서 로그인했을때 결과와 로그인을 안 했을때의 결과가 서로 다르게 나올 수 있다.

스프링 몇 부터인가 리소스를 만드는 방법이 달라졌다.
이유는 모르겠다.

  • ResourceSupport => RepresentationModel

  • Resource => EntityModel

  • Resources => CollectionModel

  • PagedResources => PagedModel

리소스를 만들기전에 어떤 것을 만들지 미리 테스트코드를 작성해보자.

@Test
  void create_Delivery() throws Exception {
    DeliveryDto delivery = DeliveryDto.builder()
        .item("book")
        .user("klom")
        .deliveryTime(LocalDateTime.now())
        .deliveryEndTime(LocalDateTime.now().plusDays(10))
        .build();
    mockMvc.perform(post("/api/delivery/")
        .accept(MediaTypes.HAL_JSON_VALUE)
        .contentType(MediaType.APPLICATION_JSON_VALUE)
        .content(objectMapper.writeValueAsString(delivery))
    )
        .andDo(print())
        .andExpect(status().isCreated())
        .andExpect(jsonPath("id").value(Matchers.not(10)))
        .andExpect(jsonPath("status").value(DeliveryStatus.READY.name()))
        .andExpect(jsonPath("_links.query-events").exists())
        .andExpect(jsonPath("_links.update-events").exists())
        .andExpect(jsonPath("_links.self").exists());
  }

query-events : 목록으로 가능 링크
update-events : 업데이트 권한이 있는 사람이 갈 수 있는 링크
self : 보는거?

당연한 이야기지만, 이 코드는 실패하게 된다.

java.lang.AssertionError: No value at JSON path "_links.query-events"

왜냐하면 나는 리소스를 만들지 않았기 때문이다.

리소스를 만들어보자. 근데 이제 리소스라는 단어보다 모델이라는 단어가 더 잘 어울리나?..
트렌드에 맞춰 모델이라고 이름을 지어줬다.

public class DeliveryModel extends RepresentationModel {
  private Delivery delivery;

  public DeliveryModel(Delivery delivery) {
    this.delivery = delivery;
  }

  public Delivery getDelivery() {
    return delivery;
  }
}

그럼 이제 이것을 사용해 보자.
기존에는 생성하는 부분은 다음과 같았다.

return ResponseEntity.created(createUri).body(deliver);

원래 delivery를 new DeliveryModel로 감싸주면 된다.
그러면 이제

return ResponseEntity.created(createUri).body(new DeliveryModel(deliver));

이렇게 바뀌었다.
이것을 실행하면....,

{"delivery":{"id":1,"item":"book","user":"klom","deliveryTime":"2021-02-16T21:53:46.112417","deliveryEndTime":"2021-02-26T21:53:46.112448","status":"READY","itemPrice":null,"deliveryCost":5000}}

원래 delivery라는것에 감싸는 형태는 아니였지만, 
이것을 추가하고 보니 이렇게 바뀌었다.ㅜㅜ

해결 하는 방법은 3가지가 존재한다.

1. 무식한 방법
엄청 무식한 방법이다.

package restapiset;

import java.time.LocalDateTime;
import org.springframework.hateoas.RepresentationModel;

public class DeliveryModel extends RepresentationModel {
  private int id;
  private String item;
  private String user;
  private LocalDateTime deliveryTime;
  private LocalDateTime deliveryEndTime;
  private DeliveryStatus status;
  private Integer itemPrice;
  private Integer deliveryCost;

  public DeliveryModel(Delivery delivery) {
    this.id = delivery.getId();
    this.item = delivery.getItem();
    this.user = delivery.getUser();
    this.deliveryTime = delivery.getDeliveryTime();
    this.deliveryEndTime = delivery.getDeliveryEndTime();
    this.status= delivery.getStatus();
    this.itemPrice = delivery.getItemPrice();
    this.deliveryCost = delivery.getDeliveryCost();
  }

  public int getId() {
    return id;
  }

  public String getItem() {
    return item;
  }

  public String getUser() {
    return user;
  }

  public LocalDateTime getDeliveryTime() {
    return deliveryTime;
  }

  public LocalDateTime getDeliveryEndTime() {
    return deliveryEndTime;
  }

  public DeliveryStatus getStatus() {
    return status;
  }

  public Integer getItemPrice() {
    return itemPrice;
  }

  public Integer getDeliveryCost() {
    return deliveryCost;
  }
}

Delivery대신에 이렇게 넣어주면 된다.
get을 넣는 이유는 이것으로 값을 찾기 때문이다.
이것을 실행해보면,

{"id":1,"item":"book","user":"klom","deliveryTime":"2021-02-16T22:01:40.563615","deliveryEndTime":"2021-02-26T22:01:40.563635","status":"READY","itemPrice":null,"deliveryCost":5000}

아까 처럼 delivery가 사라졌다.
하지만 이 방법은 코드양이 너무 많아지기 때문에 별로라고 생각이 든다.

두 번째 방법 : get위에 @JsonUnwrapped을 넣어주면 된다.

public class DeliveryModel extends RepresentationModel {
  private Delivery delivery;

  public DeliveryModel(Delivery delivery) {
    this.delivery = delivery;
  }

  @JsonUnwrapped
  public Delivery getDelivery() {
    return delivery;
  }
}

이렇게 해도.

{"id":1,"item":"book","user":"klom","deliveryTime":"2021-02-16T22:05:20.685635","deliveryEndTime":"2021-02-26T22:05:20.685659","status":"READY","itemPrice":null,"deliveryCost":5000}

delivery가 나오지 않았다.

세 번째방법 : 이 방법은 백기선님이 알려준 방법으로 약간의 꼼수?다.
RepresentationModel의 하위 클래스로 EntityModel이 존재하는데

@Nullable
@JsonUnwrapped
@JsonSerialize(using = MapSuppressingUnwrappingSerializer.class)
public T getContent() {
	return content;
}

@JsonUnwrapped가 존재한다는 것을 알 수 있다.
그러면 이것을 어떻게 사용하냐 하면,

public class DeliveryModel extends EntityModel<Delivery> {
  public DeliveryModel(Delivery delivery, Link... links) {
    super(delivery, links);
  }
}

이렇게 사용하고 실행해보자.

{"id":1,"item":"book","user":"klom","deliveryTime":"2021-02-16T22:14:26.890015","deliveryEndTime":"2021-02-26T22:14:26.890037","status":"READY","itemPrice":null,"deliveryCost":5000}

역시 없어졌다.
근데 이거 사용해도 되나 @Deprecated사용하지 말라는데...

잘 모르겠으니 일단 사용하자.

이제 리소스에 연결되는 링크들을 만들어보자. 내가 만들어될 링크는 총 3개
query-events : 목록으로 가능 링크
update-events : 업데이트 권한이 있는 사람이 갈 수 있는 링크
self : 보는거?

model.add(linkTo(DeliveryController.class).withRel("query-events"));
model.add(linkTo(DeliveryController.class).slash(newDelivery.getId()).withRel("update-events"));
model.add(selfRelationBuilder.withSelfRel());

이렇게 수정해줬다.

원하는 그림이다.
그런데 위 링크를 다른곳에 넣을 수 는 없을까?

 public DeliveryModel(Delivery delivery, Link... links) {
    super(delivery, links);
    add(linkTo(DeliveryController.class).withRel("query-events"));
    add(linkTo(DeliveryController.class).slash(delivery.getId()).withRel("update-events"));
    add(linkTo(DeliveryController.class).slash(delivery.getId()).withSelfRel());
  }

이렇게 해결했다.!

super(delivey,links)는 @Deprecated되었기 때문에 다른 방안을 생각해 봐야 될듯싶다.

반응형

댓글

Designed by JB FACTORY