클라이언트는 서버로부터 받은 응답에 포함된 하이퍼미디어 링크를 통해 상태 전이를 수행할 수 있어야 합니다. 즉, 클라이언트는 서버로부터 받은 응답을 통해 애플리케이션의 상태를 파악하고 상호작용할 수 있는 링크 정보를 동적으로 받아들이게 됩니다.
REST API를 향상하여 데이터를 반환할 뿐만 아니라 리소스에 관한 작업을 수행하는 방법의 정보를 제공하면 어떨까요?
{
"name": "example",
"birthDate": "2023-01-12",
"_links": {
"all-users": {
"href": "http://localhost:8080/users"
}
}
}
여기서는 응답으로 name, birthDate가 있습니다. 또한, links는 후속 작업을 수행하는 방법을 소비자에게 알려줍니다.
이렇게 사용자가 사용하기 편한 REST API 설명서가 있습니다. 우리는 이런 설명서는 몇 가지 옵션으로 작성할 수 있습니다.
위와 같은 형식을 HAL을 사용하여 지정한다면 스프링 HATEOAS를 사용하면 됩니다.
지금부터 스프링 HATEOAS와 HAL을 구체적으로 알아보겠습니다.
<!-- HATEOAS -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>study.rest.webservices</groupId>
<artifactId>restful-web-services</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>restful-web-services</name>
<description>restful-web-services</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- HATEOAS -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>
<!-- validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!--<!– Swagger –>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.4.0</version>
</dependency>
<!– XML –>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
UserResource로 가서 retrieveUser()의 URI로 접속해 보겠습니다.

먼저 User빈의 일부로 응답에 몇 개 링크를 추가해야 합니다. 하지만 빈에 대한 구조를 변경해서는 안됩니다. 이때 사용하는 것이 EntityModel입니다.
package study.rest.webservices.restfulwebservices.user;
import jakarta.validation.Valid;
import org.springframework.hateoas.EntityModel;
import org.springframework.hateoas.server.mvc.WebMvcLinkBuilder;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import java.net.URI;
import java.util.List;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.*;
@RestController
public class UserResource {
private UserDaoService service;
public UserResource(UserDaoService service) {
this.service = service;
}
//GET /users
@GetMapping("/users")
public List<User> retrieveAllUsers() {
return service.findAll();
}
//EntityModel
//WebMvcLinkBuilder
@GetMapping("/users/{id}")
public EntityModel<User> retrieveUser(@PathVariable int id) {
User user = service.findOne(id);
if (user == null) {
throw new UserNotFoundException("id:" + id);
}
EntityModel<User> entityModel = EntityModel.of(user);
WebMvcLinkBuilder link = linkTo(methodOn(this.getClass()).retrieveAllUsers());
entityModel.add(link.withRel("all-users"));
return entityModel;
}
@DeleteMapping("/users/{id}")
public void deleteUser(@PathVariable int id) {
service.deleteById(id);
}
//POST /users
@PostMapping("/users")
public ResponseEntity<User> createUser(@Valid @RequestBody User user) {
User savedUser = service.save(user);
URI location = ServletUriComponentsBuilder.fromCurrentRequest()
.path("{id}")
.buildAndExpand(savedUser.getId())
.toUri();
return ResponseEntity.created(location).build();
}
}
