Spring HATEOAS로 REST 서비스 구축하기

Bluewind·2022년 5월 28일
0

Spring

목록 보기
5/5

Spring 공식 Guide Document의 Building REST services with Spring를 참고하였습니다.

REST API의 규약 중에는 "어플리케이션 상태에 대한 엔진으로써 하이퍼미디어" 일명 HATEOAS 규약이 있습니다. 이를 Spring HATEOAS를 사용하여 간단하게 구현하는 방법을 알아보겠습니다.

Dependency 추가

먼저 dependencies를 추가합니다.
저는 gradle 기반 프로젝트이기 때문에 아래와 같이 추가했습니다.

dependencies {
	...
	implementation 'org.springframework.boot:spring-boot-starter-hateoas:2.6.6'
    ...
}

EntityModel

@RestController
@RequiredArgsConstructor
public class AdminController {
	....
    @GetMapping(value = "/companies/{companyId}")
    public EntityModel<CompanyResponseDto> viewCompanyInfo(@PathVariable Long companyId) {
        CompanyResponseDto company = companyService.findCompany(companyId);
        return EntityModel.of(company, 
            linkTo(methodOn(AdminController.class).viewCompanyInfo(companyId)).withSelfRel(),
            linkTo(methodOn(AdminController.class).getAllCompanies()).withRel("companies")
        );
    }
}
  • linkTo, methodOn 메서드는 import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.*;를 통하여 정적 import를 통하여 가져옵니다.
  • 메소드의 리턴 유형이 EntityModel<T>으로 변경되었습니다. 이는 데이터 뿐만 아니라 링크를 포함하여 리턴하는 Spring HATEOAS의 컨테이너입니다.
  • linkTo(methodOn(AdminController.class).viewCompanyInfo(companyId)).withSelfRel() 는 Spring HATEOAS가 해당 Controller의 메서드인 viewCompanyInfo()에 대한 링크를 빌드하고 해당 링크로 지정하도록 요청합니다.

그렇게 요청하고나면 아래와 같이 결과 값이 나옵니다.

{
  "companyId": 1,
  "companyName": "빅스타피자",
  "companyLogo": "/logo/logo-bigstar_pizza.png",
  "updatedAt": "2022-05-28T16:53:51.542246",
  "createdAt": "2022-05-28T16:53:51.542246",
  "_links": {
    "self": {
      "href": "http://localhost:8080/admin/companies/1"
    },
    "companies": {
      "href": "http://localhost:8080/admin/companies"
    }
  }
}

요청한 결과값 뿐만 아니라 _links 밑에 해당 URI가 추가되어있는 것을 볼 수 있습니다.

CollectionModel

@GetMapping(value = "/companies")
public CollectionModel<EntityModel<CompanyResponseDto>> getAllCompanies() {
    List<EntityModel<CompanyResponseDto>> companies = companyService.findAll().stream()
        .map(company -> EntityModel.of(company,
            linkTo(methodOn(AdminController.class).viewCompanyInfo(company.getCompanyId())).withSelfRel(),
            linkTo(methodOn(AdminController.class).getAllCompanies()).withRel("companies")
        ))
        .collect(Collectors.toList());
    return CollectionModel.of(companies,
        linkTo(methodOn(AdminController.class).getAllCompanies()).withSelfRel()
    );
}
  • CollectionModel<>은 또다른 Spring HATEOAS 컨테이너입니다. 이전과 같이 단일 리소스 엔터티 대신 리소스 컬렉션을 캡슐화하는 것을 목표로 합니다. EntityModel<>, CollectionModel<>의 링크도 포함할 수 있습니다.
{
  "_embedded": {
    "companyResponseDtoList": [
      {
        "companyId": 1,
        "companyName": "빅스타피자",
        "companyLogo": "/logo/logo-bigstar_pizza.png",
        "updatedAt": "2022-05-28T16:53:51.542246",
        "createdAt": "2022-05-28T16:53:51.542246",
        "_links": {
          "self": {
            "href": "http://localhost:8080/admin/companies/1"
          },
          "companies": {
            "href": "http://localhost:8080/admin/companies"
          }
        }
      },
      {
        "companyId": 2,
        "companyName": "도미노피자",
        "companyLogo": "/logo/domino.png",
        "updatedAt": "2022-05-28T16:53:51.897417",
        "createdAt": "2022-05-28T16:53:51.897417",
        "_links": {
          "self": {
            "href": "http://localhost:8080/admin/companies/2"
          },
          "companies": {
            "href": "http://localhost:8080/admin/companies"
          }
        }
      },
      ...
    ]
  },
  "_links": {
    "self": {
      "href": "http://localhost:8080/admin/companies"
    }
  }
}
  • 위의 단일 결과값과 마찬가지로 _links 아래에는 self 링크가 포함되어 있습니다.
  • _embedded 섹션 아래는 결과값의 Collection들이 나열되어있으며 자체의 링크 또한 포함되어 있습니다.

링크 생성 간소화

위의 예제를 보면 EntityModel을 생성하는 코드를 반복해야 합니다.

이를 해결하기 위해서 위의 예제에서의 CompanyResponseDto 객체를 EntityModel<CompanyResponseDto> 객체로 변환하는 함수를 정의해야 합니다. 이 방법은 Spring HATEOAS의 RepresentationModelAssembler 인터페이스를 구현하여 간단하게 구현할 수 있습니다.

@Component
public class DtoModelAssembler implements RepresentationModelAssembler<CompanyResponseDto, EntityModel<CompanyResponseDto>> {

    @Override
    public EntityModel<CompanyResponseDto> toModel(CompanyResponseDto dto) {
        return EntityModel.of(dto, 
            linkTo(methodOn(AdminController.class).viewCompanyInfo(dto.getCompanyId())).withSelfRel(),
            linkTo(methodOn(AdminController.class).getAllCompanies()).withRel("companies")
        );
    }
}

이 인터페이스에 Override한 메소드에는 한가지 기능이 존재하는데, 위에서 말한 CompanyResponseDto 객체를 EntityModel<CompanyResponseDto> 객체로 변환하는 메소드입니다.

이 어셈블러를 활용하기 위해서 아까 AdminController 에서 해당 어셈블러를 주입하겠습니다.

@RestController
@RequiredArgsConstructor
public class AdminController {

    private final DtoModelAssembler assembler;
    ...

그리고 해당 assembler를 사용하여 코드를 개선해 보겠습니다.

@GetMapping(value = "/companies/{companyId}")
public EntityModel<CompanyResponseDto> viewCompanyInfo(@PathVariable Long companyId) {
    CompanyResponseDto company = companyService.findCompany(companyId);
    return assembler.toModel(company);
}

@GetMapping(value = "/companies")
public CollectionModel<EntityModel<CompanyResponseDto>> getAllCompanies() {
    List<EntityModel<CompanyResponseDto>> companies = companyService.findAll().stream()
        .map(assembler::toModel)
        .collect(Collectors.toList());
    return CollectionModel.of(companies,
        linkTo(methodOn(AdminController.class).getAllCompanies()).withSelfRel()
    );
}

코드가 훨씬 간결해졌네요!
이렇게 하이퍼미디어 기반 콘텐츠를 생성하는 Spring MVC REST 컨트롤러를 만들었습니다!

profile
NO EFFORT, NO RESULTS

0개의 댓글