우와한 개발자

[Spring] Spring HATEOAS란? – EntityModel, ModelAssembler 구현

by 우와한개발자

1. HATEOAS(Hypermedia as the Engine of Application State)란?

  • 서버가 클라이언트에게 리소스 상태와 함께 관련된 링크를 포함한 응답을 반환
  • 클라이언트 디커플링(Decoupling) 강화 : URL을 하드코딩하지 않아도 됨
  • 유지보수성 개선 : URL이 바뀌어도 클라이언트 코드 수정 불필요
  • 표준화 : API 사용법을 응답 자체에서 파악 가능
// 일반 REST 응답
{ "id": 1, "name": "홍길동" }

// HATEOAS 응답 → 다음에 할 수 있는 행동(링크)을 함께 제공
{
  "id": 1,
  "name": "홍길동",
  "_links": {
    "self":   { "href": "/v1/users/1" },
    "update": { "href": "/v1/users/1" },
    "delete": { "href": "/v1/users/1" },
    "users":  { "href": "/v1/users" }
  }
}

 

2. Spring HATEOAS 주요 클래스

클래스 설명
EntityModel<T> 단일 리소스 + 링크를 감싸는 모델
CollectionModel<T> 컬렉션 리소스 + 링크를 감싸는 모델
RepresentationModelAssembler Entity → EntityModel 변환 담당
WebMvcLinkBuilder 링크 생성 빌더 (linkTo, methodOn)

 

 

3. Spring HATEOAS 구현

의존성 추가
    ↓
ModelAssembler 생성     (Entity → EntityModel 변환 + 링크 추가)
    ↓
Controller 수정         (ModelAssembler 적용)

 

1) pom.xml

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

 

2) ModelAssembler 생성

// assembler/UserModelAssembler.java
@Component
public class UserModelAssembler
        implements RepresentationModelAssembler<UserDto, EntityModel<UserDto>> {

    @Override
    public EntityModel<UserDto> toModel(UserDto dto) {
        return EntityModel.of(dto,
                // self : 자기 자신 링크
                linkTo(methodOn(UserController.class).getUserById(dto.getId())).withSelfRel(),
                // update : 수정 링크
                linkTo(methodOn(UserController.class).updateUser(dto.getId(), null)).withRel("update"),
                // delete : 삭제 링크
                linkTo(methodOn(UserController.class).deleteUser(dto.getId())).withRel("delete"),
                // users : 전체 목록 링크
                linkTo(methodOn(UserController.class).getAllUsers()).withRel("users")
        );
    }
}

 

3) Controller 수정

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

    private final UserService userService;
    private final UserModelAssembler assembler;  //  어셈블러 주입

    // 단건 조회 → EntityModel로 감싸서 반환
    @GetMapping("/{id}")
    public ResponseEntity<EntityModel<UserDto>> getUserById(@PathVariable Long id) {
        UserDto dto = userService.getUserById(id);
        return ResponseEntity.ok(assembler.toModel(dto));  // 어셈블러 적용
    }

    // 전체 조회 → CollectionModel로 감싸서 반환
    @GetMapping
    public ResponseEntity<CollectionModel<EntityModel<UserDto>>> getAllUsers() {
        List<EntityModel<UserDto>> users = userService.getAllUsers()
                .stream()
                .map(assembler::toModel)
                .collect(Collectors.toList());

        return ResponseEntity.ok(
                CollectionModel.of(users,
                        linkTo(methodOn(UserController.class).getAllUsers()).withSelfRel())
        );
    }
}

 

4) 응답 결과

// GET /v1/users/1
{
  "id": 1,
  "name": "홍길동",
  "email": "hong@example.com",
  "_links": {
    "self":   { "href": "<http://localhost:8080/v1/users/1>" },
    "update": { "href": "<http://localhost:8080/v1/users/1>" },
    "delete": { "href": "<http://localhost:8080/v1/users/1>" },
    "users":  { "href": "<http://localhost:8080/v1/users>" }
  }
}

블로그의 정보

우와한개발자 님의 블로그

우와한개발자

활동하기