API 설계의 기본 원칙과 실무 판단 기준
API를 서비스의 인터페이스로 보고, REST가 HTTP 의미체계를 어떻게 계약으로 활용하는지 설명한다. PUT과 PATCH, 버전 관리, 페이지네이션, 응답 형식, 멱등성 키처럼 리뷰와 디버깅에서 바로 부딪히는 판단 기준을 정리한다.
Script Companion
오디오와 함께 스크립트 보기
- 01
API는 서비스의 문이다. 문이 체계적이지 않으면 사용하는 사람은 매번 헷갈리고, 만드는 사람도 유지보수 비용을 계속 치른다. 내부 API를 리뷰하거나 외부 서비스와 연동할 때 중요한 질문은 단순히 동작하느냐가 아니라, 이 API가 어떤 규칙으로 설계되었고 그 규칙이 일관적인가이다. 그래서 API 설계의 기본은 프론트엔드, 백엔드, 외부 연동을 잇는 공통 언어를 이해하는 일에 가깝다. 특히 BackOps처럼 내부 도구와 외부 서비스를 함께 다루는 환경에서는 요청이 잘못된 것인지, 응답 구조가 문제인지, 버전 변경이 클라이언트를 깨뜨리는지 판단해야 한다.
- 02
REST는 갑자기 등장한 스타일이 아니라, SOAP와 XML-RPC가 웹 환경에서 드러낸 한계를 풀기 위해 자리 잡았다. 당시 방식은 HTTP를 단순한 운반 수단처럼 쓰면서 조회든 수정이든 POST와 XML envelope에 밀어 넣었다. 그러면 GET은 안전한 조회라는 의미, PUT은 멱등적 수정이라는 의미, DELETE는 삭제라는 의미가 사라진다. 모든 요청이 POST가 되면 CDN, 브라우저 캐시, 중간 프록시 캐시도 제대로 힘을 쓰기 어렵다. 문서에서는 SOAP XML envelope가 약 5KB이고 REST JSON이 약 1KB인 비교, envelope 하나당 약 30ms 처리 오버헤드, 2017년 기준 전체 API의 약 83%가 REST였다는 점도 함께 제시한다.
- 03
REST가 푼 방식의 핵심은 HTTP가 이미 가진 의미체계를 인터페이스 계약으로 드러내는 것이다. URL은 동사가 아니라 사용자, 주문 같은 명사형 리소스를 가리키고, GET, POST, PUT, PATCH, DELETE가 행위의 의미를 맡는다. 그래서 getUsers 같은 별도 동사를 URL에 넣기보다, 어떤 리소스에 어떤 HTTP 메서드를 보냈는지가 API의 의도를 설명한다. 이 전제가 있어야 PUT과 PATCH의 차이, URL 버전 관리, Idempotency-Key 같은 뒤의 결정도 의미를 가진다. HTTP Basics가 단순 전송 프로토콜 소개가 아니라 REST가 빌려 쓰는 표준 의미체계인 이유도 여기에 있다.
- 04
PUT과 PATCH는 둘 다 수정처럼 보이지만 실패 방식이 다르기 때문에 구분해야 한다. PUT은 서류 전체를 새 서류로 교체하는 방식이고, 같은 PUT 요청을 여러 번 보내도 최종 결과가 같아야 하는 멱등성을 가진다. 실무에서는 PUT 요청에 리소스의 모든 필드를 포함해야 하며, 빠진 필드는 null이나 기본값으로 덮어써질 수 있다. 반대로 PATCH는 특정 칸만 고치는 방식이다. 일부 필드만 바꿀 때 PATCH를 써야 하며, 배열에 항목을 추가하는 PATCH처럼 실행 횟수에 따라 결과가 달라질 수 있어 항상 멱등성이 보장되지는 않는다. PUT을 잘못 쓰면 200 OK가 돌아와도 기존 데이터가 조용히 사라질 수 있다.
- 05
API가 바뀔 때는 기존 클라이언트를 보호하기 위해 버전 관리가 필요하다. 문서는 URL Path, Query Parameter, Header, Content Negotiation 네 가지 전략을 소개하고, 그중 URL Path가 가장 직관적이고 많이 쓰인다고 설명한다. Query Parameter는 구현이 쉽지만 캐싱에 불리하고, Header 방식은 URL이 깔끔하지만 눈에 잘 보이지 않으며, Content Negotiation은 세밀한 제어가 가능하지만 복잡하다. 중요한 것은 어떤 방식을 고르든 팀 안에서 하나의 전략을 일관되게 쓰는 것이다. 좋은 응답도 같은 원리다. 성공과 실패 모두 일관된 JSON 구조를 쓰고, 200, 201, 400, 404, 500 같은 상태 코드를 명확히 쓰며, 날짜 형식은 ISO 8601로 통일한다.
- 06
400과 422의 차이도 API 응답 설계에서 자주 부딪힌다. 400 Bad Request는 JSON 파싱이 안 되는 것처럼 요청 구조 자체가 잘못된 경우이고, 422 Unprocessable Entity는 구조는 맞지만 이메일 형식 오류나 필수 필드 누락처럼 유효성 검증에 실패한 경우다. NestJS의 기본 ValidationPipe는 400을 반환하지만 exceptionFactory로 422로 바꿀 수 있다. 여기서 핵심은 어떤 코드가 더 멋진가가 아니라 팀의 에러 코드 체계를 통일하는 것이다. 응답 구조가 엔드포인트마다 다르면 프론트엔드는 어떤 API는 data로 감싸고 어떤 API는 바로 객체를 반환하는 차이를 매번 처리해야 한다. 이런 경우 NestJS Interceptor나 글로벌 예외 필터로 형식을 전역에서 맞추는 것이 문서의 방향이다.
- 07
페이지네이션은 데이터가 많을 때 한 번에 모두 보내지 않고 나누어 보내는 설계다. Offset 방식은 구현이 단순하고 페이지 번호 탐색이나 어드민 테이블에 잘 맞지만, 데이터가 추가되거나 삭제되면 페이지 경계가 흔들릴 수 있다. Cursor 방식은 책갈피처럼 특정 위치를 기준으로 이어 읽기 때문에 알림, 타임라인 같은 실시간 피드에서 더 안정적이다. 문서의 기준은 엔드포인트 단위로 판단하라는 쪽이다. 수만 건 이하이고 읽기 위주이며 OFFSET 100 이하, 응답 50ms 미만이면 Offset을 유지할 수 있다. 반대로 수십만 건 이상이거나 삽입과 삭제가 잦고, OFFSET 1,000 이상 또는 응답 100ms 초과라면 Cursor 전환을 검토한다. PingCAP 사례처럼 Offset은 OFFSET 100,000에서 30ms 이상으로 급등하지만 Cursor는 0.025에서 0.027ms 수준으로 일정하게 유지된다.
- 08
Cursor를 쓴다고 모든 문제가 사라지는 것은 아니다. created_at처럼 unique가 아닌 컬럼만으로 정렬과 커서를 잡으면 같은 timestamp를 가진 행들의 순서가 쿼리마다 바뀌어 중복이나 누락이 생길 수 있다. 응답은 200 OK라서 모니터링이 놓칠 수 있는 silent failure가 된다. Offset에서도 비슷한 문제가 있다. 같은 페이지를 두 번 요청했는데 결과가 다르거나 첫 페이지와 두 번째 페이지에 같은 항목이 보이면, 그 사이 새 데이터가 앞에 추가되어 기존 항목이 밀렸을 수 있다. 그래서 페이지네이션 선택은 전체 시스템 일괄 전환이 아니라 알림 피드, 주문 이력, 어드민 사용자 목록처럼 엔드포인트의 데이터 크기, 쓰기 빈도, UX, 일관성 요구를 따로 봐야 한다.
- 09
멱등성 키는 같은 POST 요청이 두 번 실행되지 않게 하는 중복 접수 방지 번호표다. 네트워크 장애로 클라이언트가 타임아웃을 받으면 같은 요청을 재시도할 수 있는데, 서버가 Idempotency-Key를 기준으로 이전 처리 결과를 기억하지 않으면 결제가 두 번 되거나 주문이 중복 생성될 수 있다. Stripe와 Toss 같은 결제 API가 이 헤더를 중요하게 다루는 이유가 여기에 있다. 다만 문서는 중요한 함정도 짚는다. Stripe는 24시간 동안 500을 포함한 응답을 통째로 캐시한다고 명시한다. 첫 요청이 일시적 DB 장애로 500을 받았다면 같은 키로 다시 시도해도 캐시된 500이 돌아올 수 있다. 복구는 새 idempotency_key로 재시도하는 것이고, 사전에는 retryable 오류와 non-retryable 오류를 나누어 같은 키를 재사용할지 새 키를 발급할지 결정해야 한다.
- 10
실무 연결점은 몇 가지 빠른 판단으로 정리할 수 있다. 새 리소스 생성은 POST와 201 Created, 전체 교체는 모든 필드를 포함한 PUT, 일부 수정은 PATCH로 생각한다. 데이터가 많아 느리다면 페이지네이션을 보되 실시간 피드인지, 어드민 테이블인지에 따라 Offset과 Cursor를 나눈다. API 에러 메시지와 응답 구조가 제각각이면 글로벌 ExceptionFilter나 Interceptor로 형식을 통일한다. API를 바꿔야 하지만 기존 클라이언트를 유지해야 한다면 URL 버전 관리처럼 팀이 합의한 전략을 적용한다. 정리하면, API 설계의 기본은 HTTP 메서드와 리소스 URL의 의미를 지키고, 응답과 에러와 버전을 일관되게 관리하며, 실패가 조용히 지나가는 지점을 미리 줄이는 것이다.
같은 레이어