반응형
🌱 HATEOAS란?
HATEOAS(Hypermedia As The Engine Of Application State)란 REST API를 통해 클라이언트와 서버 간에 동적인 상호작용을 가능케 하는 기술을 의미한다. 예를 들어, 클라이언트가 사용자를 조회했을 때 사용자 정보만 응답하는 것이 아니라 '사용자 수정', '사용자 삭제' 등을 처리할 수 있는 링크를 같이 포함하여 응답하는 것을 예시로 들 수 있다.
🌱 HATEOAS 사용 예시
앞서 설명한 내용을 토대로 실제 응답값이 어떻게 변화하는지 알아보자.
특정 사용자를 조회한다고 가정했을 때, HATEOAS를 적용하기 전에는 다음과 같이 사용자 정보만을 응답하게 된다.
{
"resultCode": "0000",
"resultData": {
"memberId": 1,
"name": "테스터1",
"birth": 19920101
}
}
만약 HATEOAS를 적용하게 된다면 다음과 같은 응답을 받을 수 있으며, 이후의 동작을 유추할 수 있게 된다.
{
"resultCode": "0000",
"resultData": {
"memberId": 1,
"name": "테스터1",
"birth": 19920101
},
"_links": {
"list": {
"href": "http://localhost:8080/members",
"type": "GET"
},
"self": {
"href": "http://localhost:8080/members/1",
"type": "GET"
},
"update": {
"href": "http://localhost:8080/members/1",
"type": "PUT"
},
"delete": {
"href": "http://localhost:8080/members/1",
"type": "DELETE"
}
}
}
🌱 HATEOAS 장단점
HATEOAS를 적용시킴으로써 얻을 수 있는 장점과 단점은 다음과 같다.
- 장점
- 다음 동작을 유추 가능하다.
- 서버에서 URI를 변경해도 클라이언트단에서는 영향이 없다.
- 클라이언트단에서 link가 있느냐 없느냐에 따라서 UI를 유기적으로 구성할 수 있다. (Ex. link가 없으면 버튼을 disabled) - 단점
- 전달되는 데이터가 커져서 네트워크 오버헤드가 발생한다.
- 링크 구성과 관련하여 코드가 지저분해질 수 있다.
🌱 HATEOAS 적용
1. 의존성 설정
<dependencies>
...
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>
...
</dependencies>
2. 도메인 및 공통 응답 객체
// 사용자 Domain
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long memberId;
private String name;
private Integer birth;
@Builder
private Member(Long memberId, String name, Integer birth) {
this.memberId = memberId;
this.name = name;
this.birth = birth;
}
}
// 공통 응답 객체
import lombok.Builder;
import lombok.Getter;
@Getter
public class ResponseData {
String resultCode;
Object resultData;
@Builder
public ResponseData(String resultCode, Object resultData) {
this.resultCode = resultCode;
this.resultData = resultData;
}
}
반응형
3. 컨트롤러
import com.web.api.domain.Member;
import com.web.api.domain.ResponseData;
import com.web.api.service.MemberService;
import lombok.RequiredArgsConstructor;
import org.springframework.hateoas.EntityModel;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.stream.Collectors;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn;
@RestController
@RequiredArgsConstructor
public class MemberController {
private final MemberService memberService;
/**
* 회원 목록 조회
*/
@GetMapping("/members")
public ResponseEntity<EntityModel<ResponseData>> findMembers() {
List<EntityModel<Member>> members = memberService.findAllMember().stream().map(member -> {
return EntityModel.of(member,
linkTo(methodOn(MemberController.class).findMember(member.getMemberId())).withRel("detail").withType("GET"));
}).collect(Collectors.toList());
return ResponseEntity.ok().body(
EntityModel
.of(ResponseData.builder()
.resultCode("0000")
.resultData(members)
.build())
.add(linkTo(methodOn(MemberController.class).findMembers()).withSelfRel())
);
}
/**
* 회원 조회
*/
@GetMapping("/members/{memberId}")
public ResponseEntity<EntityModel<ResponseData>> findMember(@PathVariable Long memberId) {
return ResponseEntity.ok().body(
EntityModel
.of(ResponseData.builder()
.resultCode("0000")
.resultData(memberService.findMember(memberId))
.build())
.add(linkTo(methodOn(MemberController.class).findMembers()).withRel("list").withType("GET"))
.add(linkTo(methodOn(MemberController.class).findMember(memberId)).withSelfRel().withType("GET"))
.add(linkTo(methodOn(MemberController.class).editMember(memberId, new Member())).withRel("update").withType("PUT"))
.add(linkTo(methodOn(MemberController.class).deleteMember(memberId)).withRel("delete").withType("DELETE"))
);
}
/**
* 회원 등록
*/
@PostMapping("/members")
public ResponseEntity saveMember(@RequestBody Member member) {
// 생략
}
/**
* 회원 수정
*/
@PutMapping("/members/{memberId}")
public ResponseEntity editMember(@PathVariable Long memberId, @RequestBody Member member) {
// 생략
}
/**
* 회원 삭제
*/
@DeleteMapping("/members/{memberId}")
public ResponseEntity deleteMember(@PathVariable Long memberId) {
// 생략
}
}
🌱 HATEOAS 적용 결과
1. 사용자 전체 조회
{
"resultCode": "0000",
"resultData": [
{
"memberId": 5,
"name": "테스터5",
"birth": 19920505,
"_links": {
"detail": {
"href": "http://localhost:8080/members/5",
"type": "GET"
}
}
},
{
"memberId": 6,
"name": "테스터66",
"birth": 19990606,
"_links": {
"detail": {
"href": "http://localhost:8080/members/6",
"type": "GET"
}
}
}
],
"_links": {
"self": {
"href": "http://localhost:8080/members"
}
}
}
2. 사용자 조회
{
"resultCode": "0000",
"resultData": {
"memberId": 6,
"name": "테스터66",
"birth": 19990606
},
"_links": {
"list": {
"href": "http://localhost:8080/members",
"type": "GET"
},
"self": {
"href": "http://localhost:8080/members/6",
"type": "GET"
},
"update": {
"href": "http://localhost:8080/members/6",
"type": "PUT"
},
"delete": {
"href": "http://localhost:8080/members/6",
"type": "DELETE"
}
}
}
References.
1. https://caffeineoverflow.tistory.com/28
반응형
'IT' 카테고리의 다른 글
[Security] Keycloak 소개 (1) | 2025.01.19 |
---|---|
[SpringBoot] Versioning (1) | 2025.01.19 |
[JPA] Caused by: org.h2.jdbc.JdbcSQLSyntaxErrorException: Column "start_value" not found (1) | 2025.01.19 |
[SpringBoot] REST API (1) | 2025.01.19 |
[SpringBoot] 모니터링 환경 구축 #3 - Grafana (1) | 2025.01.19 |