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