Programming/Spring

[Spring] Hateoas(헤이티오스)란

기록하는 백앤드개발자 2023. 8. 8. 12:04
반응형

ㅁ 개요

 ㅇ Hateoas가 무엇인지 정의해 보고 Spring Boot REST API CRUD with HATEOAS Tutorial에 따란 Maven 프로젝트를 구성해 보았습니다.

 

ㅁ Hateoas란

 HATEOAS(Hypermedia As The Engine of Application State)는 웹 API를 실제로 "RESTful"로 만드는 REST 애플리케이션 아키텍처의 제약 조건입니다. 기본적으로 요청에 대해 서버는 데이터만 클라이언트에 보냅니다. HATEOAS를 사용하면 응답에 데이터뿐만 아니라 해당 데이터와 관련된 가능한 작업도 링크 형식으로 포함됩니다.

 

 

 Leonard Richardson이 제시한 REST 성숙도 모델

  이 모델은 아래에 설명된 대로 네 가지 수준으로 구성됩니다.

출처: https://grapeup.com/blog/how-to-build-hypermedia-api-with-spring-hateoas

  이 모델은 아래에 설명된 대로 네 가지 수준으로 구성됩니다.

 

  • 레벨 0
    API 구현은 HTTP 프로토콜을 사용하지만 전체 기능을 활용하지는 않습니다. 또한 리소스에 대한 고유 주소가 제공되지 않습니다.
    method: POST  URI: /movie
  • 레벨 1
    리소스에 대한 고유 식별자가 있지만 리소스에 대한 각 작업에는 고유한 URL이 있습니다.
    method: POST  URI: /movie/1/delete
  • 레벨 2
    동작을 설명하는 동사 대신 HTTP 메소드를 사용합니다. 예를 들어 URL 대신 DELETE 메소드를 사용합니다 /delete.
    method: DELETE  URI: /movie/1
  • 레벨 3
    HATEOAS라는 용어가 도입되었습니다. 간단히 말해서 리소스에 하이퍼미디어를 도입합니다. 이를 통해 가능한 작업에 대해 알려주는 응답에 링크를 배치할 수 있으므로 API를 통해 탐색할 수 있는 가능성이 추가됩니다.
    method: DELETE  URI: /movie/1

 

 

 

요즘 대부분의 프로젝트는 레벨 2를 사용하여 작성됩니다. 하지만 완벽한 RESTful API를 사용하려면 HATEOAS를 고려해야 합니다.
잘 설계된 REST API를 구현하기 위한 단계가 존재하는데 , 그 마지막 단계가 Hypermedia Controls (하이퍼미디어 컨트롤) - HATEOAS라는 개념을 통해, 자원에 호출 가능한 API 정보를 자원의 상태를 반영하여 표현하는 것입니다. 여기서 하이퍼미디어라는 용어는 텍스트, 이미지, 영화와 같은 다른 형태의 미디어에 대한 링크를 포함하는 모든 콘텐츠를 의미합니다.

 

다음은  URI /api/accounts/3에 대한 GET 요청의 응답입니다.

{
    ~~~ 데이터 영역 ~~~
	"id": 3,
	"accountNumber": "1982094128",
	"balance": 6211.0,
    ~~~ 데이터 영역 ~~~
    
    ~~~ 링크 영역 ~~~
	"_links": {
		"self": {
			"href": "http://localhost:8080/api/accounts/3"
		},
		"collection": {
			"href": "http://localhost:8080/api/accounts"
		},
		"withdrawls": {
			"href": "http://localhost:8080/api/accounts/3/withdrawal"
		}
	}
    ~~~ 링크 영역 ~~~
}

ㅇ 응답 받은 JSON에는 데이터 영역과 3개의 링크 영역으로 구분됩니다. 각각의 링크는 자기 자신의 링크와 collection(목록), withdrawls(계정탈퇴)의 기능을 제공하는 API링크입니다. 서버의 응답에포함된 링크를 따라서 리소스의 상태를 변경할 수 있습니다.

 

 

ㅁ 스프링 부트 프로젝트 설정

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>
<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>
 
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
</dependency>

HATEOAS 기반 REST API를 제공하는 웹 애플리케이션용 Java Spring Boot 프로젝트를 생성하기 위해서 Maven 프로젝트 파일 pom.xml에 위의 종속성이 포함되어야 합니다.

 

RESTful 웹 서비스를 지원하는 spring-boot-starter-web 과 JPA 리포지토리를 지원하는 spring-boot-starter-jpa 외에도 REST API용 하이퍼미디어 링크 생성을 간소화하는 spring-boot-starter-hateoas메를 사용합니다.  메모리 데이터베이스인 h2를 사용합니다.

 

 

ㅁ Account Entity

package net.codejava.account;
 
import javax.persistence.*;
 
@Data
@Entity
@Table(name = "accounts")
public class Account {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
     
    @Column(nullable = false, unique = true, length = 20)
    private String accountNumber;
     
    private float balance;
     
    public Account() { }
     
    public Account(String accountNumber, float balance) {
        this.accountNumber = accountNumber;
        this.balance = balance;
    }
 
}

 

 

ㅁ LoadDtabase

package net.codejava;
 
import java.util.List;
 
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
 
@Configuration
public class LoadDatabase {
 
    private AccountRepository accountRepo;
     
    public LoadDatabase(AccountRepository accountRepo) {
        this.accountRepo = accountRepo;
    }
 
    @Bean
    public CommandLineRunner initDatabase() {
        return args -> {        
            Account account1 = new Account("1982080185", 1021.99f);
            Account account2 = new Account("1982032177", 231.50f);
            Account account3 = new Account("1982094128", 6211.00f);
             
            accountRepo.saveAll(List.of(account1, account2, account3));
             
            System.out.println("Sample database initialized.");
        };
    }
}

 ㅇ h2 메모리 디비에 데이터 3개를 등록합니다.

 

ㅁ Account API

@RestController
@RequestMapping("/api/accounts")
public class AccountApi {
	
	private AccountService service;
	private AccountModelAssembler assembler;
	
	public AccountApi(AccountService service, AccountModelAssembler assembler){
		this.service = service;
		this.assembler = assembler;
	}	
	
	@GetMapping
	public CollectionModel<Account> listAll() {
		List<Account> listAccounts = service.listAll();
		
		for (Account account : listAccounts) {
			account.add(linkTo(methodOn(AccountApi.class).getOne(account.getId())).withSelfRel());
		}
		
		CollectionModel<Account> collectionModel = CollectionModel.of(listAccounts);
		collectionModel.add(linkTo(methodOn(AccountApi.class).listAll()).withSelfRel());
		
		return collectionModel;
	}
	
	@GetMapping("/{id}")
	public HttpEntity<Account> getOne(@PathVariable("id") Integer id) {
		try {
			Account account = service.get(id);
			
			account.add(linkTo(methodOn(AccountApi.class).getOne(id)).withSelfRel());
			account.add(linkTo(methodOn(AccountApi.class).listAll()).withRel(IanaLinkRelations.COLLECTION));
			account.add(linkTo(methodOn(AccountApi.class).withdraw(id, null)).withRel("withdrawls"));
			
			return new ResponseEntity<>(account, HttpStatus.OK);
		} catch (NoSuchElementException ex) {
			return ResponseEntity.notFound().build();
		}
	}
}

 ㅇ List와 개별 ID에 대한 조회 API입니다.

 

 ㅇ 로컬 테스트 시 JSON 응답을 받았습니다.

 

account.add(linkTo(methodOn(AccountApi.class).getOne(id)).withSelfRel());
account.add(linkTo(methodOn(AccountApi.class).listAll()).withRel(IanaLinkRelations.COLLECTION));
account.add(linkTo(methodOn(AccountApi.class).withdraw(id, null)).withRel("withdrawls"));

 ㅇ 3개의 linkTo 함수를 써서 account에 add하면 3개의 링크가 출력됩니다.

 

 

 

ㅁ HATEOAS 장점

REST API의 HATEOAS는 웹 페이지의 하이퍼링크와 같습니다. 사용자는 도메인 이름에서 웹사이트를 탐색하고 하이퍼링크를 클릭하여 하이퍼링크에 대한 사전 지식 없이 콘텐츠를 탐색할 수 있습니다. 그리고 REST 클라이언트는 API를 사용하기 위해 리소스 URI에 대한 사전 지식이 필요하지 않습니다. 응답에 포함된 링크를 트래버스하고 따를 수 있는 기본 URI만 있으면 됩니다.

 

 그리고 HATEOAS의 가장 큰 장점은 서버와 클라이언트를 분리한다는 것입니다. 클라이언트는 기본 URI가 아닌 리소스 URI에 대해 가정하지 않아야 하므로 개발자는 클라이언트 중단에 대해 걱정하지 않고 API를 업데이트하고 발전시킬 수 있습니다. 다양한 요구사항을 즉각적으로 개발 변경하여 배포하는 환경에서 클라이언트와 백엔드의 URL이라는 강결합을 느슨하게 조정하여 배포시기로 인한 장애를 만회할 수 있습니다.

 

 

ㅁ HATEOAS의 한계

 ㅇ 리소스와 URL이 분리는 되었지만 종속적인 한계가 있다.
  위에서 언급하였듯이, HATEOAS의 가장 큰 장점은 서버와 클라이언트를 분리를 하였지만 이것은 어디까지나 리소스의 URL과 클라이언트의 분리이지 확실한 이점은 아닙니다. 여전히 클라이언트는 수행해야 할 작업, 링크의 개수와 그 의미를 알아야 하고, 이를 위해 API 변경 전략은 반드시 필요합니다. 그리고 실질적으로 URL이 빈번하게 변경되지 않습니다.

 

ㅇ DDD기준으로 HATEOAS는 자기 도메인만 관여하고 전체가 아니라는 점이다.

  API의 응답을 통해 주어진 링크들은 자기 도메인에 대한 부분이지 전체가 아니라는 점입니다. 다른 도메인과 연관된 경우, 해당 API를 호출하기 이해서는 다른 도메인의 정보를 알아야만 합니다. 예를 들어 Account 정보와 연계된 카드 대급 결제 서비스가 있다고 가정했을 때에 이를 하이퍼 텍스트로 제공하기는 어려운 점이 있습니다. 그래서 API 클라이언트 개발자의 경우 BackEnd의 API 문서를 반드시 참고해야 합니다.

 

 ㅇ 결국 Swagger...

다중 도메인에 관한 API는 Swagger 문서를 보고 개발을 하는게 더 효과적이라고 판단됩니다. 개발자들에겐 아직까진 Swagger 문서나 다른 API 문서를 통해 알아야 할 정보를 공유하고 API 변경 전략을 수립하게 더 친근한 방법이라고 생각합니다.

 

 

ㅁ 샘플코드

SpringRestHATEOAS.zip
0.17MB

 이곳에서 제공한 메이븐 기반 샘플코드입니다. 

 

 

ㅁ 함께 보면 좋은 사이트

ㅇ hateoas 초기 생성 프로젝트: https://github.com/spring-guides/gs-rest-hateoas

 

ㅇ Spring HATEOAS 레퍼런스 문석: https://docs.spring.io/spring-hateoas/docs/current/reference/html/

 

ㅇ 위키피디아 설명 : https://en.wikipedia.org/wiki/HATEOAS

ㅇ 하이퍼미디어 기반 RESTful 웹 서비스 구축 : https://spring.io/guides/gs/rest-hateoas/

ㅇ 스프링 샘플코드 + 설명 https://www.codejava.net/frameworks/spring-boot/rest-api-crud-with-hateoas-tutorial

ㅇ 위 개발자 동영상 강의 : https://youtu.be/y6R3reU1vWE

[Spring] REST API - URI 디자인 가이드

반응형