API Design on C++
TL;DR
-
API는 구현이 아니라 사용자와의 계약(contract) 이다
-
좋은 API는 존재감이 없고, 나쁜 API는 지속적인 고통을 준다
-
Top-down(사용자 중심) 설계가 거의 항상 정답이다
-
API는 성공할수록 변경 비용이 기하급수적으로 증가한다
-
구현 디테일이 API로 새는 순간, 미래의 선택지는 사라진다
개요 (Overview)
이 강연은 “API를 어떻게 설계해야 하는가?”라는 질문을
문법이나 스타일이 아닌 현실적인 유지보수 관점에서 다룬다.
핵심 메시지는 단 하나다.
API 설계의 출발점은
‘멋진 구현’이 아니라 ‘사용자가 실제로 어떻게 사용할 것인가’다.
강연은 다음 질문들에 답한다.
-
좋은 API와 나쁜 API의 차이는 무엇인가?
-
왜 사용자 관점(top-down)이 중요한가?
-
API가 커질수록 왜 변경하기 어려워지는가?
-
API/ABI 호환성은 설계에 어떤 제약을 주는가?
-
실무에서 API를 평가할 때 무엇을 봐야 하는가?
상세 설명 (Detailed Breakdown)
1. 좋은 API와 나쁜 API
좋은 API의 특징
-
사용자가 API의 존재를 의식하지 않는다
-
도메인 지식만으로 직관적인 사용이 가능하다
-
수년에서 수십 년까지 사용 가능하다
-
변경이 적고, 변경되더라도 사용자에게 주는 충격이 작다
-
테스트, 성능, 확장성에 긍정적으로 기여한다
좋은 API는 “좋다”는 느낌조차 들지 않는다
나쁜 API의 특징
-
플랫폼 또는 구현 디테일이 그대로 노출된다
-
사용할 때마다 문서를 확인해야 한다
-
오용(misuse)하기 쉽다
-
테스트가 어렵고 버그가 자주 발생한다
-
deprecate → 새 API → 또 deprecate의 반복
나쁜 API는 한 번 쓰고 끝나는 것이 아니라 지속적인 고통을 준다
2. API 설계 철학: Top-down vs Bottom-up
Top-down 설계 (권장)
-
사용자 사용 시나리오를 먼저 정의한다
-
예제 코드와 문서를 먼저 작성한다
-
API를 정의한다
-
마지막에 구현한다
→ API가 구현의 언어가 아니라 사용자의 언어로 말하게 된다
Bottom-up 설계 (위험)
-
구현(알고리즘, 외부 라이브러리)부터 시작한다
-
API가 구현에 끌려다닌다
문제점:
-
구현 디테일이 API로 그대로 새어나온다
-
사용자가 내부 구현을 알아야만 사용 가능하다
-
테스트, 확장, 교체가 모두 어려워진다
3. API 사용자 규모와 변경 비용
| 사용자 규모 | API 변경 난이도 |
|---|---|
| 개인 / 소규모 | 낮음 |
| 사내 다수 팀 | 높음 (대량 수정, 리빌드 필요) |
| 공개 API | 거의 불가능 |
API는 성공할수록 고치기 어려워진다
API / ABI 호환성
용어 정리 (Terminology)
- Library API + Compiler ABI = Library ABI
ABI 호환성
-
ABI 호환성 깨짐은
-
최악의 경우: 미묘한 오동작
-
차선의 경우: 즉시 크래시
-
-
동적 링크 라이브러리 사용 시 특히 중요 (예:
libstdc++) -
모든 사용자가 항상 전체를 다시 컴파일한다면 무시할 수도 있음
API 호환성
-
API 비호환은 보통 컴파일 자체가 되지 않음
-
라이브러리를 배포할 때 (바이너리/소스 모두)
-
전체를 다시 컴파일하는 경우에도 중요
ABI 호환 가능한 변경
-
새로운 free function 추가
-
클래스에 friend 선언 추가/제거
-
enum 끝에 값 추가 (단, enum 타입 크기가 바뀌지 않는 경우)
-
static 멤버 변수/함수 추가
-
non-virtual 메서드 추가 (오버로드 제외)
-
생성자 추가
-
기본 인자 값 변경
-
private non-virtual 메서드 제거
-
typedef 및 새 타입 추가
-
inline 함수 → non-inline 함수로 변경
- 기존에 컴파일된 코드는 이전 구현을 계속 사용
ABI 호환 불가능한 변경
-
메서드 변경
(const/constexpr, 반환 타입, 인자 위치, cv-qualifier, 템플릿 파라미터, final/override, noexcept, inline/템플릿 구현 내용 등) -
클래스 상속 구조 변경
-
클래스 멤버 추가/제거/순서 변경/타입 변경
-
오버로드 추가 또는 제거
-
멤버 함수 인라이닝 방식 변경
-
접근 제어자 변경
-
virtual 함수 추가/제거/순서 변경
(non-virtual ↔ virtual 변경 포함) -
던지는 예외 및 예외 계층 변경
-
전역 데이터의 타입 또는 cv-qualifier 변경
API 호환 가능한 변경
-
기본값을 가진 파라미터 추가
-
클래스에 메서드 추가
-
클래스에 멤버 추가
-
클래스 추가
-
멤버 또는 메서드 순서 변경
API 호환 불가능한 변경
-
인자, public/protected 메서드, 멤버, 클래스 제거
-
인자 또는 멤버 변수 타입 변경
-
public/protected 멤버나 메서드 이름 변경
-
클래스를 다른 헤더로 이동
API 설계 원칙 (Guiding Principles)
1. 사용하기 쉬워야 한다
-
온보딩 시간이 짧아진다
-
버그와 오용이 줄어든다
-
사용률이 증가한다
-
최소 놀람의 원칙(Principle of Least Surprise)
방법:
-
클래스, 메서드, 파라미터, 타입 이름을 잘 짓는다
-
의미가 명확하고
-
간결하며
-
도메인 의미를 반영하고
-
일반적으로 통용되는 용어를 사용
-
-
컴포넌트 내부/외부에서 일관성 유지
-
적절한 추상화 수준 유지
-
예: 컨테이너 자체 대신 iterator를 전달
2. 오용하기 어렵게 설계하라
-
계약이 명확하고 범위가 좁아야 한다
-
일반적으로 받아들여진 코딩 관례를 따른다
-
사용자가 예상하는 대로 정확히 동작해야 한다
3. 문서화가 되어 있어야 한다
문서가 없으면, 존재하지 않는 것이다
-
공개된 모든 요소를 문서화한다
-
계약 정의는 명확하고, 간결하며, 모호하지 않아야 한다
-
튜토리얼 제공 (문서도 코드다)
-
문서 스타일 가이드를 유지한다
4. 최소한으로 완결되어야 한다 (Minimally Complete)
-
필요한 것만 포함한다
-
API 조합으로 충분히 만들 수 있는 기능은 제외한다
-
추상화와 표현 불변성을 유지한다
-
추상화: 구현이 달라도 동작은 구분되지 않아야 함
-
표현 불변성: 객체가 항상 만족해야 하는 조건
-
예시:
-
std::mutex+std::lock_guard-
mutex는 primitive 제공
-
lock_guard는 예외 안전한 RAII 유틸리티 제공
-
5. 느슨하게 결합하라 (Loosely Coupled)
-
컴포넌트 간 상호 의존을 최소화한다
-
컴포넌트는 응집력 있게 구성한다
권장 사항:
-
순환 의존성 피하기
-
필요한 것만 include/import
-
callback, observer 패턴 활용
-
Mediator 패턴 활용 (자원 접근 단일 창구)
6. 구현 디테일을 노출하지 마라
Hyrum’s Law:
“인터페이스의 모든 관측 가능한 속성은
언젠가 누군가에 의해 의존된다”
효과:
-
사용자는 도메인 관점으로 설계 가능
-
API 제공자는 변경 자유도가 커진다
-
업그레이드와 테스트가 쉬워진다
가이드라인:
-
도메인 타입을 공개 인터페이스에 사용
-
내부 타입 노출 금지
-
의존 라이브러리 타입/함수 노출 금지
7. 장기 사용을 고려하라
-
요구사항은 변한다
-
플랫폼 제약도 변한다
-
처음부터 모든 기능을 넣을 필요는 없다
-
기능은 점진적으로 추가 가능해야 한다
예시:
- in/out 파라미터를 struct/class로 정의
→ 이후 필드 확장 가능
최소 놀람의 원칙:
-
사용자의 예측이 맞아야 한다
-
도메인 기반 추측이 틀리면 실패한 API다
8. 보일러플레이트 코드를 피하라
-
특정 작업을 수행하는 데 필요한 코드 양이
API 적합성을 판단하는 좋은 지표다 -
적절한 추상화 수준 유지
- 웹 서버 API는 socket이 아니라 HTTP 요청/헤더를 다뤄야 한다
-
반복되는 공통 작업을 위한 유틸리티 제공
-
테스트 작성이 쉬워진다
9. 플랫폼 독립적으로 설계하라
-
특정 OS에서만 동작하는 설계 금지
-
특정 OS에서만 동작하지 않는 설계도 금지
-
Windows vs POSIX
-
엔디안 문제
-
컴파일러 종속 이슈
-
-
오버로드되는 함수와 동일한 타입 사용
10. 테스트 드라이버를 고려해 설계하라
API 설계의 황금률:
네 API를 사용하는 테스트를 직접 작성해봐라
-
애플리케이션 테스트/통합 테스트에서 사용 가능해야 한다
-
mock 불가능한 concrete class API는 피한다
-
인터페이스 기반 설계 권장
-
복잡한 API는 mock 범위를 최소화
- 인터페이스 분리 원칙(ISP)
-
의존성을 끊고 단독 실행 가능해야 한다
11. 테스트 가능하게 설계하라
-
내부 기능을 테스트할 수 있도록 API를 설계한다
-
시스템/라이브러리 의존성을 대체할 수 있어야 한다
권장 기법:
- PIMPL (Pointer to Implementation) 패턴
요약 (Summary)
-
좋은 API를 작성하는 것은 매우 중요하다
-
실제 사용 방식을 반드시 고려해야 한다
-
API 설계 원칙을 적용하면 품질이 크게 향상된다
핵심 정리 (Takeaways)
-
API는 코드가 아니라 약속이다
-
사용자 관점 없이 좋은 API는 나올 수 없다
-
구현 디테일은 절대 API로 새지 않게 하라
-
API/ABI 호환성은 설계 초기에 반드시 고려해야 한다
-
“나중에 고치자”는 API 설계에서 거의 항상 틀린 선택이다
Reference
C++Now 2018: Titus Winters “Modern C++ API Design: From Rvalue-References to Type Design”
API Design Principles - John Pavan - CppNorth 2023
Testability and C++ API Design - John Pavan, Lukas Zhao & Aram Chung - C++Now 2024