RESTful에 대하여

Heechan Kang·2024년 1월 5일
post-thumbnail

서론

RESTful 설계의 컨셉은 시스템에서 제공하는 Resource를 'Bucket'화 하는 것

RESTful API는 현대의 웹 서비스 디자인에서 가장 중요한 아키텍처 스타일이라고 생각합니다. 이를 통해 우리는 효율적이고 확장 가능한 웹 서비스를 만들 수 있게 되었습니다.

따라서 이번 글에서는, RESTful API의 주요 개념과 최선의 실천 방법을 소개하겠습니다.

RESTful API의 기본 구성 요소

  • 자원(Resource, Document)

    • 웹에서 접근 가능한 모든 종류의 객체를 의미합니다.
    • 예를 들어 문서, 이미지, 데이터도 될 수 있고, 추상적인 개념이 될 수도 있습니다.
    • https://example.com/users/123 과 같은 고유한 URI로 식별 될 수 있습니다
  • 컬렉션(Collection)

    • 서버에 존재하는 리소스의 그룹 또는 집합을 나타냅니다.
    • https://example.com/users는 모든 사용자 리소스의 컬렉션을 나타냅니다.
    • 컬렉션 자체도 하나의 리소스로 간주되며, 자신만의 URI를 가집니다
  • 스토어(Store)

    • 클라이언트가 서버에 데이터를 저장할 수 있는 공간을 말합니다.
    • 이는 클라이언트가 서버에 보낸 데이터의 저장소로, 일반적으로 리소스의 컬렉션의 일종이지만, 컬렉션과 별개의 의미로 사용되는 경우도 있습니다.
  • URI, URL(Uniform Resource Identifier, Locator)

    • 네트워크상에서 특정한 자원의 위치를 나타내는 문자열입니다.

RESTful API 설계의 최선의 실천 방법

1. 이왕이면 JSON을 사용하자

  1. XML은 파싱이 어렵고, 여타 프레임워크에서 지원이 상대적으로 부족한 편입니다.
  2. 반면에 JSON은 PHP, Python 등 다른 언어에서도 지원이 잘 되어있고, 이로인해 어디서든 간편하게 사용할 수 있습니다.

2. 엔드포인트에 동사가 아닌 명사를 사용하자

  1. GET, POST 이외에도 PUT, DELETE 등의 HTTP 메소드가 이미 정의되어 동사로 사용되므로, 엔드포인트에 동사를 사용할 필요가 없습니다.
  2. GET /getPosts, POST /createPost, POST /updatePost 등의 형식이 아닌, GET /posts, POST /posts, PUT, PATCH /posts/{ id } 등의 형식을 사용한다.
  3. HTTP 메소드로 정의 할 수 없는 경우에만 동사를 사용하자.

3. 복수형 단어를 사용하자

  1. API는 기본적으로 사용자가 접근 가능한 리소스의 버킷으로 볼 수 있습니다.
  2. GET /posts, GET /posts/1 등의 형식을 사용해서, posts라는 컬렉션에 접근하고, posts의 id가 1인 리소스에 접근하는 것이 좀 더 직관적입니다.
  3. 또한 이를통해 다수의 개발자가 일관된 개발 환경을 유지 할 수 있습니다.

4. 상태코드를 적극적으로 사용하자.

  1. 상태코드를 사용하면, 클라이언트가 서버의 상태를 쉽게 파악할 수 있습니다.

  2. 따라서, body 상에서 별도의 status를 표기하는것은 불필요한 중복이 될 수 있습니다.

  3. 그리고, 가능한 한 최대한 200, 400보다는 201, 204, 409, 429 등 용도에 맞는 구체적인 상태코드를 사용하는게 좋습니다.

    상태코드설명예시
    100 - 199정보102 processing
    200 - 299성공200 OK
    300 - 399리다이렉션301 moved permanently
    400 - 499클라이언트 에러400 bad request
    500 - 599서버 에러500 internal server error

5. 중첩을 통해 관계를 표현하자

  1. 예를 들어, GET /posts/1/comments는 id가 1인 post의 댓글을 가져오는 것입니다.
  2. 이렇게 중첩을 통해 관계를 표현하면, 클라이언트가 서버의 구조를 직관적으로 파악할 수 있습니다.

6. 요청된 데이터를 조회할 때, 필터, 정렬, 페이지네이션을 사용하자

  1. 필터: GET /posts?author=1은 id가 1인 작성자의 post를 가져오는 것입니다.
  2. 정렬: GET /posts?sort=created_at은 작성일자를 기준으로 post를 정렬하는 것입니다.
  3. 페이지네이션: GET /posts?page=1&limit=10은 1페이지에 10개의 post를 가져오는 것입니다.

7. 버저닝을 통해 API를 관리하자

  1. 버저닝을 통해, API의 변경사항을 관리할 수 있습니다.
  2. 정형화된 방법은 없고, 아래와 같이 필요에 따라 사용할 수 있습니다.
  3. GET /v1/posts, GET /v2.0/posts, GET /2.0/posts

8. 정확한 API 문서를 작성하고, 제공하자

  1. 아래의 요소를 포함하는 것이 좋습니다
    • 관련된 엔드포인트의 목록
    • 엔드포인트에 대한 요청의 예제
    • 몇가지 언어로의 기본적인 구현 예제
    • 각 에러에 대한 설명

구체적인 예시

  • POST /store/{store-id}/products

    • 나쁜 예시

      Content-Type: application/json
      {
        "name": "Product `""
      }
      HTTP/1.1 200 OK
      Content-Type: application/json
      {
        "result": { "id": 1 }
      }
    • 좋은 예시

      Content-Type: application/json
      {
        "name": "Product 1"
      }
      HTTP/1.1 201 Created				--> 상태 코드를 적극 사용
      Location: /store/1/products/1		--> 생성된 리소스의 위치를 명세
      
      HTTP/1.1 200 OK
      Content-Type: application/json
      Content-Location: /store/1/products/1		--> 생성된 리소스의 위치를 명세
      {
        "id": 1,
        "name": "Product 1"
      }
  • GET /store/{store-id}/products

    • 나쁜 예시

      HTTP/1.1 200 OK
      Content-Type: application/json
      {
        "result": [{
          "id": 1,
          "name": "Product 1"
        }]
      }
    • 좋은 예시

      HTTP/1.1 200 OK
      Content-Type: application/vnd.store.product+json		--> 자기설명적인 명확한 응답
      [{
        "id": 1,
        "name": "Product 1",
        "links": [{
          "self": "/store/1/products/1"		--> HATEOAS를 준수하여 다른 리소스에 간편히 접근 가능
        }]
      }]
  • GET /store/{store-id}/products/{product-id}/comments

    • 나쁜 예시

      HTTP/1.1 200 OK
      Content-Type: application/json
      {
        "result": [{
          "id": 1,
          "product_id": 1,
          "text": "Comment 1"
        }]
      }
    • 좋은 예시

      HTTP/1.1 200 OK
      Content-Type: application/vnd.store.product.comment+json
      [{
        "id": 1,
        "store_id": 1,
        "product_id": 1,
        "text": "Comment 1",
        "links": [{
          "self": "/store/1/products/1/comments/1",
          "product": "/store/1/products/1"		--> 응답값만으로 해당 comment가 어느 store, product에 속한 것인지 알 수 있음.
          "store": "/store/1",
        }]
      }]

고민해볼 케이스

  • 파일 속성확인 / 다운로드

    • GET /files/{file-id}
      • 케이스 1
        • GET /files/{file-id}/properties 별도 사용
      • 케이스 2
        • GET /files/{file-id}?properties=true 쿼리스트링 사용
      • 케이스 3 (최적)
        • GET /files/{file-id} 엔드포인트 사용
          • Accept: application/json 헤더를 통해 JSON을 요청
          • Accept: application/octet-stream 헤더를 통해 파일을 요청 혹은
          • Accept: image/png 헤더를 통해 이미지를 요청
  • GET 요청시 데이터 변경이 필요한 경우?

    • HTTP 스펙상 GET 메서드는 세이프 메서드이므로, 데이터 변경이 불가능함.
    • 따라서 정석은 GET 요청시 데이터 변경이 필요한 경우 PUT, POST, PATCH 등의 메서드를 함께 사용해서 리퀘스트를 두 번 보내는 것.
    • 혹은 쿼리스트링을 통해 데이터 변경을 요청하는 것: GET /mails/{mail-id}?preview=true, 정석은 아니지만, 일반적으로 많이 사용됨.
  • 한번의 리퀘스트로 인해 다수의 객체의 변경이 생기는 경우?

결론

RESTful API는 그 특성상 명확한 규칙이 있는것은 아닙니다.
하지만 많은 개발자가 이를 적용해서 조금씩만 더 신경을 써 주신다면, 일관성과 표준화를 통해 좀 더 효율적인 개발환경이 구성 될 것이라고 생각합니다.


참고
REST API Best Practices – REST Endpoint Design Examples
RESTful API Designing guidelines — The best practices

profile
안녕하세요!

0개의 댓글