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 설계 (권장)

  1. 사용자 사용 시나리오를 먼저 정의한다

  2. 예제 코드와 문서를 먼저 작성한다

  3. API를 정의한다

  4. 마지막에 구현한다

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