좋은 API 설계에 대해 내가 아는 모든 것
(seangoedecke.com)- 소프트웨어 엔지니어링에서 API는 핵심 도구이며, 좋은 API는 지루할 정도로 익숙하고 단순한 것이 바람직한 특징
- API는 한 번 공개되면 변경이 어렵기 때문에 사용자 환경을 깨지 않는 원칙(WE DO NOT BREAK USERSPACE) 이 중요함
- 불가피하게 변경할 경우 버전 관리(versioning) 가 필요하지만, 이는 복잡성과 유지보수 비용을 크게 증가시키는 필요악임
- API 품질은 결국 제품 자체의 가치에 의존하며, 잘못 설계된 제품은 좋은 API를 만들기 어렵게 함
- 안정성과 확장성을 위해 API 키 기반 인증, 멱등성(idempotency), 레이트 리미트, 커서 기반 페이지네이션 등을 고려해야 함
서론: API 설계의 중요성과 맥락
- 현대 소프트웨어 엔지니어의 주요 업무 중 하나는 API와 상호작용하는 것임
- 작성자 역시 REST, GraphQL, 명령줄 도구 등 다양한 형태의 공개 및 사내용 API를 설계/구현/활용한 경험을 보유함
- 현존하는 API 설계 조언들은 복잡한 개념(REST의 정의, HATEOAS 등)에 집착하는 경향이 있음
- 본 글은 실제 경험을 바탕으로 실용적인 API 설계 원칙을 정리한 것임
친숙함과 유연성의 균형: 좋은 API의 첫 번째 조건
- 좋은 API는 '평범하고 지루한' API, 즉 기존에 접해본 API들과 사용법이 비슷해야 함
- 사용자는 API 자체보다는 본인의 목적 달성에 집중하기 때문에 진입장벽이 낮은 설계가 필요함
- 한 번 공개된 API는 변경이 매우 어려워, 최초 설계단계에서 신중함이 요구됨
- 개발자는 최대한 간결한 API를 원하면서도, 장기적인 유연성을 남기기 위한 고민이 항상 따름
- 결과적으로, 친숙함과 장기 유연성 간의 균형이 핵심 과제임
사용자 공간을 절대 깨지 않는다 (WE DO NOT BREAK USERSPACE)
- 기존의 응답 구조에서 필드를 추가하는 변화는 대부분의 경우 문제없음
- 하지만 필드 제거, 타입이나 구조 변경은 모든 소비자 코드를 깨뜨리는 결과를 초래함
- API 유지자는 기존 사용자의 소프트웨어를 고의로 망가뜨리지 않을 책임이 있음
- HTTP의 "referer" 헤더 오타조차 고치지 않는 이유는 사용자 공간을 보존하는 문화 때문임
API를 깨뜨리지 않고 변경하기: 버전 관리 전략
- 필수적일 때만 API에 파괴적 변경을 허용하며, 이때는 버전 관리가 정답임
- 구버전과 신버전을 동시에 운영하면서 점진적 전환을 유도해야 함
- 버전 식별자는 URL(
/v1/
), 헤더 등 다양한 방식 활용 가능하며, 사용자는 각자 속도에 맞게 전환 가능함 - 버전 관리에는 엄청난 유지보수 비용(엔드포인트 증가, 테스트, 지원)과 사용자 혼동이라는 단점이 존재함
- Stripe처럼 내부 트랜스레이션 계층을 두더라도, 근본적인 복잡성은 피할 수 없음
- API 버전 관리 도입은 최후의 수단이어야 함
API의 성공 요인은 전적으로 프로덕트 가치에 달려있음
- API는 본질적으로 실제 비즈니스 제품의 인터페이스에 불과함
- OpenAI, Twilio 등 API도 결국 사용자가 원한 것은 API가 제공하는 기능 그 자체임
- 가치 있는 프로덕트라면 API가 불편해도 사용하게 됨
- API 품질은 "마진" 특성: 본질적 경쟁력이 비슷할 때만 선택 요소가 됨
- 반면, 아예 API가 없는 제품은 기술 사용자에게 큰 장애물임
프로덕트 설계가 나쁘면 API도 좋아질 수 없음
- 기술적으로 완성도 높은 API가 있어도, 시장성이 없는 제품이면 의미 없음
- 더 중요한 것은, 기본 리소스 구조가 비논리적이거나 비효율적이라면 API에서도 드러남
- 예를 들어, 댓글을 링크드 리스트로 저장하는 시스템은 RESTful 설계조차 자연스럽게 나오기 어렵게 만듦
- UI에서는 숨겨질 수 있는 기술적 문제들이 API에서는 모두 노출되며, 사용자의 시스템 이해도를 불필요하게 강요함
인증(Authenticaton)과 사용자 다양성
- 긴 수명의 API 키 기반 인증을 반드시 지원해야 함
- OAuth 같은 보안성 높은 방식을 추가 지원하더라도, API 키의 진입장벽이 월등히 낮음
- API 소비자는 엔지니어뿐 아니라 비개발자(영업, 기획, 학생, 취미 개발자 등)도 많음
- 어렵거나 복잡한 인증 요구(OAuth 등)는 비전문 사용자에게 장벽이 됨
멱등성(Idempotency)과 재시도 처리
- 액션성 요청(예: 결제, 상태변경 등)은 실패 시 재시도(retry) 에 대한 안전성이 중요함
- 멱등성이란, 동일 요청을 여러 번 보내도 결과가 한 번만 처리됨을 보장하는 것임
- 표준 방법은 "멱등성 키"를 파라미터나 헤더로 전달하여 중복 처리 방지임
- 멱등성 키 저장은 Redis 등 단순 키/값 저장소로 충분하며, 대부분의 경우 주기적 만료를 적용해도 무방함
- 읽기/삭제 요청(REST 방식)에는 일반적으로 필요 없음
API 안전성과 속도 제한(Rate limiting)
- 코드를 통한 API 요청은 사용자의 조작보다 훨씬 빠른 속도로 발생 가능함
- 무심코 배포한 API 한 건이 의도치 않은 방식(예: 대규모 채팅 시스템)에 활용될 수 있음
- 속도 제한(ratelimit)은 반드시 필요하며, 비용이 높은 연산에는 더 엄격하게 적용되어야 함
- 특정 고객에 대한 일시적 API 비활성화(killswitch)도 선택지로 고려해야 함
- 응답 헤더(
X-Limit-Remaining
,Retry-After
등)로 속도 제한 정보를 안내해야 함
페이징(Pagination) 전략
- 대규모 데이터셋(예: 수백만 티켓)을 효율적으로 반환하려면 페이징이 필수임
- 오프셋 기반(Offset-based) 페이징은 간단하지만 대량 데이터에선 점차 느려짐
- 커서 기반(Cursor-based) 페이징은 쿼리 성능 저하 없이 아주 큰 데이터셋에도 효과적임
- 커서 기반은 구현과 활용이 다소 어렵지만, 장기적으로는 필수적 변화일 가능성 높음
- 응답에
next_page
필드 등을 포함해, 다음 요청의 커서를 명확히 안내하는 것이 현명함
선택적 필드 및 GraphQL에 대한 견해
- 비용이 크거나 느린 필드는 기본 응답에서 제외하고 필요시만 선택적으로 추가해야 함
-
includes
파라미터 등으로 연관 데이터 포함 가능 - GraphQL은 데이터 구조 유연성 장점이 있으나, 비개발자 접근성 저하, 캐싱/엣지케이스 복잡화, 뒷단 구현 난이도 등의 문제점 있음
- 실무 경험상 GraphQL 도입은 꼭 필요한 경우에 한정하는 것이 적합함
내부용 API에 대한 특징
- 사내 API는 외부(API 공개형)와는 여러 조건이 다름
- 소비자는 대부분 전문 소프트웨어 엔지니어이므로, 더 복잡한 인증이나 파괴적 변경 가능함
- 그래도, 멱등성과 사고 예방, 운영 부담 최소화를 위한 설계 원칙은 유효함
요약 정리
- API는 변경이 어렵고 사용은 쉬워야 하는 특성을 가짐
- 사용자 공간을 깨지 않는 것이 API 유지자의 가장 중요한 의무임
- API 버전 관리는 비용이 크기 때문에 최후의 수단으로만 활용해야 함
- 최종적으로 API의 품질은 프로덕트의 본질적 가치가 좌우함
- 잘못 설계된 프로덕트는 API 수준에서 보완해도 한계가 큼
- 간단한 인증 방식 지원, 필수 액션 요청엔 반드시 멱등성, 그리고 속도 제한/페이징 등 안정성 대책 중요함
- 내부 API는 용도와 대상에 따라 전략이 다르지만, 설계 신중함은 여전히 요구됨
- REST, JSON 등 포맷이나 OpenAPI 등은 본질적 논점이 아님. 명확한 문서화가 더 중요함
Hacker News 의견
-
"userspace를 절대 깨지 말라"는 조언이 유명하지만, 사실 그 반대 측면도 있다는 점을 잘 언급함. 즉, "커널 API는 예고 없이 깨질 수 있다"는 것임. 중요한 건 "모든 API를 아무렇게나 깨지 말라"가 아니라, "안정성을 선언한 부분만 절대 깨지 말라"는 미묘한 균형임
-
리눅스 커널이 userspace를 깨지 않는다고 해도, GNU libc는 굉장히 자주 userspace 호환성을 깸. 그래서 결과적으로 리눅스 사용자 공간은 커널 개발자들이 아무리 노력해도 깨지는 일이 빈번함. 새 버전 libc에서 빌드된 프로그램과 라이브러리는 하위 libc에서는 제대로 실행이 안 되기도 해서, 결국 모든 구성 요소를 한 번에 업그레이드해야 하는 실정임. 약간 아이러니하게도, 윈도우는 이미 수십 년 전에 redistributable 방식으로 이 문제를 해결했음
-
리눅스에는 유명하게도 안정적인 공개 드라이버 API가 없다는 점이 있는데, 이게 바로 구글이 Fuschia OS를 개발한 동기라고 들음. 리눅스는 사용자 공간과 하드웨어 모두에 대해 각기 다른 방식으로 방향성을 가진 셈임
-
-
글쓴이가 버전 기반 API를 별로 좋아하지 않는 듯하지만, 나는 애초에 앱을 만들 때부터 버전 관리를 반드시 도입하라고 항상 추천함. 미래를 예측할 수 없으니 언젠가 외부 요인으로 인해 깨지는 변경이 당신에게도 일어날 수밖에 없음
-
실제로 글쓴이도 버전 관리를 추천했다고 생각함. 본문에는 "버전은 API를 책임 있게 변경하는 방법"이라고 했으니, 결국 버전 관리 자체를 장려하는 셈임. 다만, 새 버전으로의 전환은 최후의 수단으로 하라고 함
-
나는 굳이 엔드포인트에 "v1"을 붙이지 말라는 의견에 동의함. 실제 API가 성장하면서 벌어지는 일은, 먼저 기존 엔드포인트에 필드나 옵션을 추가해서 하위 호환을 지키려고 노력함. 그리고, 완전히 호환이 안 되는 작업이 필요해지면, 보통 엔드포인트 이름 자체를 새로 짓고 아예 새로운 엔드포인트(/v2가 아니라)를 만듦. 만약 전체 API를 바꿔야 하면, 기존 서비스를 폐기하고 이름부터 새로 지은 전혀 다른 서비스를 런칭하게 됨. 25년간 일하면서 "/v1"과 "/v2"가 나란히 쓰이는 서비스를 딱 한 번만 봤음
-
저자의 의도가 처음부터 /v1을 엔드포인트에 넣지 말라는 건 아니라고 생각함. 요점은 새 버전(/v2)이 생기지 않도록 최선을 다해야 한다는 것임. /v2가 생기면 버그 픽스마다 양쪽에 다 코드 수정을 해야 하고, 조건 분기가 지수적으로 늘어나서 코드베이스가 스파게티처럼 복잡해짐. 결국 다중 버전을 지원하게 된 애초의 /v1 설계가 미래 호환에 대한 배려가 부족한 셈임
-
버전 관리를 나중에 추가하는 것도 문제없다고 봄. 예를 들어 처음에는 /api/posts로 시작하고, 다음 버전은 /api/v2/posts로 추가하면 충분함
-
처음부터 버전을 박아 넣는 방식에 동의하지 않음. 그렇게 하면 정말로 다중 버전이 자주 쓰이게 되는데, 그게 오히려 더 좋지 않다고 생각함
-
-
이 글 아주 유익했음. 여기에 한 가지 조언을 추가하겠음. API 문서를 얼마나 어렵게 얻을 수 있느냐와 API 품질은 반비례함. 만약 계약서에 사인해야만 문서를 얻을 수 있는 상황이라면, 그 API의 품질이 형편없을 것으로 가정해도 무방함
-
idempotency key를 comment 테이블에 따로 저장하는 대신, Redis 같은 key/value 저장소에 넣으라고 했는데, 모든 실패 케이스에서 이 방식이 확실한 idempotency를 보장할 수 있을지 궁금함. 가령 서버가 SET key 1 NX 같은 조건부 쓰기를 하다가 이미 key가 있는 걸 발견하면, 댓글 생성을 그냥 건너뛰어야 하는데, 이 시점에 앞선 요청이 실제로 DB에는 반영되지 않았을 수도 있음. idempotency key 저장은 실제 작업과 트랜잭션 단위로 같이 커밋되어야 하고, 필요시 롤백도 되어야 함. 결국 idempotency key 본질은 ‘이 작업 혹은 요청의 고유 ID’가 되어야 함. 예를 들어 “댓글 생성”, “댓글 업데이트” 등 각각에 맞는 리소스별 식별자여야 한다는 것임
- idempotency를 위한 별도 컴포넌트(예: redis 등)를 더하는 건 지양해야 함. 추상화가 깨지거나 이상하게 동작하는 문제 혹은 딜리버리 보장에 대한 이해 부족에서 오는 오류가 생길 수 있음. 대신에, 쓰기 작업에 라벨이나 메타데이터를 함께 저장하는 식으로, 사용자가 직접 진척 상황을 추적하고 기존 데이터와 함께 보관하는 방식이 훨씬 나음
-
커서 기반 페이지네이션의 장점은, 사용자 입장에서 페이지를 로드하고 ‘다음’ 버튼을 누르는 사이 새 아이템이 추가돼도, 기존에 봤던 항목을 또 보지 않아도 된다는 점임. 커서 방식은 이전 페이지의 마지막 객체 ID를 기록해두고 그 이후 아이템을 주니까, 무한 스크롤에 특히 유용함. 반면, 커서 기반은 “N번째 페이지로 점프” 기능을 만들기 어렵다는 단점이 있음
- 커서는 반드시 불투명하게(opaque) 만들어야 DB 크기 같은 것을 외부에 노출하지 않을 수 있음. 그리고 커서에 상태 정보(검색 파라미터, 캐시 상태, 라우팅 정보 등)를 인코딩해서 더 다양한 기능을 구현할 수도 있음
-
요즘 "API"라고 하면 대부분 웹앱에 요청 보내고, 파라미터랑 헤더 세팅해서 데이터 가져오는 걸 떠올리는데, 본래 API란 "Application Programming Interface" 즉, ‘애플리케이션 프로그램의 인터페이스’라는 뜻임. 1940년대에 처음 쓰였고, 그 뒤 1990년대까지는 거의 다른 의미 없이 사용됐음. API의 역사는 80년이 넘으며, 엄청난 옛날 자료들도 많음. 그때 프로그래머들이 어떤 문제를 다뤄서 어떻게 풀었는지 고민해보면, 오늘날 본인에게도 도움이 되는 부분이 있을 거라고 봄
-
내부 사용자를 단순히 '사용자'로만 본다는 의견에는 동의하지 않음. 비록 다들 더 기술적인 사람들이고, 프로그래머일 확률이 높아도, 이들도 바쁘고 자기 프로젝트에 집중하느라 API 변경에 대응할 시간이나 여유가 부족한 경우가 많음. 가능하면, 오픈하기 전에 팀 내부에서 "dogfooding"(실사용) 테스트를 충분히 하는 게 중요함. 일단 외부에 공개되면, ‘userspace를 깨지 않는다’는 약속을 반드시 지켜야 함
-
내부 사용자라면 직접 컨택해서 마이그레이션을 유도할 수 있는 계측 도구들이 보통 구현돼 있음. 덕분에 API 버전 폐기도 가능해서, 버전 관리를 전략적으로 도입하는 게 충분히 매력적임. 실제 API 버전 관리에 참여해봤고, 기본적으로 이걸 안 쓰는 조직과 비교해서도 확실히 효과를 봤음
-
버전 관리 방식은 이 문제를 푸는 데 도움이 된다고 봄. 내부 사용자를 배려하는 최선의 방법 중 하나는 스펙에 대해 협업하고, 그 스펙의 작업 중인 버전도 이해관계자들에게 공유하는 것임. 계속 업데이트되는 문서라 해도, 기준점을 잡아주면 내외부 피드백도 원활해지고, 굳이 정책적인 충돌 위험만 피하면 매우 유용하게 쓸 수 있음
-
-
idempotency key를 redis에 저장하는 대신, 가능하다면 실제 데이터를 기록하는 동일 트랜잭션 내에서 idempotency key도 함께 저장하는 게 더 확실하다고 생각함
-
"userspace를 절대 깨지 말라"는 경고는 정말 중요함. Spotify, Reddit, Twitter 등 최근 이 원칙을 무시해서 아쉬웠음
-
참고로 https://jcs.org/2023/07/12/api 링크에 좋은 API 관련 권장사항이 잘 정리되어 있으니 함께 보길 추천함