HATEOAS(Hypermedia as the Engine of Application State)는 RESTful API 설계에서 클라이언트가 API의 리소스를 동적으로 탐색하고 사용할 수 있게 해주는 강력한 개념입니다. 이 포스팅에서는 HATEOAS가 무엇인지, 왜 중요한지, 그리고 어떻게 구현할 수 있는지에 대해 알아보면서 더 유연하고 유지보수하기 쉬운 API를 만드는 방법을 소개합니다.
1. HATEOAS란?
HATEOAS(Hypermedia as the Engine of Application State)는 RESTful API 설계의 중요한 원칙 중 하나로, 클라이언트와 서버 간의 상호작용에서 클라이언트가 서버의 응답 내에서 하이퍼미디어 링크를 통해 동적으로 리소스를 탐색하고 상태를 전이할 수 있도록 하는 개념입니다. 이 원칙은 REST의 창시자인 Roy Fielding이 제안한 것으로, 클라이언트가 서버에서 제공하는 링크를 통해 필요한 추가 정보나 동작을 알 수 있게 함으로써 클라이언트의 API 사전 지식 없이도 서버와 상호작용을 가능하게 합니다.
HATEOAS를 적용하면 클라이언트는 서버로부터 단순히 데이터를 받아오는 것뿐만 아니라, 해당 데이터를 기반으로 어떤 행동을 취할 수 있는지에 대한 정보도 함께 받을 수 있습니다. 예를 들어, 특정 사용자 정보를 조회하는 API 응답에 그 사용자를 수정하거나 삭제할 수 있는 링크가 포함될 수 있습니다. 이 링크들은 HTTP 메서드(GET, POST, PUT, DELETE 등)와 함께 제공되어, 클라이언트가 다음에 어떤 작업을 할 수 있는지를 명확하게 안내합니다.
2. 왜 중요한가?
- 클라이언트의 독립성 증가: HATEOAS를 사용하면 클라이언트는 서버의 내부 구조나 동작을 미리 알 필요 없이, 서버가 제공하는 하이퍼미디어를 통해 필요한 작업을 수행할 수 있습니다. 이는 API의 버전이 업데이트되거나 리소스의 구조가 변경되더라도 클라이언트가 새로운 구조에 적응할 수 있는 유연성을 제공합니다.
- API의 자가 문서화: API 응답 내에 포함된 링크와 관련된 메타데이터는 API의 사용 방법을 클라이언트에게 자연스럽게 알려줍니다. 이는 API 문서화의 필요성을 줄이고, 클라이언트가 자동으로 서버의 기능을 탐색하고 이해할 수 있게 도와줍니다.
- 응집력 있는 설계: HATEOAS를 활용한 API는 리소스와 리소스 간의 관계를 명확하게 정의하고, 이 관계를 통해 클라이언트가 어떤 흐름으로 작업을 진행할 수 있는지 명확히 안내합니다. 이는 API 설계의 일관성을 높이고, 유지보수성을 향상시킵니다.
- 동적인 클라이언트 동작: 서버가 클라이언트에게 어떤 링크를 제공하느냐에 따라 클라이언트의 동작을 유연하게 변경할 수 있습니다. 예를 들어, 사용자의 권한에 따라 다른 링크를 제공하여 권한 관리 기능을 구현할 수 있습니다.
- 분산 시스템에서의 유연성: HATEOAS는 분산 시스템에서의 상호작용을 단순화하고, 서버와 클라이언트 간의 결합도를 낮춤으로써 시스템의 확장성과 변경에 유연하게 대응할 수 있게 합니다.
HATEOAS는 RESTful API 설계에서 자주 간과되지만, 이를 효과적으로 활용하면 API의 사용성을 크게 향상시킬 수 있습니다. 이는 특히 대규모 시스템이나 다양한 클라이언트가 사용하는 API에서 더욱 유용합니다.
3. 간단한 예제
package com.api.hateoas.controller;
import com.api.hateoas.dto.*;
import com.api.hateoas.service.UserService;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.hateoas.EntityModel;
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("/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
/**
* 1. 아이디 중복체크
*/
@GetMapping("/check/{id}")
public ResponseEntity<EntityModel<DupRes>> dupCheck(@PathVariable String id) {
DupReq req = new DupReq(id);
DupRes res = userService.dupCheck(req);
EntityModel<DupRes> model = EntityModel.of(res);
// self 링크
model.add(linkTo(methodOn(UserController.class).dupCheck(id)).withSelfRel());
return ResponseEntity.ok(model);
}
/**
* 2. 회원가입
*/
@PostMapping("/join")
public ResponseEntity<EntityModel<JoinRes>> join(@RequestBody JoinReq joinReq, HttpServletRequest request) {
JoinRes response = userService.join(joinReq);
EntityModel<JoinRes> model = EntityModel.of(response);
// self 링크
model.add(linkTo(methodOn(UserController.class).join(joinReq, request)).withSelfRel());
// 로그인 링크
model.add(linkTo(methodOn(UserController.class).login(new LoginReq(), request)).withRel("login"));
return ResponseEntity.ok(model);
}
/**
* 3. 로그인
*/
@PostMapping("/login")
public ResponseEntity<EntityModel<LoginRes>> login(@RequestBody LoginReq loginReq, HttpServletRequest request) {
LoginRes response = userService.login(loginReq);
EntityModel<LoginRes> model = EntityModel.of(response);
if (response.isLogin()) {
request.getSession().setAttribute("id", response.getId());
}
// self 링크
model.add(linkTo(methodOn(UserController.class).login(loginReq, request)).withSelfRel());
// 회원수정 링크
model.add(linkTo(methodOn(UserController.class).update(null, null)).withRel("update"));
// 회원탈퇴 링크
model.add(linkTo(methodOn(UserController.class).quit(null, null)).withRel("quit"));
return ResponseEntity.ok(model);
}
/**
* 4. 회원수정
*/
@PutMapping("/mod/{id}")
public ResponseEntity<EntityModel<UpdateRes>> update(@PathVariable String id, @RequestBody UpdateReq updateReq) {
updateReq.setId(id);
UpdateRes response = userService.update(updateReq);
EntityModel<UpdateRes> model = EntityModel.of(response);
// self 링크
model.add(linkTo(methodOn(UserController.class).update(id, updateReq)).withSelfRel());
// 회원가입 링크
model.add(linkTo(methodOn(UserController.class).join(null, null)).withRel("join"));
return ResponseEntity.ok(model);
}
/**
* 5. 회원탈퇴
*/
@DeleteMapping("/mod/{id}")
public ResponseEntity<EntityModel<UpdateRes>> quit(@PathVariable String id, @RequestBody UpdateReq quitReq) {
quitReq.setId(id);
UpdateRes response = userService.quit(quitReq);
EntityModel<UpdateRes> model = EntityModel.of(response);
// self 링크
model.add(linkTo(methodOn(UserController.class).quit(id, quitReq)).withSelfRel());
// 회원가입 링크
model.add(linkTo(methodOn(UserController.class).join(null, null)).withRel("join"));
return ResponseEntity.ok(model);
}
}
- Self 링크: 각 엔드포인트에서, 해당 엔드포인트를 다시 호출할 수 있는 "self" 링크를 응답에 포함시킵니다. 예를 들어, 아이디 중복 체크 API에서는 linkTo(methodOn(UserController.class).dupCheck(id)).withSelfRel()을 통해 현재 리소스에 대한 "self" 링크를 추가합니다. 이를 통해 클라이언트는 이 API를 다시 호출할 수 있습니다.
- 관련 리소스 링크: 각 주요 API 응답에는 관련된 다른 리소스에 대한 링크도 포함됩니다. 예를 들어, 회원가입 응답에는 로그인 링크를 포함시켜, 클라이언트가 가입 후 바로 로그인할 수 있도록 유도합니다. 이러한 링크들은 linkTo(methodOn(UserController.class).login(new LoginReq(), request)).withRel("login"))과 같이 추가됩니다.
3.1. 아이디중복체크
3.2. 회원가입
3.3. 로그인
3.4. 회원수정
3.5. 회원삭제
HATEOAS를 사용하면 클라이언트와 서버 간의 결합도를 낮출 수 있습니다. 클라이언트는 서버가 제공하는 링크를 통해서만 리소스에 접근하므로, 서버의 URI 구조가 변경되더라도 클라이언트 코드를 수정할 필요가 없습니다. 이는 시스템 확장성과 유지보수성 측면에서 큰 장점이 됩니다.
전체예제소스는 Github 를 참고해주세요.
감사합니다.
'프레임워크 > SpringBoot' 카테고리의 다른 글
[SpringBoot] Apache Kafka 간단한 예제만들기 (0) | 2024.09.19 |
---|---|
[SpringBoot] 공공데이터포탈 IP 정보검색 구현 (5) | 2024.08.30 |
[SpringBoot] openNLP를 이용해 고객센터 챗봇 만들기 (0) | 2024.08.20 |
[SpringBoot] 외장톰캣 배포 404 오류 (0) | 2024.08.08 |
[SpringBoot] Thymeleaf를 이용한 카카오톡 클론 코딩 - 3 (0) | 2024.08.07 |