C++ API 문서 자동화: Doxygen 주석, Sphinx·Breathe·Exhale 연동, GitHub Pages 배포

들어가며: API 문서가 코드와 어긋나는 이유

README나 위키에 손으로 쓴 API 설명은 헤더가 바뀔 때 함께 고쳐지지 않습니다. 함수 시그니처가 바뀌어도 문서는 예전 그대로 남고, 결국 팀원들은 문서 대신 코드를 직접 읽게 됩니다. 문서를 소스 주석에 두고 도구로 생성하면 선언과 설명이 같은 파일, 같은 커밋 안에서 바뀌므로 이 문제가 크게 줄어듭니다.

C++에서 이 역할을 맡는 표준적인 도구가 Doxygen입니다. 다만 Doxygen은 API 레퍼런스에는 강하지만 튜토리얼이나 개념 설명 같은 긴 글을 쓰기에는 불편하므로, 사용자 가이드가 필요한 프로젝트는 Sphinx로 문서 사이트를 만들고 Breathe·Exhale로 Doxygen 결과를 끌어오는 구성을 많이 씁니다. 마지막으로 이 빌드를 CI에 올려 main에 머지될 때마다 GitHub Pages에 배포하면 문서가 항상 최신 코드를 따라갑니다.

문제해결
수동 문서 동기화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 1.9 이상, Python 3.8 이상(Sphinx 사용 시), CMake 3.15 이상(선택)을 기준으로 합니다.


Doxygen과 Sphinx는 무엇이 다른가

도구 비교

도구강점약점적합한 용도
DoxygenC++ 네이티브, 주석 파싱, 자동 API 문서사용자 가이드 작성 부족, 디자인 제한API 레퍼런스, C/C++ 전용
SphinxreStructuredText, 사용자 가이드, 테마 풍부C++ 파싱은 Doxygen 의존튜토리얼, 개념 설명, Python
BreatheDoxygen XML → Sphinx 디렉티브수동 디렉티브 작성 필요Sphinx에 API 문서 삽입
ExhaleBreathe 기반, 자동 계층 생성대규모 프로젝트 시 느림중소형 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
튜토리얼 + APISphinx + 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 graphviz
# Ubuntu/Debian
sudo apt-get install doxygen graphviz
# Windows: doxygen.nl의 설치 파일을 쓰거나 Chocolatey로 설치
choco install doxygen.install graphviz
# 버전 확인
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 계열 후반 버전부터 지원)로 기본값과의 차이만 출력해 확인하는 방법도 있습니다.

최소 동작 예제

# 프로젝트 구조
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(기본값)에서는 문서 주석이 없는 심볼이 아예 목록에서 빠집니다. 두 번째 경우는 주석 형식만 바꾸면 해결됩니다.

// ❌ 일반 주석이라 Doxygen이 문서 주석으로 인식하지 않음
// Config 클래스
class Config { };
// ✅ 올바른 예
/**
 * @brief Config 클래스
 */
class Config { };

Sphinx에서 “breathe: could not find project” 에러

Doxygen XML이 아직 생성되지 않았거나 breathe_projects의 경로가 실제 XML 위치와 다를 때 납니다. Doxygen을 먼저 실행하고, 경로는 conf.py 위치를 기준으로 한 절대 경로로 계산해 두면 Sphinx를 어느 디렉터리에서 실행하든 같은 곳을 가리킵니다.

# 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"))
}

매크로·전처리기로 인한 파싱 실패

ENABLE_PREPROCESSING = YES이면 Doxygen도 #ifdef를 평가하므로, 문서 생성 시점에 정의되지 않은 매크로로 감싼 코드는 문서에서 빠집니다. 문서에 포함할 매크로를 PREDEFINED에 넣어 줍니다.

// ❌ Doxygen이 파싱 안 함
#ifdef WIN32
void windows_only();
#endif
// ✅ PREDEFINED에 추가
// Doxyfile: PREDEFINED = WIN32
# Doxyfile
PREDEFINED = WIN32 __cplusplus=201703L

링크가 깨짐 (내부 링크, 외부 링크)

다른 프로젝트의 문서로 링크하려면 그 프로젝트가 GENERATE_TAGFILE로 태그 파일을 만들어 두어야 하고, 이쪽에서는 TAGFILES에 태그 파일 경로와 해당 문서의 URL을 짝지어 적어야 합니다. 둘 중 하나가 빠지거나 경로가 틀리면 링크가 일반 텍스트로 남습니다.

# 다른 프로젝트 문서 링크
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을 하위 프로세스로 실행할 때 작업 디렉터리나 경로가 예상과 달라 실패하는 경우가 많습니다. Exhale에 Doxygen 실행을 맡기지 말고 빌드 스크립트에서 직접 순서를 보장하면 원인을 분리하기 쉽습니다.

# exhaleExecutesDoxygen = False로 두고, Doxygen은 빌드 스크립트에서 먼저 실행
exhale_args = {
    "exhaleExecutesDoxygen": False,
    # ...
}
# 빌드 스크립트에서 순서 보장
doxygen docs/Doxyfile
sphinx-build -b html docs docs/_build/html

CMake에서 “Doxygen not found”

Doxygen이 PATH에 없거나, find_package(Doxygen REQUIRED dot)처럼 dot 컴포넌트를 필수로 요구했는데 Graphviz가 설치되지 않은 경우입니다. 문서 빌드가 선택 사항이라면 REQUIRED를 빼고 찾았을 때만 타깃을 만듭니다.

# 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인데, 리팩터링 때 매개변수 이름만 바꾸고 주석은 그대로 둔 곳이 이 경고로 드러납니다.

주석 규칙

명령용도
@brief모든 public 클래스·함수·열거형의 한 줄 요약
@param매개변수마다 의미와 허용 범위
@return반환값의 의미
@pre / @post전제조건·사후조건
@throws예외 발생 조건
@deprecated폐기 예정 API 표시

디렉터리 구조

project/
├── include/           # 공개 API
├── src/               # 구현
├── docs/
│   ├── Doxyfile
│   ├── doxygen/
│   └── sphinx/       # Sphinx 사용 시
├── .github/
│   └── workflows/
│       └── docs.yml
└── CMakeLists.txt

버전 관리

Doxyfile의 $(NAME)은 셸 명령이 아니라 환경 변수를 펼치는 문법입니다. 그래서 PROJECT_NUMBER = $(git describe --tags)처럼 쓰면 명령이 실행되지 않으므로, 빌드 스크립트에서 환경 변수를 만든 뒤 Doxyfile에서 그 변수를 참조합니다.

# Doxyfile
PROJECT_NUMBER = $(PROJECT_VERSION)
# 빌드 시 버전 주입
export PROJECT_VERSION=$(git describe --tags --always)
doxygen Doxyfile

문서화 제외

@cond/@endcond도 Doxygen 명령이므로 ///나 /** */ 같은 문서 주석 안에 있어야 인식됩니다. 일반 // 주석에 쓰면 아무 효과가 없습니다.

// 내부 구현·디테일은 문서화 제외
namespace detail {
/// @cond
void internal_impl();  // @cond ~ @endcond 사이는 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: 아래에 build-docs와 나란히 추가
  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
  id: doxygen-cache
  uses: actions/cache@v4
  with:
    path: docs/doxygen/xml
    key: doxygen-${{ hashFiles('include/**', 'src/**', 'docs/Doxyfile') }}
- name: Build Doxygen XML
  if: steps.doxygen-cache.outputs.cache-hit != 'true'
  run: doxygen docs/Doxyfile

캐시를 복원해도 다음 단계에서 Doxygen을 무조건 다시 실행하면 시간이 줄지 않으므로, cache-hit 결과로 실행 여부를 나눠야 합니다. 키에 Doxyfile도 넣어 두어야 설정만 바꿨을 때 낡은 XML이 재사용되지 않습니다.

문서 버전별 배포 (태그/브랜치)

# 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"
  apt_packages:
    - doxygen
  jobs:
    post_install:
      - pip install sphinx breathe exhale
    pre_build:
      - 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

같이 보면 좋은 글