본문 바로가기
Framework/Spring Boot

[Spring Boot] Spring HATEOAS: 진정한 REST API (Level 3) 달성기

반응형

웹 개발을 하다 보면 누구나 "우리는 RESTful API를 사용합니다"라고 말합니다. HTTP 메서드(GET, POST, PUT, DELETE)를 쓰고 /users/1처럼 예쁜 URL을 쓰면 REST API라고 믿기 때문입니다. 하지만 REST의 창시자인 '로이 필딩(Roy Fielding)' 아저씨는 분노하며 "네놈들이 만든 건 진짜 REST가 아니야!"라고 일갈합니다. 로이 필딩이 말하는 완벽한 REST(Richardson Maturity Model Level 3)의 필수 조건 중 하나는 바로 HATEOAS (Hypermedia As The Engine Of Application State)입니다. 이름이 끔찍하게 길지만 핵심은 단순합니다. "서버가 클라이언트(앱/웹)에게 응답(JSON)을 내려줄 때, 그 데이터를 가지고 '다음에 할 수 있는 행동(상태 전이)'들의 URL 링크까지 통째로 응답에 포함해서 줘라!"라는 것입니다.

이번 [Spring Boot 실무 완벽 가이드] 시리즈에서는 "클라이언트를 API 문서 지옥에서 해방시키는 마법!" Spring HATEOAS를 활용한 궁극의 RESTful API 아키텍처에 대하여 다뤄보겠습니다.

🔗 1. HATEOAS의 필요성: 클라이언트 하드코딩 방지

여러분(백엔드)이 "주문 상세 조회" API를 만들었습니다.

HATEOAS 개념도

일반적인(Level 2) 가짜 REST 응답:
{ "orderId": 100, "status": "SHIPPED" }
이걸 받은 프론트엔드 개발자는 코드를 짭니다. "음, status가 SHIPPED(배송중)일 때는 환불이 불가능하니까 화면에서 '결제취소' 버튼을 숨겨야지!" (클라이언트에 비즈니스 로직이 하드코딩됨)

진정한(Level 3) HATEOAS 응답:
{ "orderId": 100, "status": "SHIPPED", "_links": { "self": { "href": "/orders/100" }, "tracking": { "href": "/orders/100/delivery" } } }
백엔드가 응답을 줄 때 결제 취소(cancel) 링크를 아예 빼버리고 배송 조회(tracking) 링크만 줍니다. 프론트엔드는 "status가 뭔지는 내 알 바 아니고, JSON 안에 'cancel' 링크가 없네? 그럼 결제취소 버튼 숨길게!"라고 반응합니다. 비즈니스 로직이 오직 서버에만 존재하게 되며, URL 주소가 바뀌더라도 클라이언트 코드를 수정할 필요가 없는 완벽한 결합도 분리(Decoupling)가 이루어집니다!


⚙️ 2. Spring HATEOAS 실전 구현

스프링 부트는 이 복잡해 보이는 링크 삽입 작업을 EntityModelWebMvcLinkBuilder라는 천재적인 도구로 쉽게 만들어 줍니다.

// 의존성 추가: implementation 'org.springframework.boot:spring-boot-starter-hateoas'

@RestController
@RequestMapping("/api/orders")
public class OrderController {

    @GetMapping("/{id}")
    public EntityModel<OrderDto> getOrder(@PathVariable Long id) {
        OrderDto order = orderService.findById(id);

        // 1. 순수한 데이터 객체를 HATEOAS용 모델로 감쌉니다.
        EntityModel<OrderDto> entityModel = EntityModel.of(order);

        // 2. 자기 자신(Self)을 조회하는 링크 추가
        // 하드코딩("/api/orders/1")이 아니라, 메서드 맵핑 정보를 역추적하여 링크를 자동 생성합니다!
        WebMvcLinkBuilder selfLink = linkTo(methodOn(OrderController.class).getOrder(id));
        entityModel.add(selfLink.withSelfRel());

        // 3. 비즈니스 상태에 따른 동적 링크 추가
        if (order.getStatus().equals("PAYMENT_COMPLETED")) {
            // 결제 완료 상태면 "주문 취소" 링크를 꽂아준다!
            WebMvcLinkBuilder cancelLink = linkTo(methodOn(OrderController.class).cancelOrder(id));
            entityModel.add(cancelLink.withRel("cancel-order"));
        }

        return entityModel;
    }
    
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> cancelOrder(@PathVariable Long id) { /* 취소 로직 */ return ResponseEntity.ok().build(); }
}

💀 3. HATEOAS의 치명적 단점: 오버엔지니어링의 경계

로이 필딩 아저씨는 기뻐하겠지만, 실무(스타트업~중견)에서 HATEOAS를 도입하는 기업은 생각보다 극소수입니다. "오버엔지니어링(Over-engineering)"이기 때문입니다. 모든 엔티티와 리스트 응답마다 링크를 만들어주는 코드를 덕지덕지 발라야 하고, JSON 응답 크기도 무거워집니다. 요즘은 Swagger(Springdoc)나 포스트맨 같은 API 문서화 툴이 너무 잘 되어 있어서 프론트엔드가 그냥 문서 보고 URL을 하드코딩하는 것이 개발 속도 측면에서 훨씬 빠르기 때문입니다.


🎯 4. 마무리 및 다음 단계

지금까지 클라이언트와 서버의 강한 결합(URL 하드코딩)을 끊어내고, 서버가 스스로 다음 상태를 지시하는 진정한 REST API 성숙도 최고 레벨(Level 3)의 경지, Spring HATEOAS의 철학과 실무 구현법에 대해 상세히 다루어 보았습니다. 비록 실무 채택률은 낮더라도, 이를 이해하고 있는 개발자는 아키텍처 토론에서 압도적인 통찰력을 뽐낼 수 있습니다.

REST API에 대한 고찰을 마쳤으니, 이번엔 REST API 자체의 치명적인 한계를 박살 내보겠습니다. 모바일 앱 팀에서 화를 냅니다. "야! 나는 유저의 '이름' 딱 하나만 화면에 그리면 되는데, 네가 만든 GET /users/1 API는 주소, 생일, 구매 내역까지 쓸데없는 1MB짜리 JSON을 통째로 다 내려주잖아! 우리 앱 데이터 요금 많이 나오면 네가 낼래?" 이 끔찍한 오버패칭(Over-fetching) 문제를 극복하기 위해, 프론트엔드가 "나 유저 이름만 필요해!"라고 쿼리(요청)하면 정확히 이름만 발라내서 던져주는 혁명적인 통신 기술이 등장했습니다. 이어지는 43단계 포스팅에서는 "REST API 시대의 종말을 선언하다!" 페이스북(Meta)이 창조한 유연함의 극치, Spring Boot와 GraphQL(그래프QL) 연동 완벽 가이드에 대해 아주 뼈 때리게 파헤쳐 보겠습니다!

반응형