티스토리 뷰

Back-End/Spring

Spring HATEOAS 적용

chaibin 2020. 5. 20. 16:10

 

1. Build.gradle에 Spring HATEOAS 추가

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-hateoas'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'org.springframework.boot:spring-boot-starter-data-rest'
	compileOnly 'org.projectlombok:lombok'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation('org.springframework.boot:spring-boot-starter-test') {
		exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
	}
	testImplementation 'io.projectreactor:reactor-test'
}

org.springframework.boot:spring-boot-starter-hateoas를 추가해야 합니다.
이 글을 적은 시점에서는 spring-hateoas 1.1.0을 적용되어있습니다. 0.25 버전이 아니므로 참고해주세요

 

2. 단일 Entity HATEOAS 적용

2.1 수동으로 Entity에 대해서 적용

수동으로 적용할 때는 EntityModel<T>클래스를 이용하고 Static Method로 객체를 만들기 때문에 EntityModel.of()를 통해서 객체를 생성합니다.

    @GetMapping("/test1")
    public ResponseEntity<EntityModel<Domain>> basicHateoas() {

        Domain domain = domainService.getDomain();
        return ResponseEntity.ok().body(
                EntityModel.of(domain)
                        .add(linkTo(methodOn(DomainController.class).basicHateoas()).withSelfRel())  // /hateoas/test1 - self
                        .add(linkTo(DomainController.class).slash("test").withRel("test"))    // /hateoas/test - test
                        .add(linkTo(methodOn(DomainController.class).dummy()).withRel("dummy"))      // /hateoas/dummy - dummy
                        .add(linkTo(methodOn(DomainController.class).dummy2(1L)).withRel(IanaLinkRelations.TAG))  // /hateoas/1 - tag
        );
    }

add() 메소드를 통해서 link를 추가할 수 있습니다.
linkTo를 통해 Controller.class를 넣으면 Controller에 적용된 RequestMapping()이 들어가고
linkTo(methodOn(Controller.class).method(argument))이런식으로 넣으면 해당 API의 URI가 매핑이 됩니다.

webmvc를 이용하면 org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo와
org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn을 import static으로 지정해주세요

webflux를 이용하면 org.springframework.hateoas.server.mvc.WebFluxLinkBuilder.linkTo와 org.springframework.hateoas.server.mvc.WebFluxLinkBuilder.methodOn을 import static으로 지정해주세요

link에 대한 이름은 withSelRel()메소드와 withRel() 메소드가 있습니다.
withSelRel()메소드를 이용하면 link이름은 self로 지정이 됩니다.
그 외의 이름을 지정할 때는 withRel를 사용하고 String을 넣거나 IanaLinkRelations를 통한 유틸리티 클래스에 지정된 이름을 넣어줄 수 있습니다.

{
    "id": 1,
    "name": "domain",
    "desc": "desc",
    "_links": {
        "self": {
            "href": "http://localhost:8080/hatoeas/test1"
        },
        "test": {
            "href": "http://localhost:8080/hatoeas/test"
        },
        "dummy": {
            "href": "http://localhost:8080/hatoeas/dummy"
        },
        "tag": {
            "href": "http://localhost:8080/hatoeas/1"
        }
    }
}

2.2 RepresentationModelAssemblerSupport<T,D>클래스를 이용

2.2.1 RepresentationModel 생성

RepresentationModelAssemblerSupport클래스는 RepresetionModel을 이용합니다.

@Getter
@Setter
public class DomainModel extends RepresentationModel<DomainModel> {

    Long id;
    String name;
    String desc;

    public DomainModel() {
        super();
    }

}


위에 Model에서 RepresentationModel<DomainModel>가 반드시 상속되어야 합니다.

 

2.2.2 RepresentationModelAssemblerSupport생성

생성자는 controller클래스와 DomainModel클래스를 넣습니다.

@Component
public class DomainRepresentationModelAssembler extends RepresentationModelAssemblerSupport<Domain, DomainModel> {

    public DomainRepresentationModelAssembler(Class<?> controllerClass, Class<DomainModel> resourceType) {
        super(controllerClass, resourceType);
    }

    public DomainRepresentationModelAssembler() {
        super(DomainController.class, DomainModel.class);
    }

    @Override
    public DomainModel toModel(Domain entity) {

        DomainModel model = instantiateModel(entity);
        //DomainModel model2= createModelWithId(entity.getId(), entity);
        model.add(linkTo(methodOn(DomainController.class).basicHateoas()).withSelfRel())  // /hateoas/test1 - self
                .add(linkTo(DomainController.class).slash("test").withRel("test"))    // /hateoas/test - test
                .add(linkTo(methodOn(DomainController.class).dummy()).withRel("dummy"));     // /hateoas/dummy - dummy

        model.setId(entity.getId());
        model.setDesc(entity.getDesc());
        model.setName(entity.getName());
        return model;
    }
}

instantiateModel를 통해 DomainModel 객체를 생성합니다.(new로 해도 상관없습니다.)
createModelWithId() 메소드를 사용해서 만들 수 있는데 이 메소드는 미리 self Link를 만들어 줍니다.

그다음 Link를 추가하고 변수들을 매핑시켜 값을 넣어주면 됩니다.

    @GetMapping("/test2")
    public ResponseEntity<DomainModel> assemblerHateoas() {

        return ResponseEntity.ok().body(assembler.toModel(domainService.getDomain()));
    }

 

2.3 RepresentationModelProcessor<T> 를 이용

해당 클래스는 T가 반환될 때 process메소드를 실행시켜주는 클래스입니다. T는 EntityModel를 사용합니다.

@Component
public class DomainRepresentationModelProcessor implements RepresentationModelProcessor<EntityModel<Domain>> {

    @Override
    public EntityModel<Domain> process(EntityModel<Domain> model) {
        return model.add(linkTo(methodOn(DomainController.class).basicHateoas()).withSelfRel())  // /hateoas/test1 - self
                .add(linkTo(DomainController.class).slash("test").withRel("test"))    // /hateoas/test - test
                .add(linkTo(methodOn(DomainController.class).dummy()).withRel("dummy"));     // /hateoas/dummy - dummy

    }
}


controller에서는 EntityModel를 만들어서 반환시켜주면 됩니다.

    @GetMapping("/test3")
    public ResponseEntity<EntityModel<Domain>> processorHateoas() {

        return ResponseEntity.ok().body(EntityModel.of(domainService.getDomain()));
    }

 

3. 복수 Entity HATEOAS 적용

3.1 수동으로 적용할 때

    @GetMapping("/collection/test1")
    public ResponseEntity<CollectionModel<EntityModel<Domain>>> hateoasCollection() {
        List<EntityModel<Domain>> domains = domainService.getDomains().
                stream()
                .map((domain) ->
                        EntityModel.of(domain)
                                .add(linkTo(methodOn(DomainController.class).basicHateoas()).withSelfRel())  // /hateoas/test1 - self
                                .add(linkTo(DomainController.class).slash("test").withRel("test"))    // /hateoas/test - test
                                .add(linkTo(methodOn(DomainController.class).dummy()).withRel("dummy"))      // /hateoas/dummy - dummy
                                .add(linkTo(methodOn(DomainController.class).dummy2(1L)).withRel(IanaLinkRelations.TAG))  // /hateoas/1 - tag
                ).collect(Collectors.toList());

        return ResponseEntity.ok().body(CollectionModel.of(domains)
                .add(linkTo(methodOn(DomainController.class).hateoasCollection()).withSelfRel())
        );
    }

List에 있는 Entity를 EntityModel로 바꾸어주고 CollectionModel.of를 통해 값을 넣어주면 됩니다. 

3.2 RepresentationModelAssemblerSupport를 사용

toEntityModel과 toCollectionModel를 정의해서 사용하면 됩니다. 

@Component
public class DomainRepresentationModelAssembler extends RepresentationModelAssemblerSupport<Domain, DomainModel> {

    public DomainRepresentationModelAssembler(Class<?> controllerClass, Class<DomainModel> resourceType) {
        super(controllerClass, resourceType);
    }

    public DomainRepresentationModelAssembler() {
        super(DomainController.class, DomainModel.class);
    }

    @Override
    public DomainModel toModel(Domain entity) {

        DomainModel model = instantiateModel(entity);

        model.add(linkTo(methodOn(DomainController.class).basicHateoas()).withSelfRel())  // /hateoas/test1 - self
                .add(linkTo(DomainController.class).slash("test").withRel("test"))    // /hateoas/test - test
                .add(linkTo(methodOn(DomainController.class).dummy()).withRel("dummy"));     // /hateoas/dummy - dummy

        model.setId(entity.getId());
        model.setDesc(entity.getDesc());
        model.setName(entity.getName());
        return model;
    }

    @Override
    public CollectionModel<DomainModel> toCollectionModel(Iterable<? extends Domain> entities) {
        return super.toCollectionModel(entities)
                .add(linkTo(methodOn(DomainController.class).hateoasCollection()).withSelfRel());
    }
}

Controller에서는 toCollectionModel메소드만 불러와주면 됩니다.

    @GetMapping("/collection/test2")
    public ResponseEntity<CollectionModel<DomainModel>> assemblerHateoasCollection() {
        return ResponseEntity.ok().body(assembler.toCollectionModel(domainService.getDomains()));
    }

3.3 Page로 구성되어있을 경우

스프링에서는 PagedResourcesAssembler를 이용해서 PagedModel을 만들어 주면 됩니다.
PagedResourcesAssembler는 Autowired를 통해 주입시켜주고 toModel메소드를 통해서 구현

toModel메소드는 Page와 RepresentationModelAssembler가 들어가기 때문에 각각의 Entity에 대한 Link는 RepresentationModelAssembler 통해 구현하면 됩니다.

    @GetMapping("/page/test1")
    public ResponseEntity<PagedModel<Domain>> hateoasPageModel(@RequestParam("page") int page) {
        return ResponseEntity.ok().body(pageAssembler.toModel(domainService.getPageDomains(page), assembler));
    }

 

 

참조
https://docs.spring.io/spring-hateoas/docs/1.1.0.RELEASE/reference/html/
https://github.com/spring-projects/spring-hateoas-examples

 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/07   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31
글 보관함