ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [스프링 인 액션] Chapter 6 - REST 서비스 생성하기 :: 하이퍼미디어 사용하기
    개발서적읽기/Spring in Action 제 5판 2020. 8. 18. 01:22



    지금까지 생성했던 기본적인 API에서는 


    해당 API를 사용하는 클라이언트가 API의 URL 스킴을 알아야 한다.


    만약 API의 URL 스킴이 변경되면 어떻게 될까?


    하드코딩된 클라이언트 코드는 API를 잘못 인식하여 정상적으로 실행되지 않을 것이다.


    이를 극복할 수 있는 REST API 구현 방법이 있다.


    바로 HATEOAS(Hypermedia As The Engine Of Application State)이다.


    HATEOAS는 API로부터 반환되는 리소스에 


    해당 리소스와 관련된 하이퍼링크들이 포함되는 방식이다.


    따라서 클라이언트가 최소한의 API URL만 알면 반환되는 리소스와 관련하여 


    처리 가능한 다른 API URL들을 알아내어 사용할 수 있다.


    이러한 응답 JSON 형식을 HAL(Hypertext Application Language)라고 한다.


    HAL JSON 형식을 사용하게 되면, 클라이언트 애플리케이션이 타코 리스트의


    특정 타코에 대해 HTTP 요청을 할 때 해당 타코 리소스의 URL을 지정하지 않아도 된다.


    원래 요청했던 응답의 "_link" 링크를 사용하면 된다.




    ■HATEOAS dependency 추가하기


    스프링 HATEOAS 프로젝트는 하이퍼링크를 스프링에 지원한다.


    즉, 스프링 MVC 컨트롤러에서 리소스를 반환하기 전에 


    해당 리소스에 링크를 추가하는 데 사용할 수 있는 클래스와 리소스 어셈블러들을 제공한다.

    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-hateoas</artifactId>
    </dependency>

    전 챕터에서 추가하긴 했지만, 스프링 HATEOAS 스타터 의존성을 추가하자.


    이 스타터는 스프링 HATEOAS를 해당 프로젝트의 classpath에 추가한다.


    또한 스프링 HATEOAS를 활성화하는 자동-구성도 제공한다.


    따라서 도메인 타입 대신 리소스 타입을 반환하도록 컨트롤러를 수정하면 된다.


    지금부터는 /design/recent에 대한 GET 요청에서 반환되는 


    최근 타코 리스트에 하이퍼미디어 링크를 추가할 것이다.




    ■하이퍼링크 추가하기


    스프링 HATEOAS는 하이퍼링크 리소스를 나타내는 두 개의 기본 타입인


    Resource와  Resources를 제공한다.


    Resource 타입은 단일 리소스를, 그리고 Resources는 리소스 컬렉션을 나타낸다.


    그리고 두 타입 모두 다른 리소스를 링크할 수 있다.


    두 타입이 전달하는 링크는 스프링 MVC컨트롤러 메서드에서 반환될 때


    클라이언트가 받는 JSON(or XML)에 포함된다.


    최근 생성된 타코 리스트에 하이퍼링크를 추가하려면


    DesignTacoController.recentTacos 메소드에서 List<Taco>를 반환하는 대신


    Resources 객체를 반환하도록 수정해야 한다.

    @GetMapping("/recent")
    public Resources<Resource<Taco>> recentTacos() {
    PageRequest page = PageRequest.of(0, 12, Sort.by("createdAt").descending());

    List<Taco> tacos = tacoRepo.findAll(page).getContent();
    Resources<Resource<Taco>> recentResources = Resources.wrap(tacos);

    recentResources.add(new Link("http://localhost:8080/design/recent","recent"));

    return recentResources;
    }

    이렇게 수정된 recentTacos()에서는 타코 리스트를 직접 반환하지 않고


    Resources.wrap()을 사용해서 recentTacos()의 반환 타입인 


    Resources<Resource<Taco>>의 인스턴스로 타코 리스트를 매핑한다.


    그러나 Resource 객체를 반환하기 전에


    이름이 recents이고 URL이 http://localhost:8080/design/recent인 링크를 추가한다.


    따라서 API 요청에서 반환되는 리소스에 다음의 JSON 코드가 포함된다.

    {
    "_links" : {
    "recents" : {
    "href" : "http://localhost:8080/design/recent"
    }
    }
    }

    이제 타코 리소스 자체나 각 타코의 식자재에 대한 링크도 추가해야 한다.


    그전에 우선 recents 링크에 지정한 하드코딩된 URL을 살펴보자.


    스프링 HATEOAS는 링크 빌더를 제공하여 URL을 하드코딩하지 않도록 도와준다.


    스프링 HATEOAS 링크 빌더 중 가장 유용한 것이 ControllerLinkBuilder다.


    이 링크 빌더를 사용하면 URL을 하드코딩하지 않고 호스트 이름을 알 수 있다.


    그리고 컨트롤러의 기본 URL에 관련된 링크의 빌드를 도와주는 편리한 API를 제공한다.


    ControllerLinkBuilder를 사용하면 recentTacos()의 하드코딩된 Link를 


    다음과 같이 변경하여 생성할 수 있다.

    @GetMapping("/recent")
    public Resources<Resource<Taco>> recentTacos() {
    PageRequest page = PageRequest.of(0, 12, Sort.by("createdAt").descending());

    List<Taco> tacos = tacoRepo.findAll(page).getContent();
    Resources<Resource<Taco>> recentResources = Resources.wrap(tacos);

    //recentResources.add(new Link("http://localhost:8080/design/recent","recents"));
    recentResources.add(ControllerLinkBuilder.linkTo(DesignTacoController.class)
    .slash("recent")
    .withRel("recents"));

    return recentResources;
    }

    혹은 linkTo()를 사용하는 방법도 있다.

        @GetMapping("/recent")
    public Resources<Resource<Taco>> recentTacos() {
    PageRequest page = PageRequest.of(0, 12, Sort.by("createdAt").descending());

    List<Taco> tacos = tacoRepo.findAll(page).getContent();
    Resources<Resource<Taco>> recentResources = Resources.wrap(tacos);

    //recentResources.add(new Link("http://localhost:8080/design/recent","recents"));
    // recentResources.add(ControllerLinkBuilder.linkTo(DesignTacoController.class)
    // .slash("recent")
    // .withRel("recents"));
    recentResources.add(
    ControllerLinkBuilder.linkTo(ControllerLinkBuilder.methodOn(DesignTacoController.class).recentTacos()).withRel("recents")
    );

    return recentResources;
    }




    ■리소스 어셈블러 생성하기


    다음으로는 리스트에 포함된 각 타코 리소스에 대한 링크를 추가해야 한다.


    첫 번째 방법은 반복 루프에서 Resources 객체가 가지는 


    각 Resource<Taco> 요소에 Link를 추가하는 것이다.


    그러나 이 경우는 타코 리소스의 리스트를 반환하는 API 코드마다 


    루프를 실행하는 코드가 있어야 하므로 번거롭다.


    따라서 다른 방법이 필요하다.


    Resources.wrap()에서 리스트의 각 타코를 Resource 객체로 생성하는 대신 


    Taco 객체를 새로운 TacoResource 객체로 변환하는 유틸리티 클래스를 정의해보자.


    TacoResource 객체는 도메인 객체인 Taco와 유사하지만, 링크를 추가로 가질 수 있다.

    package tacos.web;
    /*
    * @USER JungHyun
    * @DATE 2020-08-19
    * @DESCRIPTION
    */

    import lombok.Getter;
    import org.springframework.hateoas.ResourceSupport;
    import tacos.Ingredient;
    import tacos.Taco;

    import java.util.Date;
    import java.util.List;

    public class TacoResource extends ResourceSupport {
    @Getter
    private final String name;

    @Getter
    private final Date createdAt;

    @Getter
    private final List<Ingredient> ingredients;

    public TacoResource(Taco taco){
    this.name = taco.getName();
    this.createdAt = taco.getCreatedAt();
    this.ingredients = taco.getIngredients();
    }
    }

    TacoResource는 여러모로 Taco 도메인 클래스와 그리 다르지 않다.


    하지만 TacoResource는 ResourceSupport의 서브 클래스로서 Link 객체 리스트와


    이것을 관리하는 메서드를 상속받는다.


    그리고 TacoResource는 Taco의 id 속성을 갖지 않는다.


    왜냐하면 DB에서 필요한 id를 API에 노출시킬 필요가 없기 때문이다.


    그리고 API 클라이언트 관점에서는 해당 리소스의 self 링크가 


    리소스 식별자 역할을 할 것이다.


    TacoResource는 Taco 객체를 인자로 받는 하나의 생성자를 가지며,


    Taco 객체의 속성 값을 자신의 속성에 복사한다.


    TacoResource를 여기까지만 구현한다면 Taco 객체 리스트를 


    Resource<TacoResource>로 변환하기 위해 여전히 반복 루프가 필요하다.


    이를 해결하기 위해 리소스 어셈블러 클래스를 생성하자.

    package tacos.web.api;
    /*
    * @USER JungHyun
    * @DATE 2020-08-19
    * @DESCRIPTION
    */

    import org.springframework.hateoas.mvc.ResourceAssemblerSupport;
    import tacos.Taco;
    import tacos.web.DesignTacoController;
    import tacos.web.TacoResource;

    public class TacoResourceAssembler extends ResourceAssemblerSupport<Taco, TacoResource> {
    public TacoResourceAssembler() {
    super(DesignTacoController.class, TacoResource.class);
    }

    @Override
    protected TacoResource instantiateResource(Taco taco) {
    return new TacoResource(taco);
    }

    @Override
    public TacoResource toResource(Taco taco) {
    return createResourceWithId(taco.getId(), taco);
    }
    }

    TacoResourceAssembler의 기본 생성자에서는 슈퍼 클래스인 


    ResourceAssemblerSupport의 기본 생성자를 호출하며


    이때 TacoResource를 생성하면서 만들어지는 링크에 포함되는 URL의 기본 경로를


    결정하기 위해 DesignTacoController를 사용한다.


    그리고 Taco 객체로 TacoResource 인스턴스를 생성해야 하기 때문에


    instantiateResource() 메서드를 재정의하였다.


    TacoResource가 기본 생성자를 갖고 있다면 이 메서드는 생략할 수 있다.


    마지막으로 toResource() 메서드는 ResourceAssemblerSupport로부터 상속받을 때


    반드시 오버라이드 해야 한다. 여기서는 Taco 객체로 TacoResource 인스턴스를


    생성하면서 Taco 객체의 id 속성 값으로 생성되는 self 링크가 URL에 자동 지정된다.


    외견상으로는 instantiateResource()와 toResource()가 같은 목적을 갖는 것처럼 보인다.


    하지만 instantiateResource()는 Resource 인스턴스만 생성하는 반면,


    toResource()는 Resource 인스턴스를 생성하면서 링크도 추가한다.


    toResource()는 내부적으로 instantiateResource()를 호출한다.


    이젠 TacoResourceAssembler를 사용해서 recentTacos()를 변경해보자.

    @GetMapping("/recent")
    public Resources<TacoResource> recentTacos() {
    PageRequest page = PageRequest.of(0, 12, Sort.by("createdAt").descending());

    List<Taco> tacos = tacoRepo.findAll(page).getContent();
    List<TacoResource> tacoResources = new TacoResourceAssembler().toResources(tacos);

    Resources<TacoResource> recentResources = new Resources<TacoResource>(tacoResources);

    recentResources.add(
    ControllerLinkBuilder.linkTo(ControllerLinkBuilder.methodOn(DesignTacoController.class).recentTacos())
    .withRel("recents")
    );

    return recentResources;
    }

    변경된 recentTacos()에서는 새로운 TacoResource 타입을 사용하여


    Resources<Resource<Taco>> 대신 Resources<TacoResource>를 반환한다.


    즉, 리퍼지터리로부터 타코들을 가져와서 Taco 객체 리스트에 저장한 후


    이 리스트를 TacoResourceAssembler의 toResources()에 전달한다.


    그리고 이 메서드에서는 리스트의 모든 Taco 객체에 대해


    TacoResourceAssembler에 오버라이드했던 toResource() 메서드를 호출하여


    TacoResource 객체를 저장한 리스트를 생성한다.


    그 다음에 TacoResource 객체 리스트를 사용하여 


    Resources<TacoResource> 객체를 생성한 후 recents 링크를 추가한다.


    이 시점에서 /design/recent에 대한 GET 요청은 각각 self 링크를 갖는 타코들과


    이 타코들이 포함된 리스트 자체의 recents 링크를 갖는 타코 리스트를 생성할 것이다.


    그러나 각 타코의 Ingredient 객체에는 여전히 링크가 없다. 


    따라서 식자재의 리소스 어셈블러 클래스도 새로 생성해야 한다.

    package tacos.web.api;
    /*
    * @USER JungHyun
    * @DATE 2020-08-19
    * @DESCRIPTION
    */

    import lombok.Getter;
    import org.springframework.hateoas.ResourceSupport;
    import tacos.Ingredient;

    public class IngredientResource extends ResourceSupport {
    @Getter
    private final String name;

    @Getter
    private final Ingredient.Type type;

    public IngredientResource(Ingredient ingredient) {
    this.name = ingredient.getName();
    this.type = ingredient.getType();
    }
    }
    package tacos.web.api;
    /*
    * @USER JungHyun
    * @DATE 2020-08-19
    * @DESCRIPTION
    */

    import org.springframework.hateoas.mvc.ResourceAssemblerSupport;
    import tacos.Ingredient;

    public class IngredientResourceAssembler extends ResourceAssemblerSupport<Ingredient, IngredientResource> {

    public IngredientResourceAssembler() {
    super(IngredientController.class, IngredientResource.class);
    }

    @Override
    public IngredientResource toResource(Ingredient ingredient) {
    return createResourceWithId(ingredient.getId(), ingredient);
    }

    @Override
    protected IngredientResource instantiateResource(Ingredient ingredient) {
    return new IngredientResource(ingredient);
    }
    }

    이제 Ingredient객체 대신 IngredientResource 객체를 처리하도록


    TacoResource를 수정하자.

    package tacos.web;
    /*
    * @USER JungHyun
    * @DATE 2020-08-19
    * @DESCRIPTION
    */

    import lombok.Getter;
    import org.springframework.hateoas.ResourceSupport;
    import tacos.Taco;
    import tacos.web.api.IngredientResource;
    import tacos.web.api.IngredientResourceAssembler;

    import java.util.Date;
    import java.util.List;

    public class TacoResource extends ResourceSupport {
    private static final IngredientResourceAssembler ingredientAssembler = new IngredientResourceAssembler();

    @Getter
    private final String name;

    @Getter
    private final Date createdAt;

    @Getter
    private final List<IngredientResource> ingredients;

    public TacoResource(Taco taco) {
    this.name = taco.getName();
    this.createdAt = taco.getCreatedAt();
    this.ingredients = ingredientAssembler.toResources(taco.getIngredients());
    }
    }

    이제는 최근 생성된 타코 리스트가 완벽하게 하이퍼링크를 갖게 되었다.


    즉 리스트 자체의 recents 링크, 리스트에 포함된 모든 타코의 링크, 


    각 타코의 식자재 링크를 갖게 되었다. 




    ■embedded 관계 이름 짓기


    다른 주제로 넘어가기 전에 앞에 나왔던 내용에 관해 추가로 알아볼 것이 있다.

    {
    "embedded" : {
    "tacoResourceList" : [
    ...
    ]
    }
    }

    위와 같은 최상위 수준의 요소가 있다.


    여기서 embedded 밑의 tacoResourceList라는 이름에 주목하자.


    이 이름은 Resources 객체가 List<TacoResource>로부터 생성되었다는 것을 나타낸다.


    그럴리는 없겠지만, 만일 TacoResource 클래스의 이름이 수정되면


    결과 JSON의 필드 이름이 그에 맞춰 바뀔 것이다.


    따라서 변경 전의 이름을 사용하는 클라이언트 코드가 제대로 실행되지 않을 것이다.


    이럴 때 @Relation 애노테이션을 사용하면 자바로 정의된 리소스 타입 클래스 이름과


    JSON 필드 이름 간의 결합도를 낮출 수 있다.


    즉 다음과 같이 TacoResource에 @Relation을 추가하면 


    스프링 HATEOAS가 결과 JSON의 필드 이름을 짓는 방법을 지정할 수 있다.

    @Relation(value = "taco", collectionRelation = "tacos")
    public class TacoResource extends ResourceSupport {

    여기서는 TacoResource 객체 리스트가 Resources 객체에서 사용될 때


    tacos라는 이름이 되도록 지정하였다. 


    그리고 API에서는 사용되지 않겠지만, JSON에서는 TacoResource 객체가 taco로 참조된다.


    이에 따라 TacoResource의 이름을 변경하는 것과 상관 없이 /design/recent로부터


    반환되는 JSON은 다음과 같다.

    {
    "embedded" : {
    "tacos" : [
    ...
    ]
    }
    }

    스프링 HATEOAS는 직관적이고 쉬운 방법으로 API에 링크를 추가하지만


    우리가 필요로 하지 않는 몇 줄의 코드를 자동으로 추가한다.


    API의 URL 스킴이 변경되면 클라이언트 코드 실행이 중단됨에도


    자동으로 추가되는 코드가 싫어서 API에 HATEOAS 사용을 고려하지 않는 


    개발자들도 있다. 하지만 HATEOAS를 적극 사용할 것을 권장한다.


    만일 스프링 데이터를 리퍼지터리로 사용한다면 또 다른 방법이 있다.


    다음 포스팅에서는 3장에서 스프링 데이터로 생성했던 데이터 리퍼지터리를 기반으로


    스프링 데이터 REST가 API를 자동 생성할 수 있게 돕는 방법을 알아본다.






    댓글

Designed by Tistory.