REST API가 RESTful하지 않은 이유
우리는 웹 개발을 하면서 REST라는 것을 많이 접하게 된다.
우리는 REST를 대략 아래처럼 생각하고 있을 것이다.
- •
HTTP URI를 통해 자원을 명시한다.
- •
HTTP Method를 통해 행동을 명시한다.
- •
서버는 클라이언트가 보낸 요청에 의한 응답을 보낸다.
하지만 이건 완벽한 REST이라고 할 수 없다.
왜 그럴까?
REST API 구현 단계
REST API를 구현하는 4가지 단계가 있다.
- •
레벨 0
- ◦
HTTP 프로토콜을 사용하여 API를 구현하지만 모든 기능을 활용하지 않는다.
- ◦
리소스에 대한 고유 주소는 제공되지 않는다. (/movie/1 과 같이 고유 주소가 제공되지 않고 모든 처리가 /movie 에서 이루어지게 됨.)
- ◦
ex) method: POST / URI : /movie
- •
레벨 1
- ◦
리소스에 대한 고유 식별자가 존재한다.
- ◦
각 작업에 대한 고유 URL이 있다.
- ◦
ex) method: POST / URI : /movie/1/delete
- •
레벨 2
- ◦
HTTP 메소드를 사용한다.
- ◦
ex) method: DELETE / URI : /movie/1
- •
레벨 3
- ◦
HATEOAS 도입
우리는 4단계의 레벨 중 레벨 2를 적용하여 개발을 주로 하곤 한다.
하지만 REST API를 만든 로이 필딩은 이런 방식으로 API를 사용하면서 REST라고 할 것이라면 REST라고 하지말고 HTTP API라고 말해줄 것을 당부했다고 한다.
완벽한 RESTful API를 사용하고자 한다면 HATEOAS를 사용해야 한다.
REST API의 제약 조건
- •
Client - Server
- ◦
클라이언트는 서버에서 어떤 일을 수행하더라도 내부 작업을 알지 않아도 된다.
- ◦
클라이언트와 서버가 서로 독립적이라 별도의 개발이 가능
- •
Stateless
- ◦
클라이언트에서 서버로 보내는 각 요청에는 그 요청에 필요한 모든 정보가 포함되어야 한다.
- •
Cache
- ◦
요청에 대한 응답 내의 데이터에 해당하는 요청은 캐시가 가능한지 불가능한지 명시해야 한다.
- •
Uniform Interface
- •
등등..
이런 여러가지 제약조건들이 있는데, 대부분의 제약조건들은 HTTP API를 사용하면서 지켜지게 된다.
하지만 대부분의 경우 지켜지지 않는 것이 바로 Uniform Interface이다.
Uniform Interface
Uniform Interface는 서버와 클라이언트 간의 상호운용성을 유지하기 위한 제약조건이다.
이는 서버와 클라이언트 간의 독립적인 업데이트를 보장한다는 말과 같다.
Uniform Interface에는 또다시 4가지 제약 조건이 존재한다.
- •
Resource-Based
- •
Manipulation Of Resources Through Representations
- •
Self-Descriptive Message
- •
Hypermedia As The Engine Of Application State
영어로 하니까 아주 생소할 것이고 처음보는 것들이라고 생각할 것이다.
여기서 첫번째와 두번째를 정리해서 표현한다면
URI로 지정한 리소스를 HTTP method를 통해 표현하고 구분한다.
라고 정리할 수 있다.
예를 들자면 위에서 봤듯
HTTP method : DELETE
URI : movie/1
이런거다.
그럼 3, 4번은 무슨말일까?
Self-Descriptive Message
말그대로 스스로 설명할 수 있어야 하는 것이다.
즉, 메세지의 모든 요소를 메세지만 보고도 그 뜻을 알 수 있어야 하는 것이다.
만약 요청에 대한 응답이 아래와 같이 온다면
{"id" : 1, "name" : "영남대역"}
해당 응답이 정상적인 처리에 대한 응답인지, 오류가 발생한 것인지 모르는 상황에서 해석을 할 수 있겠는가?
따라서 우리는 상태를 명시해준다.
또한, 우리는 대충 보면 json 형식이네~ 하지만 기계는 바로 해줄 수 없기 때문에 어떤 형식의 응답인지도 명시해주어야 한다,
따라서 우리는 보통 응답을 보낼 때 아래와 같이 보낸다.
HTTP/1.1 200 OK
Content-Type: application/json
{"id" : 1, "name" : "영남대역"}
하지만 이것도 Self-Descriptive 조건을 만족하지 않아서 결국 RESTful 하지 않은 설계인 것이다.
우리는 id, name이 무엇을 의미하는지 명시해야 한다.
이를 위해 우리는 link 헤더에 Swagger라던지 다양한 방식을 통해 명세를 확인할 수 있는 링크를 넣어 응답을 보낼 수 있을 것이다.
이를 통해 서버가 응답 데이터를 변경하더라도 클라이언트는 해당 API 문서를 통해 어떤 데이터가 변경되었는지 쉽게 알 수 있는 것이다.
Hypermedia As The Engine Of Application State
Hypermedia As The Engine Of Application State는 우리가 위에서 몇번 봤던 HATEOAS(헤이티오스)이다.
이를 통해 API를 실제로 RESTful하게 되는 제약 조건이라고 한다.
해당 조건을 쉽게 표현하자면 Hypermedia(링크)를 통해 어플리케이션의 상태 전이가 가능해야 한다.이다.
기본적으로 우리는 클라이언트의 요청의 응답에 대한 데이터만 클라이언트로 보낸다.
하지만 HATEOAS를 사용하면 응답에 대한 데이터뿐만 아니라 해당 데이터와 관련된 요청에 필요한 URI를 응답에 포함하여 반환한다.
이를 통해 클라이언트가 서버와 동적인 상호작용이 가능하도록 해주는 것이다.
예를 들어 게시물을 조회하는 URI가 있다.
GET https://blog.com/articles
여기서 사용자가 해당 게시물을 조회한 뒤 어떤 행동을 하게 될까?
- 1
다음 게시물 조회
- 2
게시물 좋아요
- 3
댓글 달기
등등이 있을 것이다.
우리는 이런 행동들에 대한 URI를 응답 본문에 넣어줘야 한다는 것이다.
그럼 응답은 아래와 같이 작성할 수 있다.
{
"data" : {
"id" : 1,
"name" : "게시물 입니다,",
"content" : "내용입니다.",
"self" : "http://localhost:8080/articles/1", //현재 주소
"profile" : "http://lcalhost:8080/swagger-ui/index.html#/" //api 문서 주소
"next" : "http://localhost:8080/articles/2", //다음 게시물 주소
"comment" : "http://localhost:8080/articles/1/comment", //댓글 주소
"like" : "http://localhost:8080/articles/1/like" //좋아요 주소
},
}
이 방식을 통해 우리는 링크를 통해 상태전이를 쉽게할 수 있다.
즉, 링크가 변경되더라도 클라이언트에서 일일이 수정해줄 필요가 없는 것이다.
하지만 이 응답은 아직 완벽하지 않다,
우리는 HAL (Hypertext Application Language)를 활용해서 완벽하게 구현할 수 있다.
해당 기능은 JSON, XML 코드 내에 외부 리소스에 대한 링크를 추가하기 위한 특별한 데이터 타입이다.
HAL 타입은 리소스와 링크로 나뉘어 지고, 이를 통해 응답 본문과 링크를 나눌 수 있다.
{
//리소스
"id" : 1,
"name" : "게시물 입니다,",
"content" : "내용입니다.",
//링크
"_links" : {
"self" : {
"href" : "http://localhost:8080/articles/1" //현재 주소
},
"profile" : {
"href" : "http://lcalhost:8080/swagger-ui/index.html#/" //api 문서 주소
},
"next" : {
"href" : "http://localhost:8080/articles/2", //다음 게시물 주소
},
"comment" : {
"href" : "http://localhost:8080/articles/1/comment", //댓글 주소
},
"like" : {
"href" : "http://localhost:8080/articles/1/like" //좋아요 주소
},
},
}
HATEOAS의 장단점
- •
장점
- ◦
클라이언트와 서버 간의 결합도가 낮아지고 유연한 상호작용이 가능하다.
- ◦
클라이언트의 API 이해도가 상승한다.
- ◦
API 변경에 더 유연하게 대응할 수 있다.
- •
단점
- ◦
구현하고 관리하는 데에 추가적인 복잡성을 초래한다.
- ◦
오버헤드가 생기긴한다.
스프링에서 써보자
스프링에서 사용하기 위해서는 아래의 HATEOAS의 의존성을 추가해야 한다.
dependencies {
...
implementation 'org.springframework.boot:spring-boot-starter-hateoas:2.6.6'
...
}
public class ArticleController {
...
@GetMapping("/articles/{articleId}")
public ResponseEntity<?> getArticle(@PathVariable Long articleId) {
ArticleRes article = articleService.getArticle(articleId);
EntityModel entityModel = EntityModel.of(article,
LinkTo(methodOn(ArticleController.class).getArticle(articleId)).withSelfRel(),
linkTo(methodOn(ArticleController.class).getArticle(articleId+1)).withRel("next")
);
return ResponseEntity.ok(entityModel);
}
}
이런식으로 하면 아래와 같은 결과를 받을 수 있다.
{
//리소스
"id" : 1,
"name" : "게시물 입니다,",
"content" : "내용입니다.",
//링크
"_links" : {
"self" : {
"href" : "http://localhost:8080/articles/1" //현재 주소
},
"next" : {
"href" : "http://localhost:8080/articles/2", //다음 게시물 주소
},
},
}