REST API HATEOAS 구현

Soo·2024년 3월 26일

HATEOAS(Hypermedia As The Engine Of Application State)

클라이언트는 서버로부터 받은 응답에 포함된 하이퍼미디어 링크를 통해 상태 전이를 수행할 수 있어야 합니다. 즉, 클라이언트는 서버로부터 받은 응답을 통해 애플리케이션의 상태를 파악하고 상호작용할 수 있는 링크 정보를 동적으로 받아들이게 됩니다.

REST API를 향상하여 데이터를 반환할 뿐만 아니라 리소스에 관한 작업을 수행하는 방법의 정보를 제공하면 어떨까요?

{
  "name": "example",
  "birthDate": "2023-01-12",
  "_links": {
    "all-users": {
      "href": "http://localhost:8080/users"
    }
  }
}

여기서는 응답으로 name, birthDate가 있습니다. 또한, links는 후속 작업을 수행하는 방법을 소비자에게 알려줍니다.

이렇게 사용자가 사용하기 편한 REST API 설명서가 있습니다. 우리는 이런 설명서는 몇 가지 옵션으로 작성할 수 있습니다.

  1. 제공자가 직접 형식을 지정하고 생성합니다. 이는 유지 관리하기 매우 까다로운 옵션입니다.
  2. 표준 형식 ⇒ HAL(JSON Hypertext Application Language) : ****HAL은 JSON 형식을 기반으로 하며, 클라이언트가 서버로부터 리소스와 리소스 간의 관계를 이해하고 상호작용할 수 있도록 돕는 목적으로 설계되었습니다.

위와 같은 형식을 HAL을 사용하여 지정한다면 스프링 HATEOAS를 사용하면 됩니다.

지금부터 스프링 HATEOAS와 HAL을 구체적으로 알아보겠습니다.

Pom.xml

  • hateoas 의존성 추가
<!-- 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>
        <!--&lt;!&ndash; Swagger &ndash;&gt;
        <dependency>
            <groupId>org.springdoc</groupId>
            <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
            <version>2.4.0</version>
        </dependency>
        &lt;!&ndash; XML &ndash;&gt;
        <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로 접속해 보겠습니다.

  • 특정 사용자의 세부 사항이 반환됩니다. 이제 데이터 반환 뿐만 아니라, 사용자에게 링크를 반환해야합니다.
  • 데이터와 링크를 반환하기 위해서 알아두어야 할 HATEOAS의 두 가지 개념이 있습니다.
  • EntityModel과 WebMvcLinkBuilder입니다.

먼저 User빈의 일부로 응답에 몇 개 링크를 추가해야 합니다. 하지만 빈에 대한 구조를 변경해서는 안됩니다. 이때 사용하는 것이 EntityModel입니다.

HATEOAS를 사용하여 링크 추가하기

  1. EntityModel에서 사용자를 래핑하기
  2. WebMvcLinkBuilder 사용해서 Link생성
  3. EntityModel에 Link 추가

UserResource

  • retrieveUser() 래핑
  • EntityModel.of(T content)를 사용하여 콘텐츠 기반으로 EntityModel 객체를 생성할 수 있습니다.
  • WebMvcLinkBuilder를 사용해서 현재 클래스에 있는 메소드의 URI를 가져와서 반환합니다.
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();
    }
}

0개의 댓글