C++ API 문서 자동화: Doxygen 주석, Sphinx·Breathe·Exhale 연동, GitHub Pages 배포
들어가며: “API 문서가 코드와 항상 어긋나요”
실제 겪는 문제 시나리오
시나리오 1: 수동 문서 유지보수 지옥
"헤더 파일을 수정했는데 README의 API 설명을 깜빡했습니다."
"함수 시그니처가 바뀌었는데 문서는 예전 버전 그대로예요."
"새 팀원이 '이 함수가 뭐 하는 거예요?' 물어볼 때마다 코드를 직접 보여줘야 해요."
원인: 수동으로 작성한 문서는 코드 변경 시 동기화되지 않습니다. 주석·README·별도 문서가 따로 놀아 유지보수 비용이 급증합니다. 시나리오 2: 오픈소스 기여 시 문서 요구
"GitHub PR에 'API 문서를 추가해 주세요' 리뷰가 달렸습니다."
"Doxygen 형식으로 주석을 달아야 한다고 하네요."
"문서 빌드가 CI에서 실패해서 머지가 안 돼요."
원인: 많은 오픈소스 프로젝트가 Doxygen·Sphinx 기반 문서화를 요구합니다. 형식 미준수·빌드 실패로 PR이 지연됩니다. 시나리오 3: 사용자 가이드와 API 레퍼런스 분리
"튜토리얼·개념 설명은 마크다운으로 쓰며, API 레퍼런스는 Doxygen으로 만들고 싶습니다."
"두 개를 한 사이트에 합쳐서 배포하고 싶습니다."
"Doxygen HTML만으로는 디자인이 밋밋해요."
원인: Doxygen은 API 문서에 강하지만 사용자 가이드·튜토리얼 작성에는 Sphinx가 유리합니다. 둘을 통합해야 합니다. 시나리오 4: CI/CD에서 문서 자동 배포
"main 브랜치에 머지될 때마다 문서를 자동으로 빌드·배포하고 싶습니다."
"GitHub Pages에 올리려면 어떻게 해야 해요?"
"문서 빌드가 10분 넘게 걸려서 CI가 느려요."
원인: 문서 빌드·배포 파이프라인이 없거나 비효율적으로 구성되어 있습니다.
문서화 도구로 해결
| 문제 | 해결 |
|---|---|
| 수동 문서 동기화 | Doxygen이 소스 주석에서 API 문서 자동 생성 |
| 주석 형식 통일 | Doxygen/Javadoc 스타일 주석 규칙 적용 |
| 가이드+API 통합 | Sphinx + Breathe/Exhale로 Doxygen XML 연동 |
| 자동 배포 | GitHub Actions로 빌드 후 GitHub Pages 푸시 |
flowchart LR
subgraph Before["수동 문서 (Before)"]
B1[코드 수정] --> B2[문서 수동 업데이트]
B2 --> B3[동기화 누락]
B3 --> B4[오래된 문서]
end
subgraph After["자동 문서화 (After)"]
A1[소스 + Doxygen 주석] --> A2[Doxygen/Sphinx 빌드]
A2 --> A3[HTML/PDF 생성]
A3 --> A4[CI로 자동 배포]
end
이 글에서 다루는 것
| 항목 | 내용 |
|---|---|
| Doxygen | Doxyfile 설정, C++ 주석 스타일, HTML/XML 출력 |
| Sphinx 통합 | Breathe, Exhale로 Doxygen XML → Sphinx 문서 |
| 완전한 예제 | 헤더 전용·라이브러리·앱 프로젝트 문서화 |
| 자주 발생하는 에러 | 경로 오류, 주석 파싱 실패, 링크 깨짐 |
| 베스트 프랙티스 | 주석 규칙, 디렉터리 구조, 버전 관리 |
| 프로덕션 패턴 | GitHub Actions, GitHub Pages, 캐싱 |
요구 환경: Doxygen 1.9+, Python 3.8+ (Sphinx 시), CMake 3.15+ (선택), C++17 이상
Doxygen과 Sphinx는 무엇이 다른가
도구 비교
| 도구 | 강점 | 약점 | 적합한 용도 |
|---|---|---|---|
| Doxygen | C++ 네이티브, 주석 파싱, 자동 API 문서 | 사용자 가이드 작성 부족, 디자인 제한 | API 레퍼런스, C/C++ 전용 |
| Sphinx | reStructuredText, 사용자 가이드, 테마 풍부 | C++ 파싱은 Doxygen 의존 | 튜토리얼, 개념 설명, Python |
| Breathe | Doxygen XML → Sphinx 디렉티브 | 수동 디렉티브 작성 필요 | Sphinx에 API 문서 삽입 |
| Exhale | Breathe 기반, 자동 계층 생성 | 대규모 프로젝트 시 느림 | 중소형 C++ 라이브러리 |
도구 선택 가이드
flowchart TD
A[문서화 필요] --> B{사용자 가이드 필요?}
B -->|아니오| C[Doxygen 단독]
B -->|예| D{Sphinx 경험 있음?}
D -->|예| E[Sphinx + Breathe/Exhale]
D -->|아니오| F[Doxygen + 별도 마크다운]
C --> G[빠른 설정, HTML 출력]
E --> H[통합 사이트, 테마 적용]
F --> I[README + Doxygen 링크]
| 상황 | 권장 |
|---|---|
| API만 문서화 | Doxygen |
| 튜토리얼 + API | Sphinx + Breathe |
| 소규모 라이브러리, 자동 트리 원함 | Sphinx + Exhale |
| Python 프로젝트에 C++ 확장 문서 | Sphinx (기존 문서에 Breathe 추가) |
문서화 파이프라인
flowchart TB
subgraph source[소스]
S1[header.h] --> S2[소스 주석]
end
subgraph doxygen[Doxygen]
D1[파싱] --> D2[XML 출력]
D2 --> D3[HTML 출력]
end
subgraph sphinx["Sphinx (선택)"]
SP1["conf.py + Breathe"] --> SP2[Doxygen XML 읽기]
SP2 --> SP3[reST + API]
SP3 --> SP4[통합 HTML]
end
S1 --> D1
D2 --> SP2
SP4 --> GH[GitHub Pages]
D3 --> GH
Doxyfile 생성과 핵심 설정
설치
# macOS (Homebrew)
brew install doxygen
# Ubuntu/Debian
sudo apt-get install doxygen graphviz
# Windows (vcpkg)
vcpkg install doxygen
# 버전 확인
doxygen --version
Doxyfile 생성
# 기본 Doxyfile 생성 (모든 옵션과 설명 주석이 들어간 템플릿)
doxygen -g
# 다른 이름으로 템플릿 생성
doxygen -g Doxyfile.custom
# Doxygen 버전을 올린 뒤 기존 Doxyfile을 새 형식으로 갱신
doxygen -u Doxyfile
핵심 Doxyfile 설정
# Doxyfile 핵심 설정
# 프로젝트 정보
PROJECT_NAME = "My C++ Library"
PROJECT_NUMBER = "1.0.0"
PROJECT_BRIEF = "고성능 C++ 유틸리티 라이브러리"
# 입력
INPUT = ./include ./src
FILE_PATTERNS = *.h *.hpp *.cpp *.c
RECURSIVE = YES
EXCLUDE = ./build ./third_party
# 출력
OUTPUT_DIRECTORY = ./docs/doxygen
GENERATE_HTML = YES
GENERATE_LATEX = NO
# XML은 Sphinx(Breathe/Exhale) 통합 시 필수
GENERATE_XML = YES
XML_OUTPUT = xml
# C++ 관련
ENABLE_PREPROCESSING = YES
MACRO_EXPANSION = YES
EXPAND_AS_DEFINED =
PREDEFINED = __cplusplus=201703L DOXYGEN_SKIP
# 그래프 (선택, graphviz 필요)
HAVE_DOT = YES
doxygen -g가 만드는 Doxyfile은 수백 개 옵션이 기본값과 긴 설명 주석으로 채워져 있어서, 처음에는 필요한 몇 줄만 바꾸면 됩니다. 위 설정에서 실무적으로 중요한 것은 세 가지입니다. INPUT/RECURSIVE/EXCLUDE는 무엇을 파싱할지 정하는데, 공개 API 문서가 목적이라면 src를 빼고 include만 넣는 편이 내부 구현이 문서에 섞이지 않아 깔끔합니다. GENERATE_XML은 HTML만 볼 때는 필요 없지만 Sphinx 연동에는 필수입니다. ENABLE_PREPROCESSING과 PREDEFINED는 Doxygen이 코드를 컴파일러처럼 전처리하게 만드는 설정인데, Doxygen은 실제 컴파일러가 아니라 자체 파서를 쓰기 때문에 복잡한 매크로(예: MYLIB_API 같은 내보내기 매크로가 클래스 이름 앞에 붙는 경우)를 만나면 클래스 이름을 잘못 인식합니다. 이때 PREDEFINED = MYLIB_API=처럼 빈 값으로 정의해 주면 해결됩니다.
설정 파일 전체를 저장소에 넣으면 기본값과 바꾼 값을 구분하기 어려우므로, 바꾼 줄만 모아 둔 짧은 Doxyfile을 관리하고 doxygen -x Doxyfile(1.9.3 이상)로 기본값과의 차이만 출력해 확인하는 방법도 있습니다.
최소 동작 예제
# 프로젝트 구조
mkdir -p mylib/include mylib/src
cd mylib
doxygen -g
# Doxyfile 수정 후
doxygen
# docs/doxygen/html/index.html 생성
Doxygen이 읽는 주석 문법
C++ 주석 스타일
/**
* @file config.h
* @brief 설정 관리 클래스
* @author pkglog
* @date 2026-04-22
* @version 1.0
*/
#ifndef MYLIB_CONFIG_H
#define MYLIB_CONFIG_H
#include <string>
#include <optional>
namespace mylib {
/**
* @brief 애플리케이션 설정을 관리하는 클래스
*
* 설정 파일을 로드하며, 런타임에 값을 조회·수정합니다.
* 스레드 안전하지 않습니다.
*/
class Config {
public:
/**
* @brief 설정 파일 경로로부터 설정 로드
* @param path 설정 파일 경로 (JSON 또는 YAML)
* @return 성공 시 true, 실패 시 false
* @throws std::runtime_error 파일을 열 수 없을 때
*/
bool load(const std::string& path);
/**
* @brief 키에 해당하는 문자열 값 조회
* @param key 설정 키 (예: "server.host")
* @return 값이 있으면 optional에 담아 반환, 없으면 nullopt
*/
std::optional<std::string> get(const std::string& key) const;
/**
* @brief 설정 파일 경로 반환
*/
const std::string& path() const noexcept { return path_; }
private:
std::string path_;
};
} // namespace mylib
#endif
주석은 /** ... */(Javadoc 스타일)나 ///로 시작해야 Doxygen이 문서 주석으로 인식합니다. 일반 /* */나 // 주석은 무시되므로, 기존 코드에 주석이 많은데 문서가 비어 있다면 주석 형식부터 확인하세요. JAVADOC_AUTOBRIEF = YES를 켜면 첫 문장이 자동으로 요약(@brief)이 되어 모든 주석에 @brief를 쓰지 않아도 됩니다. 위 load() 예제의 주석은 “성공 시 true, 실패 시 false”와 “파일을 열 수 없으면 예외”라는 두 실패 경로를 동시에 문서화하고 있는데, 이런 모순은 주석을 기계적으로 채울 때 흔히 생깁니다. 문서화 도구는 형식만 검사할 뿐 내용이 코드와 맞는지는 알려 주지 않으므로, 리뷰에서 반환 규약과 예외 규약을 코드와 함께 확인해야 합니다.
함수 파라미터·반환값
/**
* @brief 두 벡터의 내적을 계산
* @param a 첫 번째 벡터
* @param b 두 번째 벡터
* @return 내적 값 (a·b)
* @pre a.size() == b.size()
*/
double dot_product(const std::vector<double>& a,
const std::vector<double>& b);
/**
* @brief 버퍼를 비동기로 전송
* @param[in] data 전송할 데이터
* @param[out] bytes_sent 실제 전송된 바이트 수 (반환 시 설정)
* @param[in,out] context 연결 컨텍스트 (진행 시 업데이트됨)
*/
void send_async(const std::vector<uint8_t>& data,
size_t* bytes_sent,
ConnectionContext* context);
템플릿·매크로
/**
* @tparam T 요소 타입 (기본 생성 가능해야 함)
* @tparam Allocator 할당자 (기본: std::allocator<T>)
*/
template <typename T, typename Allocator = std::allocator<T>>
class Pool {
public:
/**
* @brief 풀에서 객체 할당
* @return 할당된 객체 포인터
*/
T* allocate();
};
/**
* @def MYLIB_API
* @brief DLL/공유 라이브러리 내보내기 매크로
*/
#ifdef _WIN32
#define MYLIB_API __declspec(dllexport)
#else
#define MYLIB_API __attribute__((visibility("default")))
#endif
그룹·모듈
/**
* @defgroup network 네트워크
* @brief 네트워크 관련 클래스와 함수
*/
/**
* @class TcpClient
* @ingroup network
* @brief TCP 클라이언트
*/
class TcpClient { };
/**
* @class UdpSocket
* @ingroup network
*/
class UdpSocket { };
Sphinx에 Breathe·Exhale로 API 문서 붙이기
설치
pip install sphinx breathe exhale sphinx-rtd-theme
conf.py 설정
# conf.py
import os
import subprocess
project = "My C++ Library"
copyright = "2026, pkglog"
author = "pkglog"
release = "1.0.0"
extensions = [
"breathe",
"exhale",
]
# Breathe: Doxygen XML 경로
breathe_projects = {
"mylib": os.path.join(os.path.dirname(__file__), "..", "doxygen", "xml")
}
breathe_default_project = "mylib"
# Exhale: 자동 API 문서 생성
exhale_args = {
"containmentFolder": "./api",
"rootFileName": "library_root.rst",
"rootFileTitle": "API Reference",
"doxygenStripFromPath": "..",
"createTreeView": True,
"exhaleExecutesDoxygen": True,
"exhaleDoxygenStdin": "INPUT = ../include"
}
Breathe만 사용 (수동)
.. index.rst
My Library Documentation
========================
.. toctree::
:maxdepth: 2
tutorial
api
.. api.rst
API Reference
=============
.. doxygenclass:: mylib::Config
:members:
:undoc-members:
:protected-members:
:private-members:
.. doxygenfunction:: mylib::dot_product
Breathe는 “Doxygen XML을 읽어 Sphinx 문서의 원하는 위치에 끼워 넣는” 다리 역할만 합니다. 그래서 API 레퍼런스 페이지 구성을 사람이 직접 doxygenclass, doxygenfunction 디렉티브로 정해야 하고, 그만큼 튜토리얼 문장 사이에 특정 함수 설명을 넣는 식의 자유로운 구성이 가능합니다. Exhale은 그 위에 “모든 클래스·파일에 대한 rst 페이지를 자동 생성”하는 층을 얹은 것이라 손은 덜 가지만, 심볼마다 파일을 만들기 때문에 클래스가 수천 개인 프로젝트에서는 Sphinx 빌드가 눈에 띄게 느려집니다. 또 Exhale은 Sphinx·Breathe보다 릴리스 주기가 길어 최신 Sphinx 버전과 호환 문제가 생길 수 있으니, 새 프로젝트라면 도입 전에 사용할 Sphinx 버전에서 빌드가 되는지 먼저 확인하는 것이 좋습니다.
디렉터리 구조
docs/
├── conf.py
├── index.rst
├── tutorial.rst
├── doxygen/
│ ├── Doxyfile
│ └── xml/ # Doxygen XML 출력
└── _build/
└── html/
헤더 전용 라이브러리부터 CMake 연동까지 네 가지 구성
헤더 전용 라이브러리
header-only-lib/
├── include/
│ └── mylib/
│ └── algorithm.hpp
├── Doxyfile
└── docs/
// include/mylib/algorithm.hpp
/**
* @file algorithm.hpp
* @brief 유틸리티 알고리즘
*/
namespace mylib {
/**
* @brief 이진 탐색
* @param first 시작 반복자
* @param last 끝 반복자
* @param value 찾을 값
* @return 찾으면 해당 반복자, 없으면 last
*/
template <typename It, typename T>
It binary_search(It first, It last, const T& value) {
// 구현...
return last;
}
}
# Doxyfile (단순화)
INPUT = include
RECURSIVE = YES
GENERATE_LATEX = NO
CMake 연동 문서 빌드
# CMakeLists.txt
find_package(Doxygen REQUIRED dot)
set(DOXYGEN_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/docs)
set(DOXYGEN_GENERATE_HTML YES)
set(DOXYGEN_GENERATE_XML YES)
set(DOXYGEN_PROJECT_NAME "MyLib")
doxygen_add_docs(
docs
${CMAKE_SOURCE_DIR}/include
COMMENT "Generating API documentation"
)
cmake -B build
cmake --build build --target docs
Exhale로 자동 API 트리 생성
# conf.py (Exhale 전체 설정)
exhale_args = {
"containmentFolder": "./api",
"rootFileName": "library_root.rst",
"rootFileTitle": "API Reference",
"doxygenStripFromPath": "..",
"createTreeView": True,
"exhaleExecutesDoxygen": True,
"exhaleDoxygenStdin": """
INPUT = ../include
RECURSIVE = YES
GENERATE_XML = YES
XML_OUTPUT = xml
""",
"treeViewIsBootstrap": True,
}
.. index.rst
Welcome to My Library
=====================
.. toctree::
:maxdepth: 2
tutorial
api/library_root
전체 Doxyfile (실전용)
# Doxyfile 실전용
PROJECT_NAME = "pkglog-core"
PROJECT_NUMBER = "1.2.0"
PROJECT_BRIEF = "고성능 C++ 코어 라이브러리"
INPUT = include src
FILE_PATTERNS = *.h *.hpp *.cpp *.c
RECURSIVE = YES
EXCLUDE = build/ third_party/ test/
EXCLUDE_PATTERNS = */detail/* */impl/*
OUTPUT_DIRECTORY = docs
GENERATE_HTML = YES
HTML_OUTPUT = html
GENERATE_LATEX = NO
GENERATE_XML = YES
XML_OUTPUT = xml
# 소스 브라우저
SOURCE_BROWSER = YES
INLINE_SOURCES = NO
REFERENCED_BY_RELATION = YES
REFERENCES_RELATION = YES
# 경고
QUIET = NO
WARNINGS = YES
WARN_IF_UNDOCUMENTED = NO
WARN_IF_DOC_ERROR = YES
# C++17
PREDEFINED = __cplusplus=201703L
ENABLE_PREPROCESSING = YES
MACRO_EXPANSION = YES
# 그래프
HAVE_DOT = YES
심볼 누락·Breathe 경로·매크로 파싱 문제 해결
”Could not find symbol” / 문서에 클래스가 안 나옴
원인: INPUT 경로 오류, FILE_PATTERNS 누락, RECURSIVE 미설정
해결법:
# ❌ 잘못된 설정
INPUT = include
RECURSIVE = NO
# ✅ 올바른 설정
INPUT = ./include ./src
FILE_PATTERNS = *.h *.hpp *.cpp *.c
RECURSIVE = YES
“warning: documented symbol X was not declared or defined” / 문서가 비어 있음
원인: 두 가지 다른 원인이 비슷한 증상을 만듭니다. 이 경고 자체는 @fn, @class처럼 떨어진 곳에서 대상을 지정하는 주석이 실제 선언과 맞지 않을 때 납니다. 네임스페이스를 빼고 @fn Config::load라고 적었거나 매개변수 목록이 선언과 다르면 Doxygen이 대상을 찾지 못합니다. 반면 문서가 경고 없이 비어 있다면 주석이 //처럼 문서 주석 형식이 아닌 경우이고, EXTRACT_ALL = NO(기본값)에서는 문서 주석이 없는 심볼이 아예 목록에서 빠집니다.
해결법:
// ❌ 잘못된 예: 주석이 선언과 분리
// Config 클래스
class Config { };
// ✅ 올바른 예
/**
* @brief Config 클래스
*/
class Config { };
Sphinx에서 “breathe: could not find project” 에러
원인: Doxygen XML이 먼저 생성되지 않음, Breathe 경로 오류 해결법:
# 1. Doxygen 먼저 실행
cd docs && doxygen Doxyfile
# 2. conf.py 경로 확인
# breathe_projects[mylib] = "../doxygen/xml" # 실제 경로
# conf.py
breathe_projects = {
"mylib": os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "doxygen", "xml"))
}
매크로·전처리기로 인한 파싱 실패
원인: #ifdef로 감싼 코드가 Doxygen에서 제외됨
해결법:
// ❌ Doxygen이 파싱 안 함
#ifdef WIN32
void windows_only();
#endif
// ✅ PREDEFINED에 추가
// Doxyfile: PREDEFINED = WIN32
# Doxyfile
PREDEFINED = WIN32 __cplusplus=201703L
링크가 깨짐 (내부 링크, 외부 링크)
원인: GENERATE_TAGFILE 미설정, TAGFILES 경로 오류
해결법:
# 다른 프로젝트 문서 링크
GENERATE_TAGFILE = mylib.tag
TAGFILES = /path/to/other/docs/other.tag=https://other-docs.example.com/
한글 깨짐
원인: INPUT_ENCODING의 기본값은 이미 UTF-8이므로, 한글이 깨지는 경우는 대부분 소스 파일 자체가 UTF-8이 아닐 때입니다. Windows에서 오래 관리된 코드는 CP949(EUC-KR 확장)로 저장된 파일이 섞여 있는 경우가 많습니다.
해결법:
# Doxyfile — 소스가 CP949라면
INPUT_ENCODING = CP949
# 파일별로 다르면 패턴별 지정 (최근 버전의 Doxygen에서 지원)
INPUT_FILE_ENCODING = *.h=CP949
# 생성되는 고정 문구(“Public Member Functions” 등)를 한국어로
OUTPUT_LANGUAGE = Korean
근본적인 해결은 소스를 UTF-8로 변환하는 것입니다(iconv -f CP949 -t UTF-8). MSVC는 BOM 없는 UTF-8 파일을 시스템 코드 페이지로 읽어 한글 문자열 리터럴이 깨질 수 있으므로, 변환 후에는 컴파일 옵션에 /utf-8을 추가해야 컴파일러와 Doxygen이 같은 인코딩으로 파일을 읽습니다.
”exhale: Doxygen failed” / Exhale 빌드 실패
원인: Exhale이 Doxygen을 하위 프로세스로 실행할 때 경로·권한 문제 해결법:
# exhaleExecutesDoxygen = False로 두며, 수동으로 Doxygen 먼저 실행
exhale_args = {
"exhaleExecutesDoxygen": False,
# ...
}
# 빌드 스크립트에서 순서 보장
doxygen docs/Doxyfile
sphinx-build -b html docs docs/_build/html
CMake에서 “Doxygen not found”
원인: find_package(Doxygen) 실패, graphviz 미설치
해결법:
# graphviz 없이도 동작하도록
find_package(Doxygen)
if(DOXYGEN_FOUND)
set(DOXYGEN_HAVE_DOT NO) # graphviz 없으면 비활성화
doxygen_add_docs(...)
endif()
대용량 프로젝트에서 문서 빌드가 너무 느림
원인: 전체 소스 파싱, 그래프 생성 오버헤드 해결법:
# Doxyfile
HAVE_DOT = NO # 그래프 생성이 가장 큰 비용인 경우가 많음
# 그래프가 필요하면 범위를 줄이고 병렬화
CALL_GRAPH = NO
CALLER_GRAPH = NO
DOT_NUM_THREADS = 0 # 0 = CPU 코어 수만큼
NUM_PROC_THREADS = 0 # 입력 처리 병렬화 (1.9.0 이상)
# EXCLUDE로 불필요한 디렉터리 제외
EXCLUDE = test/ benchmark/ examples/
SOURCE_BROWSER = NO # 소스 코드 페이지 생성 생략
대형 프로젝트에서 빌드 시간을 먼저 잡아먹는 것은 대개 Graphviz 그래프입니다. HAVE_DOT = YES에 CALL_GRAPH까지 켜면 함수마다 그래프 이미지를 만들어 파일 수와 시간이 폭증합니다. CI에서는 그래프를 끈 빠른 빌드로 경고만 확인하고, 배포용 문서만 전체 설정으로 만드는 식으로 나누는 것도 방법입니다.
문서 경고·주석 규칙·버전 관리 운영
문서 경고를 CI에서 관리하기
문서가 코드와 어긋나는 것을 막는 가장 실용적인 방법은 Doxygen 경고를 빌드 실패로 다루는 것입니다. WARN_AS_ERROR = FAIL_ON_WARNINGS(1.9 계열 이상)를 켜면 경고가 있을 때 종료 코드가 0이 아니게 되어, 매개변수 이름을 바꾸고 @param을 고치지 않은 PR이 CI에서 걸립니다. 기존 코드베이스에 경고가 수백 개라면 처음부터 켜기보다 WARN_LOGFILE로 경고를 파일에 모아 개수를 추적하고, 새 경고만 막는 식으로 점진적으로 적용하는 것이 현실적입니다. 제가 이 방식을 적용할 때 가장 많이 걸린 경고는 @param 이름이 실제 매개변수와 다르다는 argument 'x' of command @param is not found in the argument list였는데, 리팩터링 때 이름만 바꾸고 주석을 두고 간 흔적이 그대로 드러났습니다.
주석 규칙
| 규칙 | 설명 |
|---|---|
| 모든 public API 문서화 | 클래스, 함수, 열거형에 @brief 필수 |
| @param | 파라미터마다 설명 |
| @return | 반환값 설명 |
@pre / @post | 전제조건·사후조건 |
| @throws | 예외 발생 조건 |
| @deprecated | 폐기 예정 API 표시 |
디렉터리 구조
project/
├── include/ # 공개 API
├── src/ # 구현
├── docs/
│ ├── Doxyfile
│ ├── doxygen/
│ └── sphinx/ # Sphinx 사용 시
├── .github/
│ └── workflows/
│ └── docs.yml
└── CMakeLists.txt
버전 관리
# Doxyfile
PROJECT_NUMBER = $(git describe --tags --always)
# 빌드 시 버전 주입
export PROJECT_VERSION=$(git describe --tags)
doxygen Doxyfile
문서화 제외
// 내부 구현·디테일은 문서화 제외
namespace detail {
// @cond
void internal_impl(); // Doxygen이 무시
// @endcond
}
# Doxyfile
EXCLUDE = include/detail
GitHub Actions로 문서 빌드와 GitHub Pages 배포
GitHub Actions: Doxygen 문서 배포
# .github/workflows/docs.yml
name: Build and Deploy Documentation
on:
push:
branches: [main]
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
jobs:
build-docs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Doxygen
run: sudo apt-get update && sudo apt-get install -y doxygen graphviz
- name: Build Doxygen
run: doxygen Doxyfile
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: docs/html
GitHub Pages 설정
# 동일 워크플로에 추가
jobs:
deploy:
needs: build-docs
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deploy.outputs.page_url }}
steps:
- name: Deploy to GitHub Pages
id: deploy
uses: actions/deploy-pages@v4
Sphinx + Doxygen 통합 빌드
# .github/workflows/docs-sphinx.yml
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install dependencies
run: pip install sphinx breathe exhale sphinx-rtd-theme
- name: Build Doxygen XML
run: doxygen docs/Doxyfile
- name: Build Sphinx
run: cd docs && sphinx-build -b html . _build/html
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: docs/_build/html
캐싱으로 빌드 시간 단축
- name: Cache Doxygen
uses: actions/cache@v4
with:
path: docs/doxygen/xml
key: doxygen-${{ hashFiles('include/**', 'src/**') }}
문서 버전별 배포 (태그/브랜치)
# main → latest, v* 태그 → 해당 버전
- name: Set docs path
run: |
if [[ $GITHUB_REF == refs/tags/* ]]; then
echo "DOCS_PATH=docs/${{ github.ref_name }}" >> $GITHUB_ENV
else
echo "DOCS_PATH=docs/latest" >> $GITHUB_ENV
fi
Read the Docs 연동 (대안)
# .readthedocs.yml
version: 2
build:
os: ubuntu-22.04
tools:
python: "3.11"
jobs:
post_checkout:
- pip install sphinx breathe exhale
post_install:
- doxygen docs/Doxyfile
sphinx:
configuration: docs/conf.py
fail_on_warning: false
로컬 문서 미리보기
# Doxygen HTML 로컬 서버
cd docs/html && python -m http.server 8000
# Sphinx HTML
cd docs/_build/html && python -m http.server 8000
문서화 도입 점검 목록
Doxygen 단독 사용
-
doxygen -g로 Doxyfile 생성 -
INPUT,RECURSIVE,FILE_PATTERNS설정 -
GENERATE_HTML,GENERATE_XML설정 -
PREDEFINED에__cplusplus등 추가 - public API에
@brief,@param,@return주석 -
EXCLUDE로 빌드·테스트 디렉터리 제외
Sphinx 통합
-
pip install sphinx breathe exhale -
conf.py에breathe_projects,exhale_args설정 - Doxygen XML 먼저 생성
-
sphinx-build실행
CI/CD
- GitHub Actions 워크플로 작성
-
actions/upload-pages-artifact사용 -
actions/deploy-pages로 GitHub Pages 배포 - (선택) Doxygen XML 캐시
품질
-
WARN_IF_DOC_ERROR = YES -
WARN_IF_UNDOCUMENTED = NO(점진적 적용) - 버전 번호 자동 주입
Doxygen·Sphinx 역할 요약
| 항목 | 설명 |
|---|---|
| Doxygen | C++ API 문서 자동 생성, Doxyfile로 설정 |
| Sphinx | 사용자 가이드·튜토리얼, Breathe/Exhale로 API 통합 |
| 주석 | @brief, @param, @return 등 Javadoc 스타일 |
| CI/CD | GitHub Actions로 빌드·GitHub Pages 배포 |
핵심 원칙:
- 코드와 문서를 한 곳(소스 주석)에서 관리
- 모든 public API에 최소한
@brief문서화 - Doxygen 단독 vs Sphinx 통합은 프로젝트 규모·요구에 따라 선택
- CI에서 문서 빌드·배포 자동화
자주 묻는 질문 (FAQ)
Q. #ifdef로 감싼 함수가 Doxygen 문서에서 빠지는 이유는 무엇인가요?
A. Doxygen은 전처리기를 실행한 결과를 기준으로 파싱하므로, 문서 생성 시점에 정의되지 않은 매크로로 감싼 코드는 존재하지 않는 것으로 처리됩니다. Doxyfile에서 ENABLE_PREPROCESSING과 MACRO_EXPANSION을 켜고 PREDEFINED에 WIN32 같은 매크로를 추가하면 해당 블록도 문서에 포함됩니다. 플랫폼별 API를 모두 문서화하려면 문서 빌드용 매크로를 하나 정해 PREDEFINED에 넣어 두는 방법도 쓸 수 있습니다.
Q. Sphinx 빌드에서 breathe: could not find project 에러가 납니다.
A. Breathe는 Doxygen이 만든 XML을 읽기 때문에, Doxygen을 먼저 실행하지 않았거나 conf.py의 breathe_projects 경로가 실제 XML 디렉터리와 다르면 이 에러가 납니다. doxygen Doxyfile을 먼저 실행하고, 경로는 os.path.abspath로 conf.py 위치를 기준으로 계산해 두면 빌드를 어디서 실행하든 같은 경로를 가리킵니다.