Principal

책 <웹 API 디자인>

팅리엔 2024. 2. 14. 04:13

사용자를 위한 API 디자인하기

프로바이더가 아닌 컨슈머의 입장에서 디자인한다.

  • 데이터 모델을 그대로 노출하지 않는다.
    e.g. 테이블이 2개라고 api를 2개 제공하거나, 이해하기 어려운 데이터 이름을 그대로 사용하기 No!
  • 비즈니스 로직을 그대로 노출하지 않는다.
    e.g. 고객 주소를 가져와서, 주소를 수정하고, 새주소 추가 api들을 제공하는 대신(이는 비즈니스 로직을 컨슈머에게 위임하는 것이며, 컨슈머가 api 사용을 깜빡할시 치명적 문제가 될 수 있다), 고객 주소 수정 api 하나만 제공하기
  • 소프트웨어 아키텍처를 그대로 노출하지 않는다.
    e.g. 상품 정보와 상품 가격이 다른 시스템에서 처리된다고 상품 검색, 상품 가격 조회 api 2개를 제공하기 No!

API를 디자인할 때 질문하기

  • 누가 사용자인가? 무엇을 하나? 어떻게 하나? 입력값(and 어떻게 입력받을 것인가)? 반환값(and 어떻게 반환받을 것인가)? 목표가 무엇인가?
    e.g. 누가(고객), 무엇을(상품을 구입한다), 어떻게(상품을 검색한다, 상품을 카트에 추가한다), 목표(상품을 비정형쿼리를 이용해서 카탈로그에서 검색한다, 상품을 카트에 추가한다)

 

 

REST API

Representational State Transfer

  • 리퀘스트는 경로와 HTTP 메서드로 구성된다. 'GET /products/{productId}'
  • 리스펀스는 HTTP 상태 코드로 리퀘스트의 처리 상태를 알려주고, Response body는 리소스의 콘텐츠를 포함한다.
  • 경로의 일반적인 포맷: '/상위리소스의-복수형/{상위리소스ID}/하위리소스의-복수형/{하위리소스ID}'
  • HTTP 메서드
    • GET: 리소스 리턴, 쿼리 파라미터
    • POST: 리소스 생성, 바디 파라미터
      e.g. 고객 생성, 메뉴에 음식 추가, 상품 주문, 타이머 시작, 포스팅 발생, 메시지를 고객 서비스에 발송, 서비스 구독, 계약서에 서명, 은행 계좌 열람, 사진 업로드, 소셜 네트워크 공유 등
    • DELETE: 리소스 삭제, 쿼리 파라미터
      e.g. 고객 삭제, 주문 취소, 케이스 종료, 절차 종결, 타이머 종료 등
    • PATCH: 리소스 부분 수정, 바디 파라미터
    • PUT: 리소스 완전히 교체, 없는 경우 새로 생성, 쿼리/바디 파라미터
  • 리소스의 속성(property)를 명시한다: 이름, 타입, 필수 여부, 필요한 경우 부가 설명
    속성의 타입을 정할 땐, 되도록 기본 데이터 타입을 써서 여러 프로그래밍 언어에서 지원할 수 있도록 해주어야 한다. (문자열, 숫자, 일자, 불리언 같은 타입들)
  • 리퀘스트 파라미터는 반드시 필요한 데이터만을 제공해야 하고 불필요한 정보를 제공해서는 안 된다. 또한 백엔드가 자체적으로 처리할 수 있는 데이터를 포함해서도 안 된다. 
  • RESTful 하기 위해서는,
    • 클라이언트/서버 분리
    • Stateless: 리퀘스트를 처리하는 데 필요한 모든 정보는 해당 리퀘스트에 포함되어 있어야 한다. 서버는 리퀘스트를 처리하는 데 필요한 클라이언트의 그 어떤 컨텍스트도 서버의 세션에 담지 않는다. 정보들은 컨슈머가 원래부터 알고 있거나, 이전에 호출된 목표의 출력을 통해서 제공받은 것이다.
    • Cacheability: 리스펀스는 저장 가능 여부(클라이언트가 동일 요청을 다시 하지 않고 재사용할 수 있도록) 및 기간을 표시해야 한다.
    • Layered system: 클라이언트가 서버와 상호작용을 할 때, 오직 서버만을 알고 있어야 한다. 서버를 이루는 인프라 스트럭쳐는 계속 뒷단에 숨겨져 있어야 한다. 클라이언트는 오직 시스템에서 하나의 레이어만 볼 수 있어야 한다.

 

 

직관적인 API 디자인하기

  • 직관적인 표현
    • 속성의 명확한 이름
    • 사용하기 쉬운 데이터 타입과 포맷: timestamp보다 날짜 문자열, 계좌번호 데이터 타입은 숫자보다 문자열
    • 바로 사용할 수 있는 데이터: 계좌 정보 조회 api 응답값으로 balance:500 대신 balance:500, currency:"USD"처럼 부가적인 데이터를 제공하여 컨슈머가 추가적인 작업을 하지 않도록 한다.
  • 직관적인 상호작용
    • 직관적인 입력 요청하기: uuid보다 계좌번호 같은 의미있는 값, 숫자코드 대신 문자열
    • 발생 가능한 모든 에러 피드백 식별하기: 규격에 맞지 않는 에러, 기능적 에러(비즈니즈 규칙에 어긋남), 서버 에러(서버의 장애나 구현의 버그)
    • 유용한 에러 피드백 반환하기: HTTP 상태코드 + 에러 메시지 + 에러 타입 + 문제를 유발하는 속성(source)
      • 404 Not Found 잘못된 파라미터
      • 400 Bad Request 필수 속성의 누락, 잘못된 데이터 타입
      • 403 Forbidden 기능적 에러 (e.g. 금액이 소비 한도를 초과)
      • 409 Conflict 기능적 에러 (e.g. 지난 5분 이내에 동일 송금이 발생한 전력이 있음)
      • 500 Internal Server Error 서버 에러
      • 에러 피드백은 반드시 무엇이 문제인지 알려주고 컨슈머 스스로 해결할 수 있게 도와주어야 한다.
      • 에러를 정의할 때 에러마다 특정 타입(e.g. AMOUNT_OVER_SAFE)을 정의할 필요는 없다. 좀 더 포괄적인 타입(e.g. MISSING_MANDATORY_PROPERTY)을 정의하는 게 좋다.
      • 여러 개의 에러를 하나씩 따로 응답하는 것은 피한다.
    • 성공 피드백은 무슨 일이 벌어졌는지와 그 다음에 무엇을 해야하는지에 대한 정보를 제공해주는 것이 좋다. 

 

 

예측 가능한 API 디자인하기

  • 일관된 이름/타입/포맷
  • 그동안 201 Created나 202 Accepted 대신 200 OK만을 반환했다면 새로운 API도 다른 HTTP 상태코드를 반환하지 않고, 그동안 필수 속성이 누락되었음을 나타내기 위해 MISSING_MANDATORY_PROPERTY를 정의했다면 API에서 필수 속성이 빠졌을 땐 무조건 이 코드만을 사용한다.
  • 가능한 많은 메타데이터 제공하기
{
    "pagination": {
        "page": 1,
        "totalPages": 9,
        "next": "/accounts/1234567/transactions?page=2",
        "last": "/accounts/1234567/transactions?page=9"
    },
    "items": [
        {
            "id": "00001",
            "date": "2022-07-01",
            "source": "1234567",
            "destination": "7654321",
            "amount": "1045.2",
            "actions": ["cancel"]
        },
        {
            "id": "00002",
            "date": "2018-03-01",
            "source": "1234567",
            "destination": "7654321",
            "amount": "189.2"
        },
        ...
    ]
}

 

 

 

안전한 API 디자인하기

  • 허용된 컨슈머만 API를 사용하도록 한다. - 개발자들이 스스로 개발자 포털에서 컨슈머로 등록할 수 있어야 한다. 개발자 포털이 없더라도, 데이터베이스에 응용 프로그램 이름, 범위 및 자격 증명을 저장하여 동일 구성을 수행할 수 있다.
  • OAuth 2.0: Client가 Resource owner의 동의를 얻어 Authorization server에서 Access token을 발급 받아 Resource server의 리소스에 접근한다.
  • HTTP 프로토콜을 이용할 경우 전송 계층 레이어 보안(TLS: Transport Layer Security)의 암호화(일반적으로 SSL: Secure Sockets Layer)로 지켜낸다.
  • API를 분할하여 접근을 제어한다. 최소 권한의 원칙(컨슈머가 접근 가능한 영역을 꼭 필요한 수준으로 제한을 둬서 공격에 대한 가능성을 줄인다).
  • 민감한 데이터를 식별하고 적절한 표현을 선택한다.
    • 민감한 데이터가 필수 요소가 아니라면 제거한다.
    • 제거할 수 없다면 민감하지 않은 형태로 표현을 순화한다 e.g. 카드번호 4자리만 표현
    • 값을 암호화한다.
    • 민감한 데이터에 접근할 수 있는 API를 별도로 분리한다.
  • 안전하게 에러 피드백을 준다.
    • '사용자가 해당 카드에 접근 권한이 없습니다'와 같은 메시지는 암시적으로 해당 카드가 존재한다는 것을 알려준다.
    • 스택트레이스, 소프트웨어 오류에 대한 상세 설명, 서버 주소, 에러 유발점등을 노출해서는 안 된다.\
  • 주의해서 로그를 남긴다. 
    • 경로 파라미터나 쿼리 파라미터에 로그에 남을 우려가 있는 민감 정보는 포함하지 않는다. 

 

 

API 디자인 발전시키기

API 업데이트에 맞춰 모든 컨슈머들이 업데이트를 진행하는 것은 불가능 하므로, API 변경은 피하거나 최소한으로는 서로가 변경점을 인식하는 것이 중요하다. 

  • 확장 가능한 데이터
    • 데이터를 오브젝트에 담아 확장성을 보장한다. 
    • 데이터 타입은 self descriptive 해야한다. 불리언은 확장성이 있지 않지만(true, false뿐), 문자열은 확장성이 있다.
      (e.g. "validated": true 대신에 "status": "VALIDATED")
    • 유사한 데이터를 목록으로 묶는 것이 더 확장 가능하다. 
      (e.g. "createdAt": "2018-01-01", "updatedAt": "2019-02-01" 대신에 "events": [{...}, {...}])

 

 

네트워크 효율적인 API 디자인하기

  • 한 화면을 그리기 위해 너무 많은 양의 API 호출이 일어날 수 있다. 네트워크 호출 횟수와 교환되는 데이터의 양을 신경써야 한다.
  • 압축 & 지속적인 연결 활성화하기 - HTTP/2 고려
  • 캐싱 & 조건부 리퀘스트 활성화하기: HTTP 리스펀스 헤더의 Cache-Control & ETag
    • Cache-Control 값만큼 API 호출을 멈춘다.
    • ETag는 데이터의 버전 정보로 컨슈머가 알 필요는 없다. - 서버는 데이터가 변경되지 않았으면 다른 데이터 없이 304 Not Modified 리스펀스를 보낸다.
    • 어플리케이션이 오직 데이터의 변경 여부에만 관심이 없다면 GET 대신 HEAD 리퀘스트를 사용해서 메타데이터만 주고받을 수 있다
    • 캐시 정책 선택: 얼마나 자주 수정되느냐, 사람들이 정보를 얼마나 실시간으로 사용하느냐, 법에 걸릴수도(부정확한 정보를 보여주는 게)
    • 사용자가 정보를 실시간으로 얻고자 한다면 Cache-Control은 0으로 하고 ETag만을 사용할 수 있다.
  • 필터링 활성화하기: 컨슈머들이 정말 필요한 것들만 요청하게 되어 주고받는 데이터의 크기를 절약할 수 있다.
  • 목록 데이터를 가져올 때, 요약 정보를 반환하는 것이 일반적이지만 때로는 모든 정보가 포함된 완전한 표현을 반환하는 것이 더 효율적이다.
  • 사용자 데이터와 사용자의 주소 목록 데이터를 따로 따로 읽는 것보다 한 번의 읽기 호출로 가져오는 게 효율적일 수 있다. (자주 변경되지 않는 목록) - 그러나 만약 지속적인 연결을 서버상에 활성해두었다면 이런 전략은 그다지 효율적이지 않을 것이다. 또한 작은 데이터의 변경이 캐시를 사용하지 못하고 전체를 다시 읽게 만들 수도 있다. 또한 컨슈머가 API의 작동 방식을 이해하기 어려울 수 있다.
  • API 리퀘스트 Accept 헤더에 application/vnd.bankingapi.extended+json와 값을 줘서 요약된 데이터가 아닌 확장된 표현을 가져오도록 선택할 수 있다.
  • 쿼리 활성화하기: 만약 모든 바이트와 밀리초까리 고려해야 한다면, 컨슈머들이 원하는 속성 하나 하나를 선택하게 할 수 있다. e.g. GET /owners?_fields=id (반환되는 소유자 목록에 오직 소유자의 id만 포함), like GraphQL
  • API를 최적화하는 것은 좋은 일이지만 사용성과 재사용성을 희생하면서까지 해서는 안 된다.

 

 

API 문서화하기

  • 데이터모델: 속성의 포맷과 값에 대한 자세한 정보 (type, description, minLength, maxLength, pattern, example)
  • 목표: 목적(summary), 사용에 필요한 것(path, http method, request body example), 성공 또는 실패 시 피드백의 종류(responses: http code, description, content, ref-데이터모델의참조) - 데이터모델로 추론할 수 있지만 사람이 읽기 쉽도록 단순하게 설명을 제공한다.
  • 보안: 보안과 관련된 정보
  • API의 개요: API의 이름, 버전, description, contact
  • 사용자 안내서: API를 전체적으로 사용하는 방법과 그 속에 담긴 원칙 및 접근 방법(등록 및 액세스 토큰 가져오기)
  • 변경 이력change log: 사용하지 않는 속성은 'deprecated: true'

 

 


 

 

짧은 요약 - API 디자인 시 고려할 점:

  • 사용자 입장 고려하기 (사용자가 진짜 필요로 하는 건 무엇인가?, 내부 구현 숨기기)
  • RESTful 스타일: url과 HTTP 메서드 고려하기, 적절한 성공/실패 리스펀스(+메타데이터)
  • 일관된 스타일 고수하기 (경로, 이름, 응답코드, 에러코드 등)
  • 보안: 인증과 권한 체크, 어떤 데이터를 노출할 것인가
  • API가 어떻게 성장해 나갈 것인지 고려하기
  • 문서화

'Principal' 카테고리의 다른 글

책 <도메인 주도 개발 시작하기>  (8) 2024.09.10
TDD 원칙  (0) 2024.02.18
애자일 Agile 개발 방법론  (0) 2022.10.17
책 <객체지향의 사실과 오해>  (1) 2022.10.05
DDD와 어플리케이션 이벤트 스토밍  (0) 2022.08.24