이 글을 쓰는 이유
고민
- 저는 현재 회사에서 msa구조를 갖고 서비스를 개발중입니다.
- 새로운 기능을 개발하는 도중 자원을 표현하는 계층을 어떻게 표현해줘야 할까?라는 고민을 했습니다.
- 본문 하단에 고민에 대한 테스크와 결과를 적어두었습니다.
1. Restful api가 나온 이유
- 로이 필딩은 HTTP의 주요 저자 중 한 사람으로 그 당시 웹(HTTP) 설계의 우수성에 비해 제대로 사용되어지지 못하는 모습에 안타까워하며 웹의 장점을 최대한 활용할 수 있는 아키텍처로써 REST(Representational State Transfer)를 발표했습니다.
- Rest를 직역하면 자원(리소스)의 표현에 의한 상태 (정보) 전달이라고 해석할 수 있습니다.
- 조금 풀어서 해석하면 웹에 존재하는 모든 자원에 고유한 URL을 부여하여 활용하는 것을 의미합니다.
2. Restful api란?
- REST 아키텍처 스타일을 따르는 API를 REST API라고 합니다.
- REST 아키텍처를 구현하는 웹 서비스를 RESTful 웹 서비스라고 합니다.
- 그렇기에 Restful한 api와 Rest api는 같은의미로 사용할 수 있습니다.
3. Rest 아키텍쳐 스타일
1. URI는 자원의 위치를 표현한다.
- id가 1인 아이템을 조회한다.
# good
GET /items/1
#bad
GET /my-item/listup/1
2. HTTP Method (GET, POST, PUT, DELETE 등)를 사용하여 자원에 대한 행위를 표현합니다.
# 아이템을 등록한다
POST /items
body {"title":"초코 아이스크림", "price": 1000, "type":"snack"}
#1번 아이템을 가져온다
GET /items/1
#snack타입인 아이템들을 가져온다
GET /items?type=snack
#1번 아이템의 가격을 바꾼다
PATCH /items/1
{"price":1200}
#1번 아이템의 title과 type을 바꾼다
PUT /items/1
{"title": "해리포터와 마법사의 돌", "price":1000, "type":"book"}
#1번 아이템을 DB에서 지운다
DELETE /items/1
위의 설계대로라면 Entity는 아래처럼 유추해볼 수 있습니다
class Item(
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long = 0L,
val title: String = "",
val price: Int,
val type: ItemType,
)
enum class ItemType{
BOOK, SNACK ... ;
}
2-1 Put vs Patch
Put
- 요청을 일부분만 보낸 경우 나머지는 default 값으로 수정되는 게 원칙으로 바뀌지 않는 속성도 모두 보내야 합니다.
- 그렇기 때문에 해당 api를 사용하는 클라이언트는 resource의 상태(값)를 알고 있어야합니다.
- 해당 resource에 값이 없으면 요청 받은 body로 create해줘도 무방합니다.
- 그렇기에 1번을 요청하든 100번을 요청하든 멱등성이 보장됩니다.
Patch
- 하나의 resource에 하나의 property를 변경합니다.
- 멱등성을 보장하지 않습니다.
4. 고민한 테스크
4-1 문제
- msa구조로 Accounts서버와 Products서버가 나눠져있습니다.
- 쇼핑몰로 예를들면 유저가 좋아요를 클릭한 아이템과 조회한 아이템에 대한 정보를 Accounts서버에 저장해주어야 합니다.
- 클라이언트의 정렬 조건등에 의해 아이템의 type을 같이 저장해주어야 합니다.
4-2 접근
- 확장 가능성을 고려하여 테이블은 좋아한 아이템, 조회한 아이템을 따로 설계를 했습니다.
- 하나의 테이블에서 관리하지 않고, 두개의 테이블에서 관리하기 때문에 api도 분리했습니다.
- 하나의 테이블로 관리하지 않았기 때문에 api에서도 다른 데이터라는 사실을 분명히 드러냈습니다.
4-3 api 스펙 작성
* header authorization Bearer ey~~~~~ -> 모든 요청에 JWT토큰을 사용해서 유저 정보를 식별함 (생략)
#사용자가 아이템을 조회했을 때 최근 본 항목에 등록한다
POST /accounts/viewed-items
body {"itemId":1, "type":"snack"}
#사용자가 아이템에 대해서 좋아요를 클릭했을때 좋아한 목록에 등록한다
POST /accounts/liked-items
body {"itemId":1, "type":"snack"}
#사용자가 좋아한 아이템을 수정한다
PUT /accounts/liked-items
body {"itemId":1, "type":"book"}
#사용자가 좋아한 아이템들을 조회한다
GET /accounts/liked-items
#사용자가 좋아한 아이템을 제거한다
DELETE /accounts/liked-items/1
위의 스펙을 보고 아래와 같은 entity를 유추해볼 수 있습니다.
class UserLikedItem(
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long = 0L,
val userId: Long = 0L,
val itemId: Long = 0L,
val itemType: ItemType = UNKNOWN,
val likedAt: LocalDateTime = LocalDateTime.now(),
)
enum class ItemType{
UNKNOWN, BOOK, SNACK ... ;
}
4-4 스펙 고민
1) 논리적 경로
- liked-items와 viewd-items는 논리적으로는 items라는 폴더로 묶어도 괜찮지 않을까?
- 그렇다면 아래처럼 가능하지 않을까?
GET /accounts/items/liked-items
GET /accounts/items?type=liked
결정: 논리적으로는 그럴 수 있지만, 실제 데이터의 위치를 표현하는게 설계를 직관적으로 꼬지않고 하는것이라고 생각해서 4-3처럼 작성했습니다.
2) Post
- 아래처럼 POST의 URI 무언가를 식별하는 키를 같이 넣어주면 안되는가? 라고 생각했지만 이는 잘못된 설계임을 알게 되었습니다.
- URI는 데이터를 식별하는 정보로 사용되어야 합니다.
- URI의 정보를 활용해서 데이터를 등록하면 안됩니다.
# bad
POST /accounts/liked-items/1
{"type":"book"}
# good
POST /accounts/liked-items
{"itemId":1, "type":"book"}
결정:
- 4-3처럼 등록했습니다
- 또한 1번 아이템에 대해 멱등성을 보장해야한다면 POST가 아닌 PUT이 올바르다고 생각합니다.
5. 추가적인 생각
- 모든 api를 restful 하게 가져갈 수 없지만, restful하게 가져갈 수 있는 api라면 최대한 아키텍처를 지켜주는게 좋다고 생각합니다.
- restful api가 나온 이유에 대해 생각해보고, 클라이언트 개발자가 api 문서만 보고도 어떤 행위를 하는 api인지 파악할 수 있다면 잘 작성한 문서라고 생각합니다.
- 추가적으로 독자님의 비슷한 경험이나 판단 근거에 대해서 댓글 달아주시면 정말 감사하겠습니다!
위키피디아에 더 자세한 추가 설명이 나와있습니다.
'개발 > 설계' 카테고리의 다른 글
Key-Value-Store 만들면서 겪은 이슈 (0) | 2023.08.20 |
---|