Skip to content

resource-oriented-design

📌 구글한테 배우는 리소스 오리엔티드 디자인

  1. 구글의 API 디자인 철학
    • 구글은 API 디자인 가이드라인에 진심이며, 특히 외부에 공개하는 퍼블릭 HTTP API는 신중하게 설계한다.
    • API는 한 번 공개되면 수정이 어렵기 때문에 신중한 설계가 필요하다.
  2. 리소스 오리엔티드 디자인 vs. 액션 오리엔티드 디자인
    • 액션 오리엔티드 디자인: 특정 동작(예: 회원가입)을 함수처럼 정의하여 signup() 같은 메소드를 만든다.
    • 리소스 오리엔티드 디자인: HTTP 메소드(GET, POST, PATCH, DELETE)를 활용하여 POST /persons 형태로 리소스를 조작한다.
    • 리소스 오리엔티드 디자인은 직관적이며 표준화된 방식으로 API를 설계할 수 있다.
  3. 리소스 오리엔티드 디자인의 핵심 원칙
    • API가 처리할 리소스(Resource) 를 정의한다.
    • 리소스 간의 관계(Relationships) 를 명확히 한다.
    • 리소스가 가지는 필드(Fields) 를 결정한다.
    • 리소스에서 지원할 HTTP 메소드(Methods) 를 정의한다.
  4. 구글의 API 설계 접근 방식
    • 내부적으로 수천 개의 API를 운영하며, 인터널 API(RPC 기반)는 보통 액션 오리엔티드 방식을 많이 사용한다.
    • 하지만 액션 오리엔티드는 확장성이 낮고, 중복된 기능이 많아지며, 유지보수가 어려워지는 문제가 있다.
    • 이를 해결하기 위해 리소스 오리엔티드 디자인을 내부 API에도 적용하려는 시도를 하고 있다.
  5. 결론
    • 리소스 오리엔티드 디자인은 API를 일관되고 유지보수하기 쉽게 만든다.
    • 구글은 내부 API에서도 이 방식을 적용하려는 노력을 기울이고 있다.

📌 실전 API 디자인 강의 | 포토 서비스 만들기 실습 - 요약

  1. API 설계 원칙
    • Create, Read, Update, Delete 기능을 표준 HTTP 메소드(GET, POST, PATCH, DELETE)로 구현해야 한다.
    • 요청(Request) 및 응답(Response) 네이밍은 일관되게 유지해야 하며, CreateResourceRequest, ResourceResponse 같은 명명 규칙을 따른다.
  2. 리소스 및 데이터 모델 정의
    • 서비스 개요: 사용자가 사진(포토)을 업로드하고, 앨범을 생성 및 관리할 수 있는 API 설계.
    • 리소스 스키마 설계:
      • Storage Model: 데이터베이스에 저장되는 내부 모델.
      • Wire Model: 클라이언트와 서버가 주고받는 API 모델.
      • Storage Model과 Wire Model은 처음부터 분리해야 하며, ID 필드는 항상 String으로 정의하는 것이 구글 API 디자인 가이드라인의 베스트 프랙티스.
  3. API 메소드 설계 원칙
    • 사진 업로드(Create Photo)
      • POST /photos 요청 시, CreatePhotoRequestPhoto를 전달.
      • 부모 리소스(parent)를 포함하여 계층 구조를 명확하게 정의.
      • 응답(Response)은 Photo 객체만 반환하고, 부가적인 정보를 포함하지 않도록 제한.
    • 사용자의 모든 사진 조회(List Photos)
      • GET /photos 요청 시, ListPhotosRequestListPhotosResponse를 사용.
      • 페이지네이션(Pagination) 필수 지원 (클라이언트가 대량 데이터를 처리하는 방식 유지).
    • 단일 사진 조회(Get Photo)
      • GET /photos/{photo_id} 요청 시, 특정 사진을 반환.
      • 구글 API 디자인 가이드에서는 name 필드를 ID로 사용하는 것을 권장.
  4. 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에 저장되는 모델이므로 idInteger, created_atTimestamp 형태로 저장됨.


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로 데이터를 주고받을 때 idstr, 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을 분리해야 하는 이유

  1. 서버 내부 최적화와 클라이언트 친화적인 데이터 형식을 분리
    • Storage Model은 DB 저장 및 조회 최적화 (Integer, Foreign Key 활용)
    • Wire Model은 API 데이터 직렬화(JSON), 하위 호환성을 고려하여 설계 (String, ISO 8601)
  2. 확장성과 유지보수성을 고려한 설계 가능
    • Storage Model을 변경하더라도 Wire Model이 유지되므로 클라이언트 영향을 최소화 가능
    • 필드 타입 변경이나 추가가 자유로움
  3. 보안 및 데이터 노출 최소화
    • 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편 - 요약

  1. 리소스 오리엔티드 디자인의 장단점
    • 장점: 일관된 스탠다드 메소드를 활용하여 API를 관리하기 쉬움.
    • 단점: 다양한 유즈케이스를 하나의 메소드로 처리해야 하므로 데이터 페이로드가 비효율적일 수 있음.
  2. 기본적인 API 설계 원칙
    • 리소스 삭제(Delete Photo):
      • DELETE /photos/{photo_id}
      • 아이디만 전달하면 되고, 응답(Response)은 비어 있어야 함.
    • 새로운 앨범 생성(Create Album):
      • 기존 포토 생성과 거의 동일한 패턴을 따름.
    • 일관된 API 설계의 장점:
      • 여러 개의 API가 동일한 패턴을 따르므로 코드 자동 생성(Code Generation)이 가능.
      • 구글 내부에서는 리소스 이름만 바꾸고 코드 제너레이션을 활용하여 API를 자동으로 생성함.
  3. 앨범 내 사진 추가 및 제거 (Update Photo Album)
    • 문제점:
      • 액션 오리엔티드 방식에서는 AddPhotosToAlbum, RemovePhotosFromAlbum, RenameAlbum 같은 개별 메소드가 생성됨.
      • 리소스 오리엔티드 방식에서는 하나의 Update 메소드가 여러 유즈케이스를 처리해야 하므로 데이터 페이로드가 증가할 수 있음.
    • 해결책: 필드 마스크(Field Mask) 사용
      • 필드 마스크를 활용하면 업데이트할 필드만 선택적으로 변경 가능.
      • 예를 들어, 앨범 이름만 변경할 경우 "field_mask": "album_name"을 설정하여 해당 필드만 업데이트.
      • PUT(전체 변경) vs PATCH(부분 변경)의 개념과 유사.
  4. 특정 필드 포함 여부 설정 (텍스트 디텍션 포함/제외)
    • 텍스트 디텍션(Textraction) 기능을 API 응답에 포함할지 여부를 선택할 수 있도록 설계.
    • 기본 방식:
      • GET /photos/{photo_id} 요청 시, 모든 필드를 포함한 응답 반환.
    • 문제점:
      • 텍스트 디텍션이 비용이 높은 연산이므로 항상 포함하는 것은 비효율적.
    • 해결책: View Enum 활용
      • 필드 마스크(Field Mask)*를 활용할 수도 있으나, API 사용성이 떨어짐 (제외할 필드를 제외하는 방식이 아닌, 포함할 필드를 지정하는 방식).
      • 대신 View Enum을 사용하여 미리 정의된 응답 형태를 제공.
      • 예시:
        • "view": "BASIC" → 텍스트 디텍션 제외
        • "view": "WITH_TEXT_DETECTION" → 텍스트 디텍션 포함
  5. 결론
    • 리소스 오리엔티드 디자인을 따르면 API의 일관성을 유지하고, 코드 자동 생성 및 유지보수가 쉬워진다.
    • 필드 마스크(Field Mask)와 View Enum을 활용하면 데이터 페이로드 최적화 가능.
    • API 설계를 처음부터 표준화하면 확장성과 유지보수성이 향상된다.

참고 자료