본문 바로가기
Backend/Spring

Spring : HATEOAS에 대해서

by 코딩쥐 2024. 11. 9.

HATEOAS(Hypermedia As The Engine Of Application State)는 클라이언트와 서버 간의 상호작용을 동적으로 할 수 있도록 돕는 방식이다. 단순히 데이터만 포함하는 것이 아니라, 이 데이터와 관련된 추가적인 액션을 할 수 있는 하이퍼링크를 포함하여 해당 하이퍼링크를 따라가 애플리케이션 상태를 변화시키는 역할을 한다.

 

HATEOAS시작하기

1. Dependencies에 Spring HATEOAS를 추가한다. 

	implementation 'org.springframework.boot:spring-boot-starter-hateoas'

 

2. 보낼 데이터 객체를 생성한다. 

레코드 타입의 UserDTO를 생성한다. 

package com.example.exercise1.dto;

public record UserDTO (String name, String id) {
}

 

3. Controller에서 HATEOAS를 적용한다. 

EntityModel을 통해 데이터 객체를 감싸 전송하고자하는 데이터와 함께 하이퍼링크를 함께 전송한다. WebMvcBuilder는 HATEOAS에서 하이퍼링크를 생성하는데 사용되는 클래스이다. REL(Relationship)은 링크가 나타내는 관계의 의미를 설명하는데 사용된다.  무엇을 위한 링크인지 설명하는 식별자이다.

package com.example.exercise1.controller;


import com.example.exercise1.dto.UserDTO;
import org.springframework.hateoas.EntityModel;
import org.springframework.hateoas.server.mvc.WebMvcLinkBuilder;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/user")
public class UserController {
    @PostMapping("/example")
    //EntityModel로 UserDTO를 감싸 리턴한다.
    public EntityModel<UserDTO> example(@RequestBody UserDTO userDTO){
        EntityModel<UserDTO> entityModel =
                //객체의 인스턴스를 생성하여 EntityModel로 감싼다.
                EntityModel.of(userDTO);

        //함께 보내줄 하이퍼링크를 설정한다
        WebMvcLinkBuilder linkTo = WebMvcLinkBuilder.linkTo(
                // 설정한 요청이 들어오면, 해당하는 메서드를 찾는다.
                WebMvcLinkBuilder.methodOn(this.getClass()).subexample());

        //linkTo의 식별자를 subexample라고 설정한다.
        entityModel.add(linkTo.withRel("subexample"));

        //EntityModel을 리턴한다.
        return entityModel;
    }

    @GetMapping("/subexample")
    public ResponseEntity<String> subexample(){
        return new ResponseEntity<>("반갑습니다.", HttpStatus.OK);
    }

}

 

아래 이미지처럼 바디에 해당 내용을 담아서 전송을 하면, 하이퍼링크를 포함시켜 전송해준다. subexample 링크를 클릭하거나 해당 URL을 호출하면, "반갑습니다."라는 메시지를 포함한 응답을 받을 수 있다. 

 

EntityModel을 생성할 때 EntityModel.of에 보낼 객체와 하이퍼링크를 한번에 작성할 수도 있다.

EntityModel<UserDTO> entityModel2 = EntityModel.of(
	userDTO, 
	WebMvcLinkBuilder.linkTo(WebMvcLinkBuilder.methodOn(this.getClass()).subexample()).withRel("subexample"));

 

HATEOAS 로직 축약하기

위의 로직의 경우 WebMcvLinkBuilder에서 linkTo와 methodOn을 import받아서 축약시킬 수 있다.

package com.example.exercise1.controller;


import com.example.exercise1.dto.UserDTO;
import org.springframework.hateoas.EntityModel;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn;

@RestController
@RequestMapping("/user")
public class UserController {
    @PostMapping("/example")
    public EntityModel<UserDTO> example(@RequestBody UserDTO userDTO){
        //linkTo와 methodOn을 import받아서 축약해서 사용이 가능하다. 각각의 식별자를 first와 second로 설정했다.
        return EntityModel.of(
                userDTO,
                linkTo(methodOn(this.getClass()).ex1()).withRel("first"),
                linkTo(methodOn(this.getClass()).ex2()).withRel("second"));
    }

    @GetMapping("/ex1")
    public ResponseEntity<String> ex1(){
        return new ResponseEntity<>("첫번째 예제로 들어왔습니다.", HttpStatus.OK);
    }

    @PostMapping("/ex2")
    public ResponseEntity<String> ex2(){
        return new ResponseEntity<>("두번째 예제로 들어왔습니다.", HttpStatus.OK);
    }
}

 

아래 사진을 확인하면 "first"로 해당 ex1()의 하이퍼링크가 그리고 "second"로 ex2()의 하이퍼링크가 온 것을 확인할 수 있다. 

 


HATEOAS의 CollectionModel

EntityModel의 경우 단일 객체를 감싸서 보내는 것은 가능하지만 복수의 객체를 담아서 리턴하는 것이 불가능하다. 복수의 객체를 담아서 리턴하기 위해서는 HATEOAS에서 제공하는 CollectionModel 클래스를 사용해야한다. CollectionModel로 데이터를 생성할 경우에는 "_embedded"와 "_links"로 나뉘어, "_embedded"는 실제 데이터 객체를 포함하고 "_links"는 리소스와 관련된 하이퍼링크를 포함한다.

 

아래의 예시는 EntityModel 타입의 객체의 list를 생성하고, 각각의 데이터들은 현재 리소스를 나타내는 하이퍼링크(self)를 포함하여 데이터를 리턴한다. 

package com.example.exercise1.controller;

import com.example.exercise1.dto.UserDTO;
import org.springframework.hateoas.CollectionModel;
import org.springframework.hateoas.EntityModel;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.ArrayList;
import java.util.List;

import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn;

@RestController
@RequestMapping("/users")
public class UserController {

    @GetMapping
    public CollectionModel<EntityModel<UserDTO>> getAllUsers() {
        // 여러 사용자의 데이터 리스트 생성한다.
        List<EntityModel<UserDTO>> userlist = new ArrayList<>();

        // 리스트 안의 데이터는 EntityModel 객체이다.
        userlist.add(EntityModel.of(new UserDTO("코딩쥐", "codingji"),
                linkTo(methodOn(this.getClass()).getUser("codingji")).withSelfRel()));
        userlist.add(EntityModel.of(new UserDTO("임꺽정", "thiefIm"),
                linkTo(methodOn(this.getClass()).getUser("thiefIm")).withSelfRel()));
        userlist.add(EntityModel.of(new UserDTO("나하나", "onlyme"),
                linkTo(methodOn(this.getClass()).getUser("onlyme")).withSelfRel()));

        // CollectionModel을 생성하여 리스트를 감싸고 자기자신을 가리키는 링크를 추가한다.
        return CollectionModel.of(userlist);
    }

    @GetMapping("/{id}")
    public ResponseEntity<String> getUser(@PathVariable("id") String id) {
        return new ResponseEntity<>(id + "님 반갑습니다.", HttpStatus.OK);
    }
}