CS

당신의 API가 실패하는 이유: 최고의 API 설계 비법 5가지

ri5 2024. 1. 7. 20:09


백엔드 개발자가 가장 자주 하는 업무 중 하나를 꼽으라고 한다면 바로 "API 개발"이라고 이야기할 수 있을 것이다. 우리는 API들을 만들면서 API를 구현하는 비지니스 로직은 많이 공부하고 익혔었지만 API 자체에 대해서 잘 설계하는 것은 고려하지 못한 부분이 종종 있을 것이 있을 것이다. 아래와 같은 상황을 겪어 본적이 있는가?

 

일부 API는 단 하나의 용도로만 사용할 수 있는 방면, 어떤 API는 검색, 필터 등 유연하게 활용할 수 있다. 그리고 과거에 만들어진 어떤 API는 오랫동안 계속 사용하고 있지만 어떤 API는 최근에 만들었음에도 불구하고 약간의 변경이나 기능이 추가될 때 수정이 어려워 새로운 API를 만들거나 기존 API의 버전을 올려야 하는 상황이 발생한다.

 

서론이 길었지만 이글은 API를 설계하면사 실패할 수 있는 사례들을 피하고 좋은 API를 설계하면서 지속가능한 API들을 만들 수 있는 비법들을 담은 글이다.

 

1. 단순하게 정의하라

예를 들어 특정 주문의 영수증을 가져와야하는 API를 만들어 달라고하는 요구사항이 들어왔을 때 단순하게 생각하고 설계한다면 아래와 같은 API 형태로 될 것이다. 

GET /orders/:id/receipts

 

위와 같은 형태로 API를 디자인해도 당장은 큰 문제 없이 무난하게 활용할 수 있다. 하지만 만약 유저가 결제 영수증들을 조회하거나 특정 기업에서 유저가 결제한 영수증을 조회해야된다고 요구사항이 들어오게되면 저렇게 디테일하게 정의된 API는 새로운 형태의 API를 계속 만들수 밖에 없게 된다. 그럼 이제 아래의 예시를 봐보자.

GET /receipts

GET /receipts?companyId=2

GET /receipts?orderId=3

GET /receipts?userId=4

 

좀 더 심플하게 수정하게 되면서 이 API는 다양한 비지니스 요구사항에 맞는 검색 조건을 동시에 적용할 수 있게 되었다. 필자도 가끔 이런 실수를 범하고는 하는데 그 이유는 RESTful 원칙 중 "URI를 보고 그 의미를 쉽게 이해할 수 있도록 정의해야 한다"는 부분에 집착하여 심플하게 설계하는 법을 잊어버리다보니 실수하게 되는 것 같다.

 

2. 올바른 형태의 Http Method를 활용

https://www.linkedin.com/pulse/http-standard-methods-why-you-should-use-them-mahmoud-mahmoud/

HTTP를 공부했었다면 모두가 당연히 알고 있을 내용이지만 다시 복기해보자.

  • GET: 하나 또는 여러개의 리소스를 가져옵니다.
  • POST: 리소스를 생성하거나 리소스의 특정 프로세스를 실행하여 처리합니다.
  • PATCH: 리소스의 일부분을 수정합니다.
  • PUT: 리소스를 입력받은 값으로 전부 갱신하거나 새로 생성합니다.
  • DELETE: 리소스를 삭제합니다.

이렇게 하자고 규율을 정했지만 이건 언제까지나 규율이기 때문에 개발의 편이성에 따라 모든 API를 POST로 사용하여 개발하는 경우가 간혹 있다. 정말 강력하게 컨벤션을 관리하는 체계가 존재하고 모든 개발자들이 그런 컨벤션에 대한 규칙을 인지하고 있다면 관리할 수 있겠지만 현실은 그렇지 않은 환경이 대부분일 것이다.

 

※ 그런 환경이 지속되다 보면 API를 개발하는 시간보다 API를 이해하는 시간이 더 길어지게 되니 규율을 준수하여 개발하는 것을 권장한다.

메서드 사용시 주의 사항

단순히 HTTP 메서드를 의도한대로 사용하는 것도 중요하지만 그안에 지켜야 하는 규칙들을 지키는 것도 중요하다. 이것까지 완전히 숙지있다면 당신은 이미 완벽하게 HTTP 메서드를 활용하고 있는 것이다. 이제 메서드별로 준수해야되는 규칙들을 알아보자.

 

※ 멱등성(중요): 동일한 요청을 한 번 보내는 것과 여러 번 연속으로 보내는 것이 같은 효과를 지니고, 서버의 상태도 동일하게 남을 때, 해당 HTTP 메서드가 멱등성을 가졌다고 말합니다.

GET

리소스를 조회하는데 사용되는 메서드이기 때문에 이 메서드를 활용할 때에는 서버의 상태가 변경되면 안되고 멱등성(중요)을 가져야 한다. 그리고 캐싱이 가능하기 때문에 같은 내용의 요청이 들어왔을 때에는 캐싱된 응답을 처리할 수 있다.

 

예외적으로 조회수 카운트나 인기순 조회는 결과가 항상 변하기 때문에 멱등성을 지키지 못한다고 이야기할 수 있다. 변수가 무수히 많이 존재하는 현실 세상에 규율을 100% 지키면서 개발하는 것은 어렵기 때문에 80% 정도로 만족하자. API 설겨와 관련없는 내용이지만 조회수 같은 경우는 대규모 트래픽이 발생하는 서비스에는 장애를 발생시킬 수 있는 요인이 될 수 있으니 이벤트를 발행하여 따로 처리하는 것이 좋다.

 

POST

설계할 때 가장 유의해야되는 메서드이다. 해당 메서드는 서버의 상태를 변경하거나 특정 데이터 프로세스를 실행하기 때문에 캐싱도 불가능하며 멱등성을 가지고 있지 않기 때문에 안전하지도 않다. 왜냐하면 동시에 요청이 들어왔을 때 두개의 리소스를 생성하거나 잘못된 데이터 입력이 들어온 그대로 데이터 프로세스를 실행할 수 있기 때문이다. 

 

클라이언트단을 개발하는 개발자들에게는 미안하지만 해당 메서드를 사용할 때에는 클라이언트에서 잘못된 값이나 요청을 가정한채로 방어적 프로그래밍에 임하도록 하자. 그렇다고 개발자의 역활을 잊고 POST 메서드를 쓰는 모든 API에 적용하다 보면 일정을 지키지 못할 수 있으니 문제가 생겼을 때 크리티컬하다고 작용할 수 있는 부분에 우선적으로 신경써서 개발하는 것을 추천한다.

 

PATCH

해당 메서드는 멱등성을 지켜질수도 있고 지켜지지 않을 수도 있다. 예를 들어 유저의 주소를 변경하는 API를 만들었을 때 같은 요청을 1번 보내도 100번을 보내도 같은 주소지로 변경이 되어 멱등성이 지켜질 것이다.

 

멱등성이 지켜지지 않을때를 예시로 들자면 비행기 티켓을 업그레이드할 때 남은 자리로 좌석이 자동으로 배정될 때를 생각할 수 있다. 수정될 때마다 변칙적으로 변경이 되기 때문에 멱등성이 지켜지지 않을 수 있는 것이다.

 

그리고 PATCH 메서드는 리소스의 일부분만 변경하는 것 뿐만 아니라 특정 필드를 추가하는 역활로도 활용할 수 있다. RDBMS는 동적으로 필드를 생성할 수 없지만(JSON 전체를 저장하는 경우 제외) MongoDB와 같은 Document DB는 Json의 필드를 동적으로 추가하거나 삭제할 수 있기 때문에 동적으로 필드를 추가하거나 삭제를 할 수 있다.

 

PUT

개발자들이 가장 헷갈려하는 메서드 중 하나일 것이다. 동시에 PATCH의 역활을 하면서도 POST의 역활을 하는 것처럼 보이기 때문이다. 이해하기 쉽게 설명하자면 upsert에 비유할 수 있을 것이다. 해당 위치에 리소스가 존재하면 입력받은 요청 데이터로 대체하고 없다면 해당 위치에 리소스를 생성하는 것이다.

PUT /files/sample.jpg

 

예시로 위와 같은 요청이 들어왔을 때 해당 위치에 sample.jpg가 있다면 새로운 이미지로 덮어 씌우고 없다면 새로 생성하는 것이다. 이메서드는 서버의 상태를 변경하지만 멱등성은 지켜진다. 왜냐하면 완전히 새로운 데이터로 덮어 씌우거나 새로 생성하기 때문에 1번을 요청하든 100번을 요청하든 입력된 요청대로 처리되기 때문이다. 원래는 updated_at 과 같은 필드도 입력된 값으로 변경되어야 하지만 개발팀에 따라 달라질 수 있다.

 

DELETE

이 메서드는 멱등성을 가진다. "1번 요청하면 삭제되고 2번 요청에는 아무런 행위를 하지 않는데 왜 멱등성을 가지는가"에 의문이 생길 수 있다.

DELETE /products/1

 

위의 API를 한번 호출했을 때와 두번 호출했을 때의 상태는 어떠한가? 동일하게 products에 1번 아이디는 없어졌다. 그렇기 때문에 멱등성을 가지고 있다고 볼 수 있다. 그리고 이 Method에서는 아직도 논쟁중인 주제가 있는데 그것은 바로 soft delete다.

 

개념적으로만 삭제하고 물리적으로는 존재하는 리소스에 DELETE 메서드를 활용하는 것이 맞는가에 대해서는 지금까지 명확하게 정답이 정해지지 않았지만 대부분의 여론은 클라이언트와 서버의 관점으로 봤을 때 클라이언트가 서버의 내부 정책까지 알 필요 없기 때문에 클라이언트는 hard delete, soft delete 상관없이 DELETE 메서드를 사용하는 쪽으로 암묵적 합의가 되었다.

 

 

3. 쿼리 파라미터를 적극 활용하자.

리소스를 조회하는 API를 설계할 때 uri만으로는 표현하기가 어려워서 동사를 사용하여 정의하는 경우가 종종 있다. 하지만 그렇게 정의했을 경우 어떤 문제가 일어날 수 있는지 아래의 예시를 통해 알아보자.

GET recruits/search

 

위와 같은 API를 통해 해당 ui에 사용되는 api를 개발했다고 가정해보자. 지금까지는 문제가 없어 보이지만 나중에 새로운 요구사항이 들어온다면 어떻게 될까? 아래의 UI를 통해 어떤 요구사항이 들어왔을 때 문제가 생기는지 확인해보겠다.

 

검색이라는 매커니즘을 제외하고는 내부적인 동작은 동일한 상황이다. 해당 UI에 데이터를 뿌려주는 API를 개발할 때 기존의 API를 활용한다면 전혀 다른 의미로 사용하기 때문에 Restful하지 못한 API가 되고 새로운 API를 만들자니 의미와 내부동작이 거의 똑같은 중복 API 개발하게 된다.

 

이런 기술부채를 피하기 위해서는 1법칙과 3법칙을 적절하게 적용하여 심플하게 디자인한 API에 쿼리 파라미터를 적극활용하는 것이 좋다.

GET recruits?keyword="삼성"

 

4. 적절한 HTTP 상태 코드 선택

 

수십개의 HTTP 상태 코드들을 적절한 상황에 맞춰 모든 상태 코드를 활용하는 웹사이트나 개발팀을 본 적이 있는가? 이처럼 모든 상태코드를 사용하는 것은 매우 어렵고 복잡한 일이다. 그래서 보통 개발자들은 200, 500, 400, 401과 같은  자주 사용되는 상태코드들만 활용하여 개발하곤한다. 사실 4가지의 상태코드만 활용해도 초기에는 무방하게 사용할 수 있기 때문에 큰 문제가 되지 않는다.

 

하지만 엔터프라이즈급 시스템이 되어갈수록 각각의 상태코드들이 너무 많은 의미를 포괄적으로 담고 있게 되면서 점점 상태코드에 대한 의미를 이해하기 힘들어지고 API들을 관리하기 어려워진다. 그렇다면 어떻게 해야 복잡한 비지니스 요구상황 속에도 지속가능한 API를 설계할 수 있을지 HTTP 메서드별로 알아보자.

 

GET 메서드

조회가 성공했을 때에는 200(OK)를 통해 반환한다. GET을 쓸 떼 가장 헷갈려하는 부분은 404(Not Found)와 204(No Content)이다.

 

이부분은 해석하기 나름이기 때문에 개발자마다 의견이 다르지만 필자는 404는 특정 리소스에 대해 직접 접근했을 때 리소스가 존재하지 않는 의미(Not Nullable한 상황)로 404를 반환하고 204는 리소스 전체를 조회하거나 특정 그룹들을 조회할 때 리소스가 비어있다는 의미(Nullable한 상황)로 204를 사용한다.

 

POST 메서드

새 리소르를 성공적으로 만들었을 때에는 201(Created)를 반환하고 리소스에 대한 프로세스가 성공적으로 처리되었을 때에는 200(OK)로 반환한다.

 

리소스가 존재하지 않거나 프로세스를 처리해도 변경사항이 없어 응답값이 존재하지 않을 때에는 204(No Content)를 응답하도록 하자. 예시로 한가지만 설명하자면 게시글에 댓글을 달았는데 해당 댓글에 내용이 비속어 필터 처리가 되었을 때 성공적으로 처리는 되었지만 댓글 자체는 존재하지 않는 것을 케이스가 있다.

 

모두 알겠지만 잘못된 요청값으로 요청이 들어올 경우 400(Bad Request)를 통해 요청 자체를 거부하는 것이 이상적이다.

 

PUT 메서드

POST 메서드와 똑같이 새로운 데이터가 생성되면 201, 성공적으로 수정했을 때에는 200 또는 204(클라이언트에 반환할 값이 없을 때)를 반환한다. 하지만 잘못된 데이터 형태가 입력되거나 허용하지 않은 타입으로 요청이 들어왔을 경우에는 409(Conflict)로 반환해야 한다.

 

PATCH 메서드

성공적으로 처리한 경우에는 200을 반환한다. PATCH 메서드는 PUT과 다르게 기존 리소스의 일부를 수정하는 메서드이기 때문에 예외 케이스들이 더 많이 존재한다.

  • 415(Unsurpported Media Type): Content-Type이나 Content-Encoding이 기존 리소스와 호환하지 않는 경우 반환.
  • 400(Bad Request): 수정할 수 없는 값이 들어오거나 리소스 포맷이 잘못된 경우 반환.
  • 409(Conflict): 리소스는 존재하지만 서버 내부 정책에 의해 처리할 수 없는 경우 반환.

DELETE

DELETE 메서드는 200과 204를 혼용하여 사용하는 경우가 있는데 보통 soft delete를 하여 개념적인 삭제일 경우 200(OK)를 반환하고 hard delete를 통해 리소스를 물리적으로 아예 삭제되어 찾을 수 없는 경우는 204(No Content)를 응답한다. 만약 해당 리소스를 찾을 수 없다면 404를 응답하는 경우도 있는데 개발자가 설계하는 방향에 따라 달라져 200이나 204로 처리하는 경우도 있을 수 있다.

 

비동기

REST API가 주로 동기적으로 통신하기 위해 사용하지만 때로는 비동기적으로 처리해야 될 때가 있다. 예시로 이메일이나 휴대폰 인증 같이 소요 기간을 제어하기 어려운 상황이거나 긴 시간이 소요될 때 비동기적으로 처리한다.

 

보통 성공적으로 요청을 받았다는 의미로 202(Accept)를 반환하지만 새로운 리소스를 만들거나 다른 엔드포인트로 이동해야할 때에는 헤더에 Location정보를 담아 303(See Other)를 반환하기도 한다. 

 

5.  체계적이고 명확한 에러 메시지

{
  "error": {
    "message": "(#803) Some of the aliases you requested do not exist: products",
    "type": "OAuthException",
    "code": 803,
    "fbtrace_id": "FOXX2AhLh80"
  }
}

 

클라이언트가 상태코드를 통해 어떤 문제가 생겼는지 짐작할 수 있겠지만 무슨 이유로 API 요청이 문제가 생겼는지 자세히 알기는 어렵다. 그렇다고 단순한 메시지 형태로 Body에 담아 보내면 같은 의미의 에러 메시지들이 각기 다른 내용으로 전달되어 점점 에러 케이스들을 관리하기가 어려워질 것이다.

 

그렇기 때문에 공통적인 에러 템플릿을 만들어 체계적이로 관리해야하며 이로 생기는 이점은 템플릿 형태로 관리되기 때문에 코드로 관리할 수 있다는 것과 문서화하기에도 용이한 것이 있다. 그리고 에러 메시지를 명확하게 하는 것 또한 중요하다. 에러 메시지만 보고도 클라이언트가 이해하기 쉬워야 무엇 때문에 잘못 처리되었는지 빠르게 파악할 수 있기 때문이다. 에러 메시지를 작성할 대 주의 해야되는 점은 너무 디테일하거나 많은 내용들을 담지 않는 것이 중요한데 그 이유는 해커들이 에러 메시지를 통해 보안취약점을 발견할 수 있기에 주의하여야한다.

 

정리하며

지금까지 API를 잘 설계하는 방법에 대해 정리하면서 나도 정말 많은 API를 잘못 만들었던 기억들이 생각 났었고 그런 잘못 설계 했었던 경험들을 살려 참조한 글들에 살들을 덧붙여 내글로 작성할 수 있었던 것 같다. 이전에 개발자 커뮤니티에 이런 내용의 글을 본적이 있는데 API만 주로 개발하는 경험만 하다보니 더이상 배울 것이 없다고 생각하는 내용의 글이였다. 그때에는 좋은 코드를 작성하는 법이나 테스트를 잘 작성하는 법만 해도 공부해야할 것이 산더미인데 기본에 충실하지 못하고 있다고 생각했었다. 하지만 막상 나 자신은 가장 기본적인 API 설계에 대해 무심했었고 잘못된 API를 왕창 만들었었다. 나 자신부터 잘했어야 됐는데...

 

이글을 쓰게된 결정적인 계기는 이전에 긱뉴스에서 https://aws.amazon.com/ko/blogs/korea/werner-vogels-lesson-learned-for-good-api-design/ 글을 보고 나중에 "내가 다시 Restful API 버전으로 다시 써야겠다!" 라고 생각하고 있었지만 잊어버리고 있다가 이제서야 부랴부랴 정리하게 되었다. 어떻게 보면 기본적인 내용들이라고 생각할 수 있겠지만 다시 복기하면서 기본기를 다시 탄탄히 다지고 내가 놓치고 있었던 점들을 다시 생각하게 되는 글이 되었으면 좋겠다.