Spring 공식 Guide Document의 Building REST services with Spring를 참고하였습니다.
REST API의 규약 중에는 "어플리케이션 상태에 대한 엔진으로써 하이퍼미디어" 일명 HATEOAS
규약이 있습니다. 이를 Spring HATEOAS
를 사용하여 간단하게 구현하는 방법을 알아보겠습니다.
먼저 dependencies
를 추가합니다.
저는 gradle 기반 프로젝트이기 때문에 아래와 같이 추가했습니다.
dependencies {
...
implementation 'org.springframework.boot:spring-boot-starter-hateoas:2.6.6'
...
}
@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가 추가되어있는 것을 볼 수 있습니다.
@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 컨트롤러를 만들었습니다!