초보 SaaS 빌더를 위한 실용적인 공개 API 설계 가이드: 버전 관리, 페이지네이션, 레이트 리밋, 문서화, 빠르게 배포할 수 있는 작은 SDK 선택 방법을 다룹니다.

공개 API는 단순히 앱이 노출한 엔드포인트가 아닙니다. 이는 팀 외부 사람들에게 제품이 변경되더라도 계약이 계속 작동할 것이라는 약속입니다.
힘든 부분은 v1을 쓰는 게 아니라, 버그를 고치고 기능을 추가하며 고객이 실제로 무엇을 원하는지 배우는 동안 그것을 안정적으로 유지하는 것입니다.
초기 선택들은 나중에 지원 티켓으로 나타납니다. 응답 구조가 경고 없이 바뀌거나, 명명 규칙이 일관되지 않거나, 클라이언트가 요청이 성공했는지 알기 어렵다면 마찰이 생깁니다. 그 마찰은 불신으로 바뀌고, 불신은 사람들이 당신의 플랫폼 위에 구축하기를 멈추게 합니다.
속도도 중요합니다. 대부분의 처음 SaaS 빌더는 유용한 것을 빠르게 내놓고 개선해야 합니다. 타협은 간단합니다: 규칙 없이 더 빨리 내보낼수록 실제 사용자가 왔을 때 그 결정을 되돌리는 데 더 많은 시간을 쓰게 됩니다.
v1에 대해 충분히 좋은 것은 보통 실제 사용자 행동에 매핑되는 소수의 엔드포인트, 일관된 명명과 응답 형태, 명확한 변경 전략(심지어는 단순한 v1 규칙이라도), 예측 가능한 페이지네이션과 합리적 레이트 리밋, 그리고 보내야 할 것과 받게 될 것을 정확히 보여주는 문서입니다.
구체적 예: 고객이 매일 밤 인보이스를 생성하는 통합을 만들었다고 합시다. 나중에 필드 이름을 바꾸거나 날짜 형식을 변경하거나 부분 결과만 반환하기 시작하면 그들의 작업은 새벽 2시에 실패합니다. 그들은 자신의 코드가 아니라 당신의 API를 탓할 것입니다.
Koder.ai 같은 채팅 기반 도구로 빠르게 많은 엔드포인트를 생성할 수 있습니다. 괜찮지만 공개 표면은 작게 유지하세요. 내부 엔드포인트는 학습하는 동안 비공개로 유지해 장기 계약에 포함될 것을 결정할 수 있습니다.
좋은 공개 API 설계는 고객이 제품을 어떻게 말하는지에 맞는 소수의 명사(리소스)를 선택하는 것으로 시작합니다. 내부 데이터베이스가 바뀌더라도 리소스 이름은 안정적으로 유지하세요. 기능을 추가할 때는 핵심 리소스 이름을 바꾸기보다 필드나 새 엔드포인트를 추가하는 편이 낫습니다.
많은 SaaS 제품에 대한 실용적 시작 세트는: users, organizations, projects, events입니다. 리소스를 한 문장으로 설명할 수 없다면 그것은 공개하기에 준비되지 않은 것입니다.
HTTP 사용을 지루하고 예측 가능하게 유지하세요:
인증은 첫날부터 복잡할 필요는 없습니다. API가 주로 서버 간(고객이 자신의 백엔드에서 호출)이라면 API 키로 충분한 경우가 많습니다. 고객이 개별 최종 사용자로 행동해야 하거나 사용자 권한 부여가 있는 서드파티 통합을 예상한다면 OAuth가 더 적합합니다. 호출자가 누구인지, 어떤 데이터에 접근 가능한지를 평이한 언어로 적어두세요.
초기에 무엇이 지원되는지와 노력 기반(best effort)인지를 명확히 하세요. 예를 들어: 리스트 엔드포인트는 안정적이고 하위 호환이 보장되지만 검색 필터는 확장될 수 있으며 완전하다고 보장되지 않는다고 명시하세요. 이렇게 하면 지원 티켓이 줄고 개선 여지가 생깁니다.
Koder.ai 같은 플랫폼 위에서 빌드한다면 API를 제품 계약으로 취급하세요: 처음에는 계약을 작게 유지하고 실제 사용을 기반으로 확장하세요.
버전 관리는 대부분 기대치 관리입니다. 클라이언트는: 내 통합이 다음 주에 깨질까?라고 알고 싶어합니다. 당신은 두려움 없이 개선할 공간이 필요합니다.
헤더 기반 버전 관리는 깔끔해 보일 수 있지만 로그, 캐시, 지원 스크린샷에서 숨기기 쉽습니다. URL 버전 관리가 보통 가장 단순한 선택입니다: /v1/.... 고객이 실패하는 요청을 보낼 때 버전을 즉시 확인할 수 있고 v1과 v2를 나란히 운영하기도 쉽습니다.
잘 동작하던 클라이언트가 코드를 바꾸지 않아도 작동을 멈춘다면 그 변화는 깨는 변경입니다. 일반적 사례:
customer_id를 customerId로)안전한 변경은 이전 클라이언트가 무시할 수 있는 변경입니다. 새 선택적 필드 추가는 보통 안전합니다. 예: GET /v1/subscriptions 응답에 plan_name을 추가해도 status만 읽는 클라이언트는 깨지지 않습니다.
실용적 규칙: 같은 메이저 버전 안에서 필드를 제거하거나 재목적화하지 마세요. 새 필드를 추가하고 기존 필드는 유지하며, 전체 버전을 폐기할 준비가 되었을 때만 제거하세요.
단순하게 유지하세요: 사용중지 공지를 조기에 하고 응답에 명확한 경고를 반환하며 종료 날짜를 제시하세요. 첫 API에는 90일 창이 현실적인 경우가 많습니다. 그 기간 동안 v1을 유지하고 짧은 마이그레이션 노트를 게시하며 지원 팀이 한 문장으로 안내할 수 있게 하세요: v1은 이 날짜까지 작동합니다; v2에서 무엇이 변경되었는지.
Koder.ai 같은 플랫폼 위에서 빌드한다면 API 버전을 스냅샷처럼 취급하세요: 개선은 새 버전으로 배포하고, 오래된 버전은 안정적으로 유지한 뒤 고객에게 이전할 시간을 주고 종료하세요.
페이지네이션은 신뢰를 얻거나 잃는 지점입니다. 요청 사이에 결과 순서가 바뀌면 사람들이 API를 신뢰하지 않습니다.
데이터셋이 작고 쿼리가 단순하며 사용자가 페이지 3을 자주 보는 경우에는 page/limit를 사용하세요. 리스트가 커지고 새 항목이 자주 들어오며 사용자가 정렬/필터를 많이 하는 경우에는 커서 기반 페이지네이션을 사용하세요. 커서 기반은 새 레코드가 추가되어도 순서를 안정적으로 유지합니다.
몇 가지 규칙으로 페이지네이션을 신뢰 가능하게 유지하세요:
토탈(total_count)은 까다롭습니다. 필터가 있는 큰 테이블에서는 비용이 비쌀 수 있습니다. 저렴하게 제공할 수 있다면 포함하세요. 그렇지 않으면 생략하거나 쿼리 플래그로 선택 가능하게 만드세요.
간단한 요청/응답 예시는 다음과 같습니다.
// Page/limit
GET /v1/invoices?page=2\u0026limit=25\u0026sort=created_at_desc
{
"items": [{"id":"inv_1"},{"id":"inv_2"}],
"page": 2,
"limit": 25,
"total_count": 142
}
// Cursor-based
GET /v1/invoices?limit=25\u0026cursor=eyJjcmVhdGVkX2F0IjoiMjAyNi0wMS0wOVQxMDozMDowMFoiLCJpZCI6Imludl8xMDAifQ==
{
"items": [{"id":"inv_101"},{"id":"inv_102"}],
"next_cursor": "eyJjcmVhdGVkX2F0IjoiMjAyNi0wMS0wOVQxMDoyNTowMFoiLCJpZCI6Imludl8xMjUifQ=="
}
레이트 리밋은 엄격하게 적용하려는 것보다 온라인 상태를 유지하려는 목적이 더 큽니다. 트래픽 급증으로부터 앱을 보호하고, 비싼 쿼리를 너무 자주 호출해 데이터베이스를 보호하며, 인프라 비용의 놀라움을 막습니다. 제한은 또한 계약입니다: 클라이언트는 정상 사용량이 무엇인지 알 수 있습니다.
간단하게 시작하고 나중에 조정하세요. 일반 사용량을 커버하고 짧은 버스트 여지를 둔 값을 선택한 뒤 실제 트래픽을 관찰하세요. 데이터가 전혀 없다면 안전한 기본값으로는 API 키당 분당 60 요청과 약간의 버스트 허용이 있습니다. 특정 엔드포인트가 훨씬 무거운 경우(검색이나 내보내기 등)에는 전체 요청을 벌주기보다 그 엔드포인트에 더 엄격한 제한이나 별도의 비용 규칙을 적용하세요.
제한을 적용할 때 클라이언트가 올바르게 행동하기 쉽도록 하세요. 429 Too Many Requests 응답을 반환하고 표준 헤더 몇 개를 포함하세요:
X-RateLimit-Limit: 윈도우 내 최대 허용량X-RateLimit-Remaining: 남은 허용량X-RateLimit-Reset: 윈도우가 리셋되는 시간(타임스탬프나 초)Retry-After: 재시도까지 기다려야 할 시간클라이언트는 429을 정상 상황으로 취급해야 합니다. 정중한 재시도 패턴은 양쪽을 만족시킵니다:
Retry-After가 있으면 그 시간만큼 기다립니다예: 고객이 야간 동기화로 API를 많이 호출한다면, 작업을 1분에 걸쳐 분산하고 429 발생 시 자동으로 속도를 늦추게 하면 전체 작업이 실패하지 않습니다.
오류 메시지가 읽기 어렵다면 지원 티켓이 빠르게 쌓입니다. 하나의 오류 형태를 선택하고(500 포함) 전역적으로 일관되게 사용하세요. 간단한 표준은: code, message, details, 그리고 사용자가 지원 채팅에 붙여 넣을 수 있는 request_id입니다.
작고 예측 가능한 형식 예시는 다음과 같습니다:
{
"error": {
"code": "validation_error",
"message": "Some fields are invalid.",
"details": {
"fields": [
{"name": "email", "issue": "must be a valid email"},
{"name": "plan", "issue": "must be one of: free, pro, business"}
]
},
"request_id": "req_01HT..."
}
}
HTTP 상태 코드를 항상 동일하게 사용하세요: 입력 오류는 400, 인증 누락/잘못은 401, 인증되었으나 권한 없음은 403, 리소스 없음은 404, 충돌(중복 고유값 등)은 409, 레이트 리밋은 429, 서버 오류는 500. 일관성이 창의성보다 낫습니다.
검증 오류는 고치기 쉽게 만드세요. 필드 수준 힌트는 내부 DB 컬럼이 아니라 문서에서 사용하는 정확한 파라미터 이름을 가리켜야 합니다. 형식 요구(날짜, 통화, 열거형)가 있다면 허용 형식을 말하고 예제를 보여주세요.
재시도는 많은 API가 실수로 중복 데이터를 생성하게 하는 지점입니다. 중요한 POST 작업(결제, 인보이스 생성, 이메일 전송)은 클라이언트가 안전하게 재시도할 수 있도록 아이덴포텐시 키를 지원하세요.
Idempotency-Key 헤더를 허용하세요.그 한 헤더는 네트워크 불안정이나 클라이언트 타임아웃 상황에서 많은 골치 아픈 엣지 케이스를 막아줍니다.
간단한 SaaS를 운영한다고 가정합시다. 주요 객체는 projects, users, invoices 세 가지입니다. 프로젝트는 여러 사용자를 갖고, 각 프로젝트는 월별 인보이스를 받습니다. 클라이언트는 인보이스를 회계 도구로 동기화하고 자신의 앱에 기본 청구 정보를 표시하고 싶어합니다.
깨끗한 v1 예시는 다음과 같습니다:
GET /v1/projects/{project_id}
GET /v1/projects/{project_id}/invoices
POST /v1/projects/{project_id}/invoices
이제 깨는 변경이 발생했습니다. v1에서는 인보이스 금액을 센트 정수로 저장했습니다: amount_cents: 1299. 나중에 다중 통화와 소수점이 필요해져 amount: "12.99"와 currency: "USD"로 바꾸고 싶습니다. 기존 필드를 덮어쓰면 모든 통합이 깨집니다. 버전 관리는 공황을 피하게 합니다: v1을 안정적으로 유지하고 /v2/...로 새 필드를 제공한 뒤 클라이언트가 마이그레이션할 때까지 둘 다 지원하세요.
인보이스 목록을 위해 예측 가능한 페이지네이션 형태를 사용하세요. 예:
GET /v1/projects/p_123/invoices?limit=50\u0026cursor=eyJpZCI6Imludl85OTkifQ==
200 OK
{
"data": [ {"id":"inv_1001"}, {"id":"inv_1000"} ],
"next_cursor": "eyJpZCI6Imludl8xMDAwIn0="
}
어느 날 고객이 인보이스를 루프에 넣어 가져오다 레이트 리밋에 걸립니다. 무작위 실패 대신에 명확한 응답을 받습니다:
429 Too Many RequestsRetry-After: 20{ "error": { "code": "rate_limited" } }클라이언트 쪽에서는 20초 동안 대기한 뒤 동일한 cursor에서 계속할 수 있어 모든 것을 다시 다운로드하거나 중복 인보이스를 생성하지 않습니다.
v1 출시를 엔드포인트 더미가 아닌 작은 제품 릴리스로 취급하면 더 잘 됩니다. 목표는 단순합니다: 사람들이 그 위에 구축할 수 있고 당신은 놀라움 없이 개선할 수 있어야 합니다.
먼저 API가 무엇을 위해 존재하는지, 무엇이 아닌지를 설명한 한 페이지를 작성하세요. 표면적 범위가 한 사람에게 1분 내로 설명될 수 있을 만큼 작게 유지하세요.
다음 순서를 따라 각 단계가 충분히 괜찮해질 때까지 넘어가지 마세요:
코드 생성 워크플로(예: Koder.ai로 엔드포인트와 응답을 스캐폴딩)로 빌드한다면도 가짜 클라이언트 테스트는 반드시 하세요. 생성된 코드는 겉보기엔 맞아도 사용하기 불편할 수 있습니다.
이 절차의 보상은 적은 지원 이메일, 적은 긴급 수정 릴리스, 그리고 실제로 유지할 수 있는 v1입니다.
첫 SDK는 별도의 제품이 아닙니다. HTTP API 주위의 얇고 친절한 래퍼로 생각하세요. 흔한 호출을 쉽게 만들되 API 동작을 숨기면 안 됩니다. 아직 래핑하지 않은 기능이 필요하면 원시 요청으로 빠져들 수 있어야 합니다.
고객이 실제로 사용하는 언어 하나를 선택하세요. 많은 B2B SaaS API의 경우 JavaScript/TypeScript 또는 Python이 일반적입니다. 완성도가 낮은 세 개를 내놓는 것보다 한 개를 잘 만드는 것이 낫습니다.
초기 좋은 구성은:
이것은 수작업으로 만들거나 OpenAPI 스펙에서 생성할 수 있습니다. 스펙이 정확하고 일관된 타입이 필요하면 생성이 좋지만 초기에는 많은 코드를 생산할 수 있습니다. 초반에는 수작업으로 만든 최소 클라이언트와 문서를 위한 OpenAPI 파일이면 충분한 경우가 많습니다. 공개 SDK 인터페이스가 안정적이라면 나중에 생성된 클라이언트로 전환해도 사용자를 깨뜨리지 않을 수 있습니다.
API 버전은 호환성 규칙을 따릅니다. SDK 버전은 패키징 규칙을 따릅니다.
새 선택 파라미터나 엔드포인트를 추가하면 보통은 마이너 SDK 버전 증가입니다. SDK 자체의 깨는 변경(메서드 이름 변경, 기본값 변경)은 메이저 SDK 릴리스로 남기세요. 이렇게 분리하면 업그레이드가 평온해지고 지원 티켓이 줄어듭니다.
대부분의 API 지원 티켓은 버그 때문이 아닙니다. 놀라움 때문입니다. 공개 API 설계는 주로 지루하고 예측 가능하게 만들어 클라이언트 코드가 월별로 계속 작동하게 하는 것입니다.
신뢰를 가장 빠르게 잃는 방법은 응답을 아무에게도 알리지 않고 변경하는 것입니다. 필드 이름을 바꾸거나 타입을 바꾸거나 이전에는 값이었던 것이 null로 바뀌면 클라이언트를 깨뜨립니다. 정말로 동작을 바꿔야 한다면 버전을 올리거나 새 필드를 추가하고 충분 기간 동안 기존 필드를 유지한 뒤 명확한 종료 계획을 제시하세요.
페이지네이션도 반복적으로 문제를 일으킵니다. 하나의 엔드포인트는 page/pageSize, 다른 하나는 offset/limit, 또 다른 하나는 커서로 각기 다른 기본값을 사용하는 상황이 나오면 안 됩니다. v1에는 하나의 패턴을 골라 전역적으로 적용하세요. 정렬도 안정적으로 유지해 새 레코드가 들어올 때 다음 페이지가 항목을 건너뛰거나 중복하지 않도록 하세요.
오류가 일관되지 않으면 많은 왕복이 발생합니다. 한 서비스는 { "error": "..." }를 반환하고 다른 서비스는 { "message": "..." }를 반환하면 클라이언트는 엔드포인트별로 지저분한 핸들러를 만들어야 합니다.
가장 긴 이메일 스레드를 만드는 다섯 가지 실수는:
간단한 습관: 모든 응답에 request_id를 포함하고 모든 429에는 재시도 시점을 설명하세요.
무엇을 공개하기 전에 일관성에 초점을 맞춰 최종 점검을 하세요. 대부분의 지원 티켓은 작은 디테일이 엔드포인트, 문서, 예제에서 일치하지 않기 때문에 발생합니다.
가장 많은 문제를 잡는 빠른 점검:
출시 후에는 사람들이 실제로 무엇을 호출하는지 관찰하세요. 작은 대시보드와 주간 검토가 초반에는 충분합니다.
먼저 모니터할 신호:
피드백을 수집하되 모든 것을 다시 쓰지 마세요. 문서에 짧은 이슈 경로를 추가하고 각 보고서에 엔드포인트, request id, 클라이언트 버전을 태그하세요. 무엇을 고치면 추가 필드, 새 선택 파라미터, 새 엔드포인트 등 가산적 변경을 우선하세요. 기존 동작을 깨는 변경은 피하세요.
다음 단계: 리소스, 버전 계획, 페이지네이션 규칙, 오류 형식을 담은 한 페이지 API 스펙을 작성하세요. 그런 다음 문서와 인증 및 핵심 2~3개 엔드포인트를 포함한 아주 작은 스타터 SDK를 만드세요. 더 빨리 움직이고 싶다면 Koder.ai 같은 채팅 기반 계획을 사용해 스펙, 문서, 초간단 SDK를 초안으로 만들 수 있습니다(플래닝 모드는 엔드포인트와 예제를 매핑하는 데 유용합니다).
시작은 5–10개의 엔드포인트가 적절합니다. 이 엔드포인트들은 실제 고객 행동에 대응해야 합니다.
좋은 규칙: 리소스를 한 문장으로 설명할 수 없으면(무엇인지, 누가 소유하는지, 어떻게 쓰이는지) 공개하지 말고 사용성을 보고 결정하세요.
고객들이 대화에서 이미 사용하는 작고 안정적인 명사 집합(리소스)을 골라 그 이름을 내부 데이터베이스가 바뀌더라도 안정적으로 유지하세요.
SaaS의 일반적인 시작 리소스는 users, organizations, projects, events입니다—명확한 수요가 있을 때만 추가하세요.
표준 의미를 사용하고 일관성을 지키세요:
GET = 읽기(사이드 이펙트 없음)POST = 생성 또는 작업 시작PATCH = 일부 필드 업데이트DELETE = 제거 또는 비활성화예측 가능성이 핵심입니다: 클라이언트가 메서드의 동작을 추측하면 안 됩니다.
기본적으로 /v1/... 같은 URL 버전 관리를 권장합니다.
로그나 스크린샷에서 버전을 바로 확인할 수 있고, 디버깅과 고객 지원이 쉽습니다. 또 v1과 v2를 나란히 운영하기가 간단합니다.
클라이언트가 코드를 바꾸지 않아도 동작이 멈출 수 있다면 그 변화는 깨는 변경입니다. 일반적 사례:
새 선택적 필드 추가는 대체로 안전합니다.
단순하게 유지하세요:
첫 API에는 90일 창이 현실적입니다. 그 기간 동안 v1을 유지하고 짧은 마이그레이션 노트를 게시하세요.
모든 리스트 엔드포인트에 대해 하나의 패턴을 선택하고 일관되게 유지하세요.
항상 기본 정렬과 동률 해소(tie-breaker, 예: created_at + id)를 정의해 결과가 요청 사이에 뒤섞이지 않게 하세요.
일관된 키당 제한부터 시작하고 실제 트래픽을 보며 조정하세요(예: 분당 60 요청 + 소폭 버스트 허용).
제한 시에는 429을 반환하고 다음 헤더들을 포함하세요:
X-RateLimit-LimitX-RateLimit-RemainingX-RateLimit-Reset항상 서비스 전반에 하나의 오류 포맷을 사용하세요(500 포함). 실용적인 구조는:
code (안정된 식별자)message (사람이 읽을 수 있는 설명)details (필드 수준 문제)request_id (지원용)상태 코드는 일관되게 사용하세요(400/401/403/404/409/429/500).
많은 엔드포인트를 빠르게 생성하더라도(예: Koder.ai를 사용) 공개 표면은 작게 유지하고 그것을 장기 계약으로 취급하세요.
출시 전에는:
POST 액션에 아이덴포텐시 키를 추가하세요그 후 인증, 타임아웃, 안전한 요청 재시도, 페이지네이션을 돕는 작은 SDK를 공개하세요—단, HTTP 동작을 숨기진 마세요.
Retry-After이렇게 하면 재시도가 예측 가능해져 지원 요청이 줄어듭니다.