Web API design best practices - Azure Architecture Center
출근 길에 지하철에서 마이크로소프트의 API 디자인 관련 게시글을 읽던 중 어디서 본 기억이 있는 익숙한 응답 형식이 눈에 들어왔다.
{
"orderID": 3,
"productID": 2,
"quantity": 4,
"orderValue": 16.60,
"links": [
{
"rel": "product",
"href": "https://adventure-works.com/customers/3",
"action": "GET"
},
{
"rel": "product",
"href": "https://adventure-works.com/customers/3",
"action": "PUT"
}
]
}
links
필드를 어디서 본 적이 있는 것 같아 내가 모르는 어떠한 응답 형식이 있는건가? 라는 궁금증을 갖고 여기저기 찾아보고 알게 된 내용을 정리했다.
REST(Representational State Transfer) API는 웹 기반의 소프트웨어 아키텍처 스타일 중 하나로, 클라이언트와 서버 간의 통신을 위한 인터페이스를 제공한다.
RESTful API는 자원(Resource)을 URI(Uniform Resource Identifier)로 표현하고, HTTP 메소드(GET, POST, PUT, DELETE 등)를 사용하여 해당 자원을 CRUD(Create, Read, Update, Delete) 작업하는 방식으로 동작한다.
또한 REST API는 상태(State)를 유지하지 않으며, 요청에 대한 응답은 클라이언트가 요청한 리소스의 상태 정보만을 포함하도록 한다.
이러한 특징들은 RESTful API가 단순하고 확장성이 뛰어나며, 다양한 클라이언트와 서버 간의 상호작용을 용이하게 만들어준다.
구성 요소 | 내용 |
---|---|
자원 | URI를 통해 표현되며, 클라이언트가 요청하는 대상입니다. 대표적으로 /users, /products 등이 있다. |
메소드 | 해당 자원에 대한 작업을 정의한다. GET은 해당 리소스의 조회 , POST는 새로운 리소스를 생성하는 작업을 수행한다. |
표현 | 클라이언트와 서버 간에 교환되는 데이터의 형식을 의미한다. 일반적으로 JSON, XML 등을 사용한다 |
HTTP 프로토콜의 구성 요소를 잘 사용해 만든 API 설계 방식이다.
나는 백엔드 개발을 하며 아래의 REST 설계 규칙을 지켰다.
https://velog.com/ing9990/blog : GET
https://velog.com/ing9990/blog/123/comments : POST
https://velog.com/ing9990/blog/123/comments/123 : DELETE
https://velog.com/ing9990/get-blog
https://velog.com/ing9990/post-blog/get_comment?bid=123
https://velog.com/ing9990/delete-blog?bid=123
응답 코드 | 응답 내용 |
---|---|
201 | 리소스의 생성을 응답할 때 사용함 |
202 | 서버의 latency를 고려해 Async로 처리한 경우 요청은 받아들여졌지만 처리가 완료되지 않았을 경우에 사용했음. |
204 | 성공에 대한 응답이지만 본문이 없을 경우 (보통 DELETE 요청에 많이 사용했던것 같음) |
404 | 리소스가 존재하지 않은 경우에 사용함. |
400 | 요청 본문이 잘못되었을때 사용했음. JSON 객체가 이상하거나 API Spec에 어긋나는 요청을 했을 경우 |
내가 RESTful API를 잘 이해하고 설계했다고 생각했지만 아래의 글을 읽고 그 생각이 틀렸음을 알게되었다.
REST의 주요 동기 중 하나는 URI에 대한 사전 지식 없이 전체 리소스를 탐색할 수 있어야 한다는 것이다.
즉 스펙이나 문서를 제공하지 않아도 클라이언트에서 전체 리소스 세트를 탐색할 수 있다.
link
필드는 현재(self)와 관계(rel)된 API의 엔드포인트를 응답해 클라이언트가 리소스 세트를 탐색할 수 있도록 만든 것이다.
{
// 응답
"orderID": 3,
"productID": 2,
"quantity": 4,
"orderValue": 16.60,
// 연관 API의 엔드포인트
"links": [
{
"rel": "product",
"href": "https://adventure-works.com/customers/3",
"action": "GET"
},
{
"rel": "product",
"href": "https://adventure-works.com/customers/3",
"action": "PUT"
}
]
}
문제는 내가 만들어온 서버를 포함한 RESTful API라 소개된 많은 백엔드 서버들이 이러한 규격을 따르지 않고 있다.
예시로 금융결제원에 REST API를 보자.
https://openapi.openbanking.or.kr/oauth/2.0/authorize_account
https://openapi.openbanking.or.kr/v2.0/account/update_info
_ 언더바
가 포함된다. update
라는 행위가 포함된다.HATEOAS (Hypermedia as the Engine of Application State)
는 REST 아키텍처 스타일의 하나로, RESTful API의 구성 요소 중 하나이다.
즉, RESTful API를 사용할 때 클라이언트가 서버로 요청을 보내면, 서버는 그에 대한 응답으로 데이터 뿐만 아니라 해당 데이터와 관련된 하이퍼미디어 링크를 함께 반환한다.
이 링크는 클라이언트가 다음에 어떤 요청을 보내야 하는지를 알려주는 정보를 포함하고 있다.
예를 들어, 은행 API에서 계좌 조회를 요청하면, 서버는 계좌 정보를 반환하는 동시에 해당 계좌의 거래 내역 조회를 위한 URI 링크를 함께 반환한다.
이 링크를 클릭하면 클라이언트는 새로운 요청을 보내어 거래 내역을 조회할 수 있다.
# 질문 API에 대한 응답
{
"status": "OK",
"data": [
{
"questionId": 1,
"authorUsername": "testuser1",
"title": "TCP와 UDP의 차이",
"detail": "TCP와 UDP의 차이를 알려주세요!!"
},
{
"questionId": 2,
"authorUsername": "testuser2",
"title": "BFS 알고리즘이 뭔가요?",
"detail": "알고리즘을 푸는데 BFS가 뭔지 모르겠어요 ㅠㅠ"
},
{
"questionId": 3,
"authorUsername": "testuser3",
"title": "파사드 패턴이 뭔가요?",
"detail": "소프트웨어 디자인 패턴이라는데 뭔지 자세히 알려주세요."
}
],
"message": ""
}
요청에 대한 응답을 잘했지만 관련된 리소스를 조회하기 위한 링크를 제공하지 않았다.
아래는 HATEOAS를 공부하고 리팩토링한 응답이다.
# 질문 API에 대한 응답
{
"content": {
"status": "OK",
"data": [
{
"questionId": 1,
"authorUsername": "testuser1",
"title": "TCP와 UDP의 차이",
"detail": "TCP와 UDP의 차이를 알려주세요!!"
},
{
"questionId": 2,
"authorUsername": "testuser2",
"title": "BFS 알고리즘이 뭔가요?",
"detail": "알고리즘을 푸는데 BFS가 뭔지 모르겠어요 ㅠㅠ"
},
{
"questionId": 3,
"authorUsername": "testuser3",
"title": "파사드 패턴이 뭔가요?",
"detail": "소프트웨어 디자인 패턴이라는데 뭔지 자세히 알려주세요."
}
],
"message": ""
},
"_links": {
"self": {
"href": "http://localhost:7878/api/questions",
"message": "질문 글 전체 조회",
"method": "GET"
},
"question&answers": {
"href": "http://localhost:7878/api/questions/0/answers",
"message": "게시글 댓글 조회",
"method": "GET"
}
}
}
만약 게시글 등록에 대한 응답이라면 아래와 같이 리팩토링할 수 있다.
{
# 응답 내용
"content": {
"status": "OK",
"data": {
"questionId": 3,
"authorName": "태우",
"site": "https://velog.com/ing9990"
}
},
# 링크
"_links": {
# 현재 호출한 API
"self": {
"href": "http://localhost:7878/question",
"method": "POST",
"message": "질문글을 등록합니다."
},
# 연관된 API
"rel": [
{
"href": "http://localhost:7878/question/3/detail",
"method": "GET",
"message": "게시글의 상세 내용을 응답합니다."
},
{
"href" : "http://localhost:7878/question/3",
"method" : "DELETE",
"message" : "게시글을 삭제합니다."
},
{
"href" : "http://localhost:7878/question/3/answer",
"method" : "POST",
"message" : "게시글에 답변을 남깁니다."
}
]
}
}
게시글을 등록했으니 클라이언트가 할 일을 예측하여 하이퍼링크를 제공합니다.
(READ)
(DELETE)
(CREATE)
이런 방식으로 설계하면 클라이언트가 리소스를 사용하면 연관된 다른 리소스를 파악할 수 있어서 REST API를 더 RESTful 하게 만들 수 있다.