HATEOAS(Hypermedia as the Engine of Application State)

2025. 3. 3. 13:19Spring Framework/Spring HATEOAS

🌐 HATEOAS(Hypermedia as the Engine of Application State) 상세 설명

🔥 1. HATEOAS란?

HATEOAS(Hypermedia as the Engine of Application State)는 REST API의 확장 개념으로,
클라이언트가 별도의 API 문서 없이도 응답 데이터에 포함된 하이퍼링크를 사용하여 동적으로 리소스를 탐색할 수 있도록 하는 기법입니다.

 즉, API 응답에 다음 가능한 액션(링크)을 포함하여, API 사용자가 이를 보고 자연스럽게 API를 활용할 수 있도록 하는 것!

 

📌 REST API vs HATEOAS

REST API HATEOAS
요청에 대해 JSON/XML 응답 반환 응답에 추가 가능한 링크(URI) 포함
API 문서를 따로 확인해야 함 응답 자체에 추가 가능한 동작 안내
클라이언트가 특정 엔드포인트를 알고 있어야 함 클라이언트가 응답을 통해 다음 액션을 동적으로 탐색

🎯 2. HATEOAS가 필요한 이유

✅ 1) API 문서가 필요하지 않음

  • 일반 REST API는 정적 엔드포인트 목록을 기반으로 동작합니다.
  • 하지만 HATEOAS를 사용하면 응답 데이터 자체에 API 탐색 링크가 포함되므로, 클라이언트는 추가 문서 없이 API를 사용할 수 있습니다.

✅ 2) 유연한 API 확장 가능

  • API가 확장될 경우 새로운 엔드포인트를 문서화하고 배포하는 과정이 번거로움
  • 하지만 HATEOAS가 적용된 API는 서버에서 응답에 포함되는 링크만 변경하면 됨 → 클라이언트 변경이 최소화됨

✅ 3) 클라이언트와 서버의 결합도(Dependency) 감소

  • 일반적인 REST API는 클라이언트가 고정된 엔드포인트를 알고 있어야 함
  • 하지만 HATEOAS를 사용하면 서버가 제공하는 동적 링크를 기반으로 탐색 가능 → API 변경 시 클라이언트가 쉽게 적응 가능

⚙️ 3. HATEOAS 적용 방법 (Spring Boot)

Spring Boot에서는 Spring HATEOAS 라이브러리를 사용하여 쉽게 구현할 수 있습니다.

🛠 1) Spring HATEOAS 의존성 추가 (pom.xml)

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>

📝 2) HATEOAS 적용을 위한 모델 생성 (License.java)

import org.springframework.hateoas.RepresentationModel;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class License extends RepresentationModel<License> {  // HATEOAS 지원을 위해 RepresentationModel 상속
    private int id;
    private String licenseId;
    private String description;
    private String organizationId;
    private String productName;
    private String licenseType;
}

RepresentationModel<License>Spring HATEOAS에서 제공하는 모델을 상속받아 링크 추가 기능 활성화!

📝 3) 컨트롤러에서 HATEOAS 링크 추가 (LicenseController.java)

import org.springframework.hateoas.Link;
import org.springframework.hateoas.server.mvc.WebMvcLinkBuilder;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping(value = "v1/organization/{organizationId}/license")
public class LicenseController {

    @GetMapping(value = "/{licenseId}")
    public ResponseEntity<License> getLicense(
            @PathVariable("organizationId") String organizationId,
            @PathVariable("licenseId") String licenseId) {

        License license = new License();
        license.setId(1);
        license.setLicenseId(licenseId);
        license.setOrganizationId(organizationId);
        license.setDescription("Software Product");
        license.setProductName("Ostock");
        license.setLicenseType("full");

        // ✅ HATEOAS 링크 추가
        Link selfLink = WebMvcLinkBuilder.linkTo(
                WebMvcLinkBuilder.methodOn(LicenseController.class)
                        .getLicense(organizationId, licenseId))
                .withSelfRel(); // 현재 리소스에 대한 링크

        Link updateLink = WebMvcLinkBuilder.linkTo(
                WebMvcLinkBuilder.methodOn(LicenseController.class)
                        .updateLicense(organizationId, license))
                .withRel("updateLicense"); // 라이선스 업데이트 링크

        Link deleteLink = WebMvcLinkBuilder.linkTo(
                WebMvcLinkBuilder.methodOn(LicenseController.class)
                        .deleteLicense(organizationId, licenseId))
                .withRel("deleteLicense"); // 라이선스 삭제 링크

        license.add(selfLink, updateLink, deleteLink);  // ✅ HATEOAS 링크 추가

        return ResponseEntity.ok(license);
    }

    @PutMapping
    public ResponseEntity<String> updateLicense(
            @PathVariable("organizationId") String organizationId,
            @RequestBody License request) {
        return ResponseEntity.ok("License updated");
    }

    @DeleteMapping(value = "/{licenseId}")
    public ResponseEntity<String> deleteLicense(
            @PathVariable("organizationId") String organizationId,
            @PathVariable("licenseId") String licenseId) {
        return ResponseEntity.ok("License deleted");
    }
}

📥 4. HATEOAS 적용된 API 응답 예시

✅ 클라이언트가 /v1/organization/123/license/456로 GET 요청을 보냈을 때:

{
  "id": 1,
  "licenseId": "456",
  "description": "Software Product",
  "organizationId": "123",
  "productName": "Ostock",
  "licenseType": "full",
  "_links": {
    "self": {
      "href": "http://localhost:8080/v1/organization/123/license/456"
    },
    "updateLicense": {
      "href": "http://localhost:8080/v1/organization/123/license"
    },
    "deleteLicense": {
      "href": "http://localhost:8080/v1/organization/123/license/456"
    }
  }
}

📝 HATEOAS가 적용된 특징

"_links" 필드에 가능한 액션(URI)들이 포함됨
✅ 클라이언트가 "updateLicense" 링크를 확인하고, 업데이트 API 호출 가능
"deleteLicense" 링크를 참고하여 삭제 API를 호출할 수도 있음

🏆 5. HATEOAS를 사용해야 하는 경우

사용해야 하는 경우 사용하지 않아도 되는 경우
API가 동적으로 변화할 가능성이 높음 API 구조가 고정되어 있고 변화가 없음
클라이언트가 서버 문서를 참고하지 않고 자동으로 API를 탐색해야 함 클라이언트가 항상 최신 API 문서를 참고 가능
다양한 서비스가 하나의 API를 소비해야 함 특정 클라이언트만 API를 사용

🎉 6. 결론

HATEOAS는 REST API의 확장 개념으로, 클라이언트가 API 문서 없이도 동적으로 API를 탐색할 수 있도록 지원!
Spring Boot에서는 spring-boot-starter-hateoas 라이브러리를 사용해 간편하게 HATEOAS를 적용 가능
클라이언트는 API 응답 내 _links를 참고하여 자동으로 리소스 탐색 가능
API가 확장되거나 변경되더라도 클라이언트는 동적으로 적응 가능 → API의 유지보수성이 향상됨! 🚀

 

💡 다음 단계 → API 응답을 HAL 또는 JSON:API 표준 형식으로 변환하여 더욱 체계적인 API 제공! 🔥

'Spring Framework > Spring HATEOAS' 카테고리의 다른 글

WebMvcLinkBuilder  (0) 2024.12.11
_links  (0) 2024.12.11
self 링크  (0) 2024.12.09
EntityModel  (0) 2024.12.09
RepresentationModel  (0) 2024.12.09