resource-oriented-design
📌 구글한테 배우는 리소스 오리엔티드 디자인⚑
- 구글의 API 디자인 철학
- 구글은 API 디자인 가이드라인에 진심이며, 특히 외부에 공개하는 퍼블릭 HTTP API는 신중하게 설계한다.
- API는 한 번 공개되면 수정이 어렵기 때문에 신중한 설계가 필요하다.
- 리소스 오리엔티드 디자인 vs. 액션 오리엔티드 디자인
- 액션 오리엔티드 디자인: 특정 동작(예: 회원가입)을 함수처럼 정의하여
signup()
같은 메소드를 만든다. - 리소스 오리엔티드 디자인: HTTP 메소드(GET, POST, PATCH, DELETE)를 활용하여
POST /persons
형태로 리소스를 조작한다. - 리소스 오리엔티드 디자인은 직관적이며 표준화된 방식으로 API를 설계할 수 있다.
- 액션 오리엔티드 디자인: 특정 동작(예: 회원가입)을 함수처럼 정의하여
- 리소스 오리엔티드 디자인의 핵심 원칙
- API가 처리할 리소스(Resource) 를 정의한다.
- 리소스 간의 관계(Relationships) 를 명확히 한다.
- 리소스가 가지는 필드(Fields) 를 결정한다.
- 리소스에서 지원할 HTTP 메소드(Methods) 를 정의한다.
- 구글의 API 설계 접근 방식
- 내부적으로 수천 개의 API를 운영하며, 인터널 API(RPC 기반)는 보통 액션 오리엔티드 방식을 많이 사용한다.
- 하지만 액션 오리엔티드는 확장성이 낮고, 중복된 기능이 많아지며, 유지보수가 어려워지는 문제가 있다.
- 이를 해결하기 위해 리소스 오리엔티드 디자인을 내부 API에도 적용하려는 시도를 하고 있다.
- 결론
- 리소스 오리엔티드 디자인은 API를 일관되고 유지보수하기 쉽게 만든다.
- 구글은 내부 API에서도 이 방식을 적용하려는 노력을 기울이고 있다.
📌 실전 API 디자인 강의 | 포토 서비스 만들기 실습 - 요약⚑
- API 설계 원칙
Create
,Read
,Update
,Delete
기능을 표준 HTTP 메소드(GET, POST, PATCH, DELETE)로 구현해야 한다.- 요청(Request) 및 응답(Response) 네이밍은 일관되게 유지해야 하며,
CreateResourceRequest
,ResourceResponse
같은 명명 규칙을 따른다.
- 리소스 및 데이터 모델 정의
- 서비스 개요: 사용자가 사진(포토)을 업로드하고, 앨범을 생성 및 관리할 수 있는 API 설계.
- 리소스 스키마 설계:
- Storage Model: 데이터베이스에 저장되는 내부 모델.
- Wire Model: 클라이언트와 서버가 주고받는 API 모델.
- Storage Model과 Wire Model은 처음부터 분리해야 하며, ID 필드는 항상 String으로 정의하는 것이 구글 API 디자인 가이드라인의 베스트 프랙티스.
- API 메소드 설계 원칙
- 사진 업로드(Create Photo)
POST /photos
요청 시,CreatePhotoRequest
와Photo
를 전달.- 부모 리소스(
parent
)를 포함하여 계층 구조를 명확하게 정의. - 응답(Response)은 Photo 객체만 반환하고, 부가적인 정보를 포함하지 않도록 제한.
- 사용자의 모든 사진 조회(List Photos)
GET /photos
요청 시,ListPhotosRequest
와ListPhotosResponse
를 사용.- 페이지네이션(Pagination) 필수 지원 (클라이언트가 대량 데이터를 처리하는 방식 유지).
- 단일 사진 조회(Get Photo)
GET /photos/{photo_id}
요청 시, 특정 사진을 반환.- 구글 API 디자인 가이드에서는
name
필드를 ID로 사용하는 것을 권장.
- 사진 업로드(Create Photo)
- API 디자인 시 고려할 점
- API의 확장성과 유지보수를 위해 처음부터 Storage Model과 Wire Model을 분리해야 한다.
- 페이지네이션 지원은 필수, 초기에 필요하지 않더라도 추가하지 않으면 클라이언트 코드 변경 없이 도입하기 어렵다.
- 응답(Response)에 불필요한 정보를 포함하지 않도록 주의하여, API의 기능을 단순하고 명확하게 유지해야 한다.
- API가 한 번 공개되면 쉽게 변경할 수 없으므로 Backward Compatibility를 유지하는 것이 중요하다.
✅ 결론
구글 API 디자인 가이드라인을 따르면, 유지보수성과 확장성을 고려한 API를 설계할 수 있으며, 클라이언트가 API를 안정적으로 사용할 수 있도록 도울 수 있다.
**📌 Storage Model vs. Wire Model 개념 정리**
1️⃣ Storage Model (저장 모델)⚑
- 서버 내부에서 데이터베이스(DB)에 저장하는 데이터 모델
- 클라이언트와 직접 교환되지 않음
- 데이터베이스 스키마(DB 스키마)와 밀접하게 연관됨
- 최적화된 저장 방식을 사용 가능 (예:
Integer ID
,Timestamp
,Foreign Key
) - 내부적으로만 사용되므로 변경이 자유로움
📌 예제 (Storage Model in Python)⚑
class PhotoAlbumStorageModel:
def __init__(self, id: int, name: str, shared_users: list[int], created_at: int):
self.id = id # Integer ID (DB 저장 최적화)
self.name = name
self.shared_users = shared_users # 유저 ID 리스트 (정수 타입)
self.created_at = created_at # UNIX Timestamp (DB 효율성 고려)
→ DB에 저장되는 모델이므로 id
는 Integer
, created_at
은 Timestamp
형태로 저장됨.
2️⃣ Wire Model (통신 모델)⚑
- 클라이언트와 서버가 HTTP API를 통해 데이터를 교환할 때 사용하는 데이터 모델
- Storage Model과 다를 수 있음
- 백워드 컴패터빌리티(Backward Compatibility, 하위 호환성) 유지가 중요함 → 변경이 어렵다!
- REST API 및 JSON 기반의 직렬화(Serialization)에 최적화됨
- ID를 정수(Integer) 대신 문자열(String)로 변환하여 클라이언트와 통신 (Google API 가이드라인)
📌 예제 (Wire Model in FastAPI)⚑
from pydantic import BaseModel
from typing import List
class PhotoAlbumWireModel(BaseModel):
id: str # ID는 항상 String으로 변환
name: str
shared_users: List[str] # User ID도 String으로 변환 (UUID 또는 Email 가능)
created_at: str # ISO 8601 날짜 형식 ("2024-02-10T12:34:56Z")
→ API로 데이터를 주고받을 때 id
는 str
, created_at
은 ISO 8601(UTC) 형식으로 변환.
📌 Storage Model과 Wire Model의 차이 정리⚑
특징 | Storage Model (저장 모델) | Wire Model (통신 모델) |
---|---|---|
사용 목적 | 데이터베이스 저장 | API 요청/응답 |
ID 타입 | int (Primary Key) | str (UUID, Slug) |
시간 형식 | int (Timestamp) | str (ISO 8601) |
유저 ID | int (Foreign Key) | str (Email, UUID) |
변경 가능성 | 자유롭게 변경 가능 | 한 번 공개되면 변경 어려움 (Backward Compatibility) |
📌 Storage Model과 Wire Model을 분리해야 하는 이유⚑
- 서버 내부 최적화와 클라이언트 친화적인 데이터 형식을 분리
- Storage Model은 DB 저장 및 조회 최적화 (
Integer
,Foreign Key
활용) - Wire Model은 API 데이터 직렬화(JSON), 하위 호환성을 고려하여 설계 (
String
,ISO 8601
)
- Storage Model은 DB 저장 및 조회 최적화 (
- 확장성과 유지보수성을 고려한 설계 가능
- Storage Model을 변경하더라도 Wire Model이 유지되므로 클라이언트 영향을 최소화 가능
- 필드 타입 변경이나 추가가 자유로움
- 보안 및 데이터 노출 최소화
- Storage Model에서 보안 관련 필드 (예: 내부 ID, 비밀번호 해시값 등)를 Wire Model에서 제외
- 클라이언트가 불필요한 내부 데이터를 알 필요 없음
📌 Storage Model과 Wire Model 변환 예제⚑
def convert_storage_to_wire(storage_model: PhotoAlbumStorageModel) -> PhotoAlbumWireModel:
return PhotoAlbumWireModel(
id=str(storage_model.id), # 정수 ID → 문자열 ID 변환
name=storage_model.name,
shared_users=[str(uid) for uid in storage_model.shared_users], # 정수 리스트 → 문자열 리스트 변환
created_at="2024-02-10T12:34:56Z" # UNIX Timestamp → ISO 8601 변환 (예제)
)
→ API 응답 시 Storage Model을 Wire Model로 변환하여 클라이언트와 통신.
🚀 결론⚑
- Storage Model → 서버 내부에서 최적화된 DB 스키마 (정수 ID, Timestamp 등)
- Wire Model → 클라이언트와 JSON 기반으로 통신하는 모델 (문자열 ID, ISO 8601 날짜 등)
- Storage Model과 Wire Model을 분리하면 확장성, 보안, 유지보수성이 향상됨.
**📌 PUT vs PATCH 차이점 완벽 정리**
PUT과 PATCH는 HTTP API에서 리소스를 업데이트하는 방식이지만, 사용 방식과 동작 방식이 다르다.
1️⃣ PUT (전체 업데이트, Replace)⚑
🔹 특징
- 리소스 전체를 교체(Replace)
- 기존 리소스가 존재하면 전체를 덮어씀
- 일부 필드만 제공하면 제공되지 않은 필드는 기본값(Default)으로 대체됨 (즉, 제거될 수도 있음)
- Idempotent (여러 번 호출해도 결과가 동일해야 함)
🔹 예제
📌 요청 (PUT)⚑
PUT /users/123
Content-Type: application/json
{
"name": "Alice",
"email": "alice@example.com",
"age": 30
}
📌 기존 데이터⚑
{
"name": "Alice",
"email": "alice@example.com",
"age": 25,
"address": "New York"
}
📌 PUT 이후 결과 (전체 교체)⚑
{
"name": "Alice",
"email": "alice@example.com",
"age": 30
// "address" 필드는 삭제됨! (데이터 손실 가능)
}
✔ PUT은 기존 필드를 유지하지 않고, 새 데이터로 완전히 교체한다.
✔ "address" 필드가 없어졌음!
2️⃣ PATCH (부분 업데이트, Modify)⚑
🔹 특징
- 일부 필드만 수정(Modify)
- 제공되지 않은 필드는 그대로 유지됨
- Non-Idempotent일 수도 있음 (요청마다 다른 결과가 나올 가능성이 있음)
- 부분 업데이트에 최적화됨 (일반적으로 PUT보다 더 유연함)
🔹 예제
📌 요청 (PATCH)⚑
PATCH /users/123
Content-Type: application/json
{
"age": 30
}
📌 기존 데이터⚑
{
"name": "Alice",
"email": "alice@example.com",
"age": 25,
"address": "New York"
}
📌 PATCH 이후 결과 (부분 업데이트)⚑
{
"name": "Alice",
"email": "alice@example.com",
"age": 30, // 나이만 업데이트됨
"address": "New York" // 나머지는 그대로 유지됨
}
✔ PATCH는 제공된 필드만 변경하고 나머지는 유지한다.
✔ "address" 필드는 그대로 유지됨!
3️⃣ PUT vs PATCH 비교 정리⚑
PUT (전체 업데이트) | PATCH (부분 업데이트) | |
---|---|---|
업데이트 방식 | 리소스 전체 교체 (Replace) | 리소스 일부만 변경 (Modify) |
제공되지 않은 필드 | 삭제될 수도 있음 | 유지됨 |
Idempotent (같은 요청 여러 번 호출 시 결과 동일?) | ✅ Yes | ⚠️ No (경우에 따라 다름) |
사용 예시 | 리소스 전체를 갱신할 때 | 특정 필드만 업데이트할 때 |
데이터 손실 위험 | 있음 (제공되지 않은 필드가 삭제될 수 있음) | 없음 (제공된 필드만 수정됨) |
HTTP Body 필요 여부 | ✅ 항상 필요 | ✅ 필수는 아님 |
🚀 PUT과 PATCH 사용 시 고려할 점⚑
✔ PUT은 모든 필드를 포함해야 하므로 데이터 손실 위험이 있음.
✔ PATCH는 부분 수정이 가능하므로 더 유연함.
✔ 데이터가 완전 교체될 필요가 없다면, 일반적으로 PATCH를 권장.
✔ PUT은 여러 번 호출해도 항상 같은 결과를 보장해야 하지만, PATCH는 아닐 수도 있음.
✔ 대규모 API 설계에서는 PATCH + 필드 마스크(Field Mask) 방식을 활용하면 더 정교한 제어가 가능함.
🔥 결론⚑
- 리소스를 전체적으로 교체해야 하면
PUT
사용 - 특정 필드만 변경해야 하면
PATCH
사용 - 데이터 손실 위험이 없다면
PATCH
가 일반적으로 더 적절한 선택 🚀
📌 실전 API 디자인 강의 | 포토 서비스 만들기 실습 - 2편 - 요약⚑
- 리소스 오리엔티드 디자인의 장단점
- 장점: 일관된 스탠다드 메소드를 활용하여 API를 관리하기 쉬움.
- 단점: 다양한 유즈케이스를 하나의 메소드로 처리해야 하므로 데이터 페이로드가 비효율적일 수 있음.
- 기본적인 API 설계 원칙
- 리소스 삭제(Delete Photo):
DELETE /photos/{photo_id}
- 아이디만 전달하면 되고, 응답(Response)은 비어 있어야 함.
- 새로운 앨범 생성(Create Album):
- 기존 포토 생성과 거의 동일한 패턴을 따름.
- 일관된 API 설계의 장점:
- 여러 개의 API가 동일한 패턴을 따르므로 코드 자동 생성(Code Generation)이 가능.
- 구글 내부에서는 리소스 이름만 바꾸고 코드 제너레이션을 활용하여 API를 자동으로 생성함.
- 리소스 삭제(Delete Photo):
- 앨범 내 사진 추가 및 제거 (Update Photo Album)
- 문제점:
- 액션 오리엔티드 방식에서는
AddPhotosToAlbum
,RemovePhotosFromAlbum
,RenameAlbum
같은 개별 메소드가 생성됨. - 리소스 오리엔티드 방식에서는 하나의 Update 메소드가 여러 유즈케이스를 처리해야 하므로 데이터 페이로드가 증가할 수 있음.
- 액션 오리엔티드 방식에서는
- 해결책: 필드 마스크(Field Mask) 사용
- 필드 마스크를 활용하면 업데이트할 필드만 선택적으로 변경 가능.
- 예를 들어, 앨범 이름만 변경할 경우
"field_mask": "album_name"
을 설정하여 해당 필드만 업데이트. - PUT(전체 변경) vs PATCH(부분 변경)의 개념과 유사.
- 문제점:
- 특정 필드 포함 여부 설정 (텍스트 디텍션 포함/제외)
- 텍스트 디텍션(Textraction) 기능을 API 응답에 포함할지 여부를 선택할 수 있도록 설계.
- 기본 방식:
GET /photos/{photo_id}
요청 시, 모든 필드를 포함한 응답 반환.
- 문제점:
- 텍스트 디텍션이 비용이 높은 연산이므로 항상 포함하는 것은 비효율적.
- 해결책: View Enum 활용
- 필드 마스크(Field Mask)*를 활용할 수도 있으나, API 사용성이 떨어짐 (제외할 필드를 제외하는 방식이 아닌, 포함할 필드를 지정하는 방식).
- 대신 View Enum을 사용하여 미리 정의된 응답 형태를 제공.
- 예시:
"view": "BASIC"
→ 텍스트 디텍션 제외"view": "WITH_TEXT_DETECTION"
→ 텍스트 디텍션 포함
- 결론
- 리소스 오리엔티드 디자인을 따르면 API의 일관성을 유지하고, 코드 자동 생성 및 유지보수가 쉬워진다.
- 필드 마스크(Field Mask)와 View Enum을 활용하면 데이터 페이로드 최적화 가능.
- API 설계를 처음부터 표준화하면 확장성과 유지보수성이 향상된다.