C++ 코드 생성 완벽 가이드 | 템플릿·매크로·Clang·외부 도구 [#55-6]
이 글의 핵심
반복 코드 폭발을 막는 C++ 코드 생성: 템플릿 인스턴스화, X-Macros, Clang libTooling, protobuf·flatbuffers, Python 스크립트. 문제 시나리오부터 프로덕션 패턴까지 실전 코드로 정리합니다.
들어가며: 구조체가 50개인데 to_json·from_json을 50번 짜고 있어요
”패킷 타입마다 직렬화·역직렬화를 손으로 작성하다 보니 실수도 많고 유지보수가 불가능해요”
C++에는 Java·C#처럼 리플렉션이 표준으로 없어서, 직렬화·RPC 스텁·에러 코드 매핑처럼 “타입·열거형·메시지 정의를 기반으로 반복 코드”가 필요할 때마다 수동 작성에 의존하게 됩니다. 구조체·열거형이 10개, 50개로 늘어나면 코드 폭발과 누락 위험이 커집니다.
해결책: 코드 생성(Code Generation)으로 소스 코드나 바이너리를 자동 생성합니다. 템플릿·매크로는 컴파일 타임에, Clang·Python 스크립트·protobuf는 빌드 전에 코드를 만들어 냅니다.
flowchart TB
subgraph problem[문제 상황]
P1[PacketA 구조체] --> P2[to_json 수동 작성]
P3[PacketB 구조체] --> P4[to_json 수동 작성]
P5[PacketC... 50개] --> P6[반복 코드 폭발]
P2 --> P7[멤버 추가 시 누락]
P4 --> P7
P6 --> P7
end
subgraph solution[코드 생성 해결]
S1[정의 1회] --> S2[템플릿/매크로/도구]
S2 --> S3[자동 코드 생성]
S3 --> S4[유지보수 용이]
end
이 글에서 다루는 것:
- 문제 시나리오: 코드 생성이 필요한 실제 상황
- 템플릿 기반: 컴파일 타임 코드 생성
- 매크로·X-Macros: 전처리기 기반 반복 제거
- Clang libTooling: AST 분석·소스 코드 생성
- 외부 도구: protobuf, flatbuffers, Python 스크립트
- 완전한 코드 생성 예제
- 자주 발생하는 에러와 해결법
- 모범 사례와 프로덕션 패턴
요구 환경: C++17 이상 (일부 예제는 C++20)
실무 적용 경험: 이 글은 대규모 C++ 프로젝트에서 실제로 겪은 문제와 해결 과정을 바탕으로 작성되었습니다. 책이나 문서에서 다루지 않는 실전 함정과 디버깅 팁을 포함합니다.
목차
- 문제 시나리오: 코드 생성이 필요한 순간
- 코드 생성 방식 선택 가이드
- 템플릿 기반 코드 생성
- 매크로·X-Macros 기반
- Clang libTooling 소스 코드 생성
- 외부 도구: protobuf·flatbuffers
- 완전한 코드 생성 예제
- 자주 발생하는 에러와 해결법
- 모범 사례
- 프로덕션 패턴
- 정리
1. 문제 시나리오: 코드 생성이 필요한 순간
시나리오 1: 네트워크 패킷 직렬화 50종
문제: 서버-클라이언트 간 LoginRequest, ChatMessage, InventoryUpdate 등 패킷 타입이 50개입니다. 각각 to_json·from_json 또는 바이너리 직렬화를 손으로 작성하면, 멤버 추가·삭제 시 누락이 잦고 유지보수가 어렵습니다.
해결: protobuf/flatbuffers로 .proto·.fbs 정의만 두고, 빌드 시 C++ 직렬화 코드를 자동 생성합니다. 또는 X-Macros로 패킷 목록을 한 곳에 모아 매크로로 반복 생성합니다.
시나리오 2: 에러 코드 → 문자열 매핑
문제: enum class ErrorCode { NotFound, Timeout, ... }가 100개 이상입니다. 로깅·디버깅용으로 errorToString(ErrorCode)를 구현하려면 switch문이 수백 줄이 됩니다.
해결: X-Macros로 ERROR_LIST(EXPAND) 한 번 정의하고, EXPAND 매크로를 바꿔가며 enum·switch·문자열 배열을 동시에 생성합니다.
시나리오 3: RPC 스텁·클라이언트 자동 생성
문제: gRPC·Thrift처럼 IDL(Interface Definition Language)로 서비스 정의를 하고, C++ 서버·클라이언트 스텁을 자동 생성해야 합니다.
해결: protobuf + gRPC, Apache Thrift 등이 IDL에서 C++ 코드를 생성합니다. 커스텀 RPC가 필요하면 Python 스크립트로 IDL 파싱 후 C++ 헤더·소스를 출력합니다.
시나리오 4: 리플렉션 등록 코드 반복
문제: 리플렉션 글처럼 타입별로 TypeInfo 등록을 수동으로 하면, 구조체가 늘어날수록 등록 코드가 폭발합니다.
해결: 매크로로 REFLECT_PROPERTY(User, id) 형태로 반복을 줄이거나, Clang 기반 도구로 헤더를 파싱해 *_reflection.generated.cpp를 생성합니다.
시나리오 5: 테스트 픽스처·모킹 코드
문제: API가 100개인데, 각각에 대한 mock·stub을 손으로 작성하기엔 시간이 부족합니다.
해결: 인터페이스 정의를 파싱해 mock 클래스를 생성하는 도구(예: Google Mock의 MOCK_METHOD 매크로)를 사용하거나, Python으로 인터페이스 헤더를 읽어 mock 소스를 생성합니다.
2. 코드 생성 방식 선택 가이드
| 방식 | 시점 | 용도 | 장점 | 단점 |
|---|---|---|---|---|
| 템플릿 | 컴파일 타임 | 타입별 코드 생성 | 표준, 의존성 없음 | 컴파일 시간·바이너리 증가 |
| 매크로/X-Macros | 전처리 | enum·switch·배열 동시 생성 | 단순, 빌드 도구 불필요 | 가독성 저하, 디버깅 어려움 |
| Clang libTooling | 빌드 전 | AST 분석·소스 생성 | 강력, C++ 문법 완전 이해 | 빌드 복잡, 학습 곡선 |
| protobuf/flatbuffers | 빌드 전 | 직렬화·RPC 스텁 | 검증됨, 생태계 풍부 | 외부 의존성, 스키마 관리 |
| Python/스크립트 | 빌드 전 | 커스텀 코드 생성 | 유연, 빠른 프로토타입 | 유지보수, 파싱 정확도 |
flowchart TD
A[코드 생성 필요] --> B{정의가 이미 있나?}
B -->|proto/IDL| C[protobuf/Thrift]
B -->|C++ 헤더| D[Clang libTooling]
B -->|enum/목록만| E[X-Macros]
B -->|타입별 로직| F[템플릿]
C --> G[직렬화/RPC]
D --> H[리플렉션/스텁]
E --> I[에러 매핑/패킷]
F --> J[컴파일 타임 생성]
3. 템플릿 기반 코드 생성
핵심 개념
템플릿은 컴파일 타임에 사용된 타입마다 별도 코드를 생성합니다. std::vector<int>와 std::vector<double>는 서로 다른 클래스 인스턴스입니다.
// template_codegen.cpp
#include <iostream>
#include <string>
#include <sstream>
// 타입별로 다른 직렬화 로직 — 컴파일 시점에 생성
template <typename T>
std::string to_string_impl(const T& value);
template <>
std::string to_string_impl<int>(const int& value) {
return std::to_string(value);
}
template <>
std::string to_string_impl<double>(const double& value) {
return std::to_string(value);
}
template <>
std::string to_string_impl<std::string>(const std::string& value) {
return "\"" + value + "\"";
}
// 제네릭 구조체 직렬화 — 멤버를 알면 자동 생성
template <typename T>
struct Serializer;
struct User {
int id;
std::string name;
};
template <>
struct Serializer<User> {
static std::string to_json(const User& u) {
std::ostringstream oss;
oss << "{\"id\":" << u.id << ",\"name\":" << to_string_impl(u.name) << "}";
return oss.str();
}
};
int main() {
User u{1, "Alice"};
std::cout << Serializer<User>::to_json(u) << "\n";
return 0;
}
variadic 템플릿으로 N개 타입 처리
// variadic_visitor.cpp
#include <iostream>
#include <variant>
template <typename... Ts>
struct Visitor : Ts... {
using Ts::operator()...;
};
template <typename... Ts>
Visitor(Ts...) -> Visitor<Ts...>;
int main() {
std::variant<int, double, std::string> v = 3.14;
std::visit(Visitor{
{ std::cout << "int: " << i << "\n"; },
{ std::cout << "double: " << d << "\n"; },
{ std::cout << "string: " << s << "\n"; }
}, v);
return 0;
}
설명: Visitor는 Ts...를 상속해 각 람다의 operator()를 가져옵니다. std::visit가 variant의 타입에 맞는 오버로드를 호출합니다. 타입 목록이 바뀌어도 코드 수정 없이 동작합니다.
4. 매크로·X-Macros 기반
X-Macros 패턴
X-Macros는 한 곳에 데이터 목록을 정의하고, 매크로를 바꿔가며 여러 형태의 코드를 생성합니다.
// xmacro_errors.cpp
#include <iostream>
#include <string>
// 1. 에러 목록 정의 (단일 소스)
#define ERROR_LIST(X) \
X(NotFound, 404, "Resource not found") \
X(Unauthorized, 401, "Unauthorized") \
X(Timeout, 408, "Request timeout") \
X(InternalError, 500, "Internal server error")
// 2. enum 생성
#define EXPAND_ENUM(name, code, msg) name,
enum class ErrorCode { ERROR_LIST(EXPAND_ENUM) Count };
// 3. 코드 매핑 배열 생성
#define EXPAND_CODE(name, code, msg) static_cast<int>(ErrorCode::name),
static const int g_error_codes[] = { ERROR_LIST(EXPAND_CODE) };
// 4. 문자열 매핑 (C++11)
#define EXPAND_STR(name, code, msg) #name,
static const char* const g_error_names[] = { ERROR_LIST(EXPAND_STR) };
#define EXPAND_MSG(name, code, msg) msg,
static const char* const g_error_messages[] = { ERROR_LIST(EXPAND_MSG) };
const char* errorToString(ErrorCode e) {
size_t i = static_cast<size_t>(e);
if (i >= sizeof(g_error_messages) / sizeof(g_error_messages[0]))
return "Unknown";
return g_error_messages[i];
}
int errorToHttpCode(ErrorCode e) {
size_t i = static_cast<size_t>(e);
if (i >= sizeof(g_error_codes) / sizeof(g_error_codes[0]))
return 500;
return g_error_codes[i];
}
int main() {
std::cout << errorToString(ErrorCode::Timeout) << "\n"; // Request timeout
std::cout << errorToHttpCode(ErrorCode::NotFound) << "\n"; // 404
return 0;
}
장점: ERROR_LIST에 한 줄만 추가하면 enum·코드 배열·문자열 배열이 모두 갱신됩니다. 단점: 매크로가 복잡해지면 가독성이 떨어집니다.
패킷 타입 목록으로 직렬화 생성
// xmacro_packets.cpp
#include <iostream>
#include <sstream>
#define PACKET_LIST(X) \
X(Login, int userId, std::string token) \
X(Chat, int roomId, std::string message) \
X(Logout, int userId)
// 구조체 선언
#define EXPAND_STRUCT(name, ...) \
struct name##Packet { __VA_ARGS__; };
PACKET_LIST(EXPAND_STRUCT)
// to_json 생성 (간단 버전)
#define EXPAND_TO_JSON(name, ...) \
inline std::string to_json(const name##Packet& p) { \
std::ostringstream oss; \
oss << "{\"type\":\"" #name "\"}"; \
return oss.str(); \
}
PACKET_LIST(EXPAND_TO_JSON)
int main() {
LoginPacket p1{};
p1.userId = 1;
p1.token = "abc";
std::cout << to_json(p1) << "\n";
return 0;
}
5. Clang libTooling 소스 코드 생성
개요
Clang libTooling은 C++ 소스 코드를 AST(Abstract Syntax Tree)로 파싱하고, 분석·변환·새 소스 생성이 가능합니다. clang-tidy, clang-format, refactoring 도구들이 이 방식입니다.
flowchart LR
A[.h/.cpp] --> B[Clang 파서]
B --> C[AST]
C --> D[RecursiveASTVisitor]
D --> E[코드 생성]
E --> F[.generated.cpp]
최소 예제: 구조체 멤버 추출
// gen_reflection.cpp — Clang 플러그인 (개념)
// 실제 빌드 시 libTooling 링크 필요
#include "clang/AST/ASTConsumer.h"
#include "clang/AST/RecursiveASTVisitor.h"
#include "clang/Frontend/CompilerInstance.h"
#include "clang/Frontend/FrontendAction.h"
#include "clang/Tooling/Tooling.h"
using namespace clang;
class StructVisitor : public RecursiveASTVisitor<StructVisitor> {
public:
bool VisitRecordDecl(RecordDecl* D) {
if (D->isStruct()) {
llvm::outs() << "struct " << D->getName() << " {\n";
for (auto* F : D->fields()) {
llvm::outs() << " " << F->getType().getAsString()
<< " " << F->getName() << ";\n";
}
llvm::outs() << "};\n";
}
return true;
}
};
class StructConsumer : public ASTConsumer {
public:
void HandleTranslationUnit(ASTContext& Ctx) override {
StructVisitor V;
V.TraverseDecl(Ctx.getTranslationUnitDecl());
}
};
class StructAction : public ASTFrontendAction {
public:
std::unique_ptr<ASTConsumer> CreateASTConsumer(
CompilerInstance&, StringRef) override {
return std::make_unique<StructConsumer>();
}
};
int main(int argc, const char** argv) {
return clang::tooling::runToolOnCode(
std::make_unique<StructAction>(), "struct User { int id; std::string name; };");
}
실무: CMake에서 find_package(LLVM) 후 add_executable(gen_reflection gen_reflection.cpp), target_link_libraries(gen_reflection PRIVATE clangTooling)로 빌드합니다. 빌드 전에 gen_reflection user.h > user_reflection.generated.cpp를 실행해 생성 코드를 포함시킵니다.
6. 외부 도구: protobuf·flatbuffers
Protocol Buffers
protobuf는 .proto 스키마에서 C++·Python·Go 등 여러 언어의 직렬화·역직렬화 코드를 생성합니다.
// user.proto
syntax = "proto3";
message User {
int32 id = 1;
string name = 2;
string email = 3;
}
message LoginRequest {
string username = 1;
string password = 2;
}
# 코드 생성
protoc --cpp_out=. user.proto
# user.pb.h, user.pb.cc 생성
// protobuf_usage.cpp
#include "user.pb.h"
#include <iostream>
int main() {
User user;
user.set_id(1);
user.set_name("Alice");
user.set_email("[email protected]");
std::string serialized;
user.SerializeToString(&serialized);
User parsed;
parsed.ParseFromString(serialized);
std::cout << parsed.name() << "\n";
return 0;
}
FlatBuffers
FlatBuffers는 zero-copy 직렬화로, 게임·임베디드에서 자주 사용됩니다.
// user.fbs
table User {
id: int;
name: string;
email: string;
}
root_type User;
flatc --cpp -o generated user.fbs
// flatbuffers_usage.cpp
#include "user_generated.h"
#include <iostream>
int main() {
flatbuffers::FlatBufferBuilder builder(1024);
auto name = builder.CreateString("Alice");
auto email = builder.CreateString("[email protected]");
auto user = CreateUser(builder, 1, name, email);
builder.Finish(user);
auto* u = flatbuffers::GetRoot<User>(builder.GetBufferPointer());
std::cout << u->name()->str() << "\n";
return 0;
}
7. 완전한 코드 생성 예제
예제 1: X-Macros로 완전한 에러 시스템
// error_system.h
#pragma once
#include <string>
#include <stdexcept>
#define ERROR_LIST(X) \
X(Ok, 0, "Success") \
X(NotFound, 1, "Resource not found") \
X(InvalidArg, 2, "Invalid argument") \
X(Timeout, 3, "Operation timed out") \
X(Internal, 4, "Internal error")
#define EXPAND_ENUM(name, code, msg) name = code,
enum class ErrorCode { ERROR_LIST(EXPAND_ENUM) };
#define EXPAND_MSG(name, code, msg) msg,
static const char* const ERROR_MESSAGES[] = { ERROR_LIST(EXPAND_MSG) };
inline const char* to_string(ErrorCode e) {
int i = static_cast<int>(e);
if (i < 0 || i >= static_cast<int>(sizeof(ERROR_MESSAGES)/sizeof(ERROR_MESSAGES[0])))
return "Unknown";
return ERROR_MESSAGES[i];
}
#undef EXPAND_ENUM
#undef EXPAND_MSG
예제 2: Python으로 C++ enum·switch 생성
# gen_enum_switch.py
import sys
ENUMS = """
NotFound, 404
Unauthorized, 401
Timeout, 408
InternalError, 500
""".strip().split('\n')
def gen_cpp():
lines = [l.strip() for l in ENUMS if l.strip()]
print("enum class HttpError {")
for i, line in enumerate(lines):
name, code = line.split(',')
name = name.strip()
code = code.strip()
suffix = "," if i < len(lines)-1 else ""
print(f" {name} = {code}{suffix}")
print("};")
print()
print("const char* httpErrorToString(HttpError e) {")
print(" switch (e) {")
for line in lines:
name, _ = line.split(',')
name = name.strip()
print(f' case HttpError::{name}: return "{name}";')
print(" default: return \"Unknown\";")
print(" }")
print("}")
if __name__ == "__main__":
gen_cpp()
python3 gen_enum_switch.py > http_error.generated.hpp
예제 3: 템플릿으로 타입별 직렬화 자동화
// json_serializer.h
#pragma once
#include <sstream>
#include <string>
template <typename T>
struct JsonSerializer;
// 기본: to_string 사용
template <typename T>
std::string json_value(const T& v) {
if constexpr (std::is_same_v<T, std::string>) {
return "\"" + v + "\"";
} else if constexpr (std::is_integral_v<T> || std::is_floating_point_v<T>) {
return std::to_string(v);
} else {
return JsonSerializer<T>::to_json(v);
}
}
struct User {
int id;
std::string name;
};
template <>
struct JsonSerializer<User> {
static std::string to_json(const User& u) {
std::ostringstream oss;
oss << "{";
oss << "\"id\":" << json_value(u.id) << ",";
oss << "\"name\":" << json_value(u.name);
oss << "}";
return oss.str();
}
};
template <typename T>
std::string to_json(const T& value) {
return json_value(value);
}
예제 4: Python으로 C++ 테스트 픽스처 생성
인터페이스 헤더를 파싱해 mock 객체를 자동 생성하는 스크립트 예시입니다.
# gen_mock.py
import re
import sys
def parse_interface(content):
"""간단한 인터페이스 파싱 — 실제로는 Clang 파서 권장"""
methods = []
for line in content.split('\n'):
m = re.match(r'\s*virtual\s+(\w+)\s+(\w+)\s*\((.*)\)\s*=\s*0', line)
if m:
ret, name, args = m.group(1), m.group(2), m.group(3)
methods.append((ret, name, args))
return methods
def gen_mock(methods):
print("#pragma once")
print("#include \"interface.h\"")
print()
print("class MockInterface : public Interface {")
for ret, name, args in methods:
args_list = args if args else ""
print(f"public:")
print(f" MOCK_METHOD({ret}, {name}, ({args_list}));")
print("};")
if __name__ == "__main__":
content = sys.stdin.read()
methods = parse_interface(content)
gen_mock(methods)
예제 5: constexpr로 컴파일 타임 문자열 생성
C++20 consteval과 constexpr로 컴파일 타임에 문자열을 조합할 수 있습니다.
// compile_time_string.cpp
#include <array>
#include <string_view>
template <size_t N>
struct FixedString {
std::array<char, N> data{};
constexpr FixedString(const char (&str)[N]) {
for (size_t i = 0; i < N; ++i) data[i] = str[i];
}
constexpr operator std::string_view() const {
return std::string_view(data.data(), N - 1);
}
};
template <FixedString S>
struct Tag {};
// 사용: Tag<"user"> 등으로 타입 태깅
8. 자주 발생하는 에러와 해결법
에러 1: X-Macros에서 마지막 항목에 쉼표
원인: #define X(a,b) a, 형태에서 마지막 항목 뒤에 불필요한 쉼표가 들어가 enum/배열 초기화 에러가 납니다.
// ❌ 잘못된 예
#define ERROR_LIST(X) \
X(A) X(B) X(C) // C 뒤에 쉼표 없음인데, EXPAND가 , 를 붙이면 A,B,C, 가 됨
// ✅ 올바른 예 — 마지막 항목 처리
#define ERROR_LIST(X) \
X(NotFound) \
X(Timeout) \
X(Internal)
#define EXPAND(name) name
#define EXPAND_COMMA(name) name,
// enum: ERROR_LIST(EXPAND_COMMA) → NotFound, Timeout, Internal,
// 마지막 쉼표는 C++에서 허용 (trailing comma)
enum class Err { ERROR_LIST(EXPAND_COMMA) };
에러 2: 매크로 확장 시 인자 개수 불일치
원인: X(a, b, c) 형태인데 EXPAND(x)처럼 인자 1개만 받는 매크로를 사용함.
// ❌ 잘못된 예
#define LIST X(1,2) X(3,4)
#define X(a,b) a+b,
int arr[] = { LIST }; // X(1,2) → 1+2, X(3,4) → 3+4, OK
#define Y(x) x, // 인자 1개
int arr2[] = { LIST }; // LIST에 Y 적용 불가 — X는 2개 인자
// ✅ 올바른 예 — 인자 개수 맞추기
#define LIST X(1,2) X(3,4)
#define EXPAND_A(a, b) a+b,
int arr[] = { LIST }; // LIST가 X(1,2) X(3,4)이므로 EXPAND_A와 연동하려면
#define X(a,b) EXPAND_A(a,b)
// 또는 LIST 정의를 X(a,b) 형태로 통일
에러 3: 템플릿 인스턴스화 시 “undefined reference”
원인: 템플릿 정의가 헤더에 없거나, 명시적 인스턴스화한 cpp가 링크되지 않음.
// ❌ 잘못된 예 — template_impl.cpp에만 정의
// template_impl.cpp
template <typename T>
void process(T x) { /* ... */ }
template void process<int>(int);
// main.cpp
process<double>(3.14); // 링크 에러: process<double> 정의 없음
// ✅ 올바른 예 — 헤더에 정의 또는 사용 타입 명시적 인스턴스화
// process.h
template <typename T>
void process(T x) { /* ... */ }
// template_impl.cpp
#include "process.h"
template void process<int>(int);
template void process<double>(double);
에러 4: protobuf 생성 파일 경로 불일치
원인: protoc --cpp_out=generated로 생성했는데 #include "user.pb.h"로 찾지 못함.
# ✅ CMake에서 include 경로 추가
add_custom_command(
OUTPUT ${CMAKE_BINARY_DIR}/generated/user.pb.cc
COMMAND protoc --cpp_out=${CMAKE_BINARY_DIR}/generated
--proto_path=${CMAKE_SOURCE_DIR}/proto
${CMAKE_SOURCE_DIR}/proto/user.proto
DEPENDS ${CMAKE_SOURCE_DIR}/proto/user.proto
)
target_include_directories(myapp PRIVATE ${CMAKE_BINARY_DIR}/generated)
에러 5: 생성 코드가 빌드에 포함되지 않음
원인: *.generated.cpp를 add_executable에 넣지 않음.
# ✅ 생성된 소스 명시적 포함
set(GENERATED_SOURCES
${CMAKE_BINARY_DIR}/user.pb.cc
${CMAKE_BINARY_DIR}/reflection.generated.cpp
)
add_executable(myapp main.cpp ${GENERATED_SOURCES})
에러 6: Clang 도구 빌드 시 LLVM 버전 불일치
원인: 시스템 Clang과 LLVM 버전이 다르면 ABI 불일치로 크래시.
# ✅ 동일 버전 사용
# Ubuntu: sudo apt install clang-15 libclang-15-dev
# CMake: find_package(LLVM 15 REQUIRED)
에러 7: Python 생성 스크립트의 인코딩 문제
원인: 한글·특수문자가 포함된 소스를 생성할 때 UTF-8 인코딩을 지정하지 않으면 MSVC에서 깨짐.
# ✅ 생성 시 UTF-8 명시
with open("output.generated.hpp", "w", encoding="utf-8") as f:
f.write(generated_code)
// 생성된 파일 상단에 BOM 또는 pragma
// MSVC: #pragma execution_character_set("utf-8")
에러 8: X-Macros에서 매크로 재정의 충돌
원인: EXPAND를 여러 목록에서 재사용하다 보니 이전 정의가 덮어씌워짐.
// ❌ 잘못된 예
#define ERROR_LIST(X) X(A) X(B)
#define EXPAND(x) x,
enum class Err { ERROR_LIST(EXPAND) };
#define PACKET_LIST(X) X(P1) X(P2)
#define EXPAND(x) x, // ERROR_LIST의 EXPAND 덮어씀
// ✅ 올바른 예 — 목록별 고유 매크로
#define ERROR_LIST(X) X(A) X(B)
#define ERROR_EXPAND(x) x,
enum class Err { ERROR_LIST(ERROR_EXPAND) };
#define PACKET_LIST(X) X(P1) X(P2)
#define PACKET_EXPAND(x) x,
에러 9: protobuf 필드 번호 중복
원인: message User { int32 id = 1; int32 id2 = 1; }처럼 같은 필드 번호를 쓰면 직렬화가 깨짐.
# ✅ 필드 번호 고유하게
message User {
int32 id = 1;
string name = 2;
string email = 3;
# 4, 5... 순서 유지, 삭제한 번호 재사용 금지
}
에러 10: 생성 코드가 .gitignore에 있어 버전 관리 누락
원인: *.generated.*를 gitignore에 넣었는데, CI·다른 개발자 환경에서 생성 도구가 없어 빌드 실패.
해결: 생성 코드를 커밋할지, 빌드 시마다 생성할지 팀 정책을 정합니다. 생성 도구가 CI에 없으면 생성물을 커밋하는 것이 안전합니다.
# 옵션 A: 생성물 커밋 — 생성 도구 없이 빌드 가능
# *.generated.* 는 ignore 하지 않음
# 옵션 B: 생성물 미커밋 — CI에 protoc, Python 등 설치 필요
generated/
*.pb.cc
*.pb.h
9. 모범 사례
1. 단일 소스 원칙 (Single Source of Truth)
정의는 한 곳에만 두고, 나머지는 생성합니다.
// ✅ 좋은 예 — ERROR_LIST가 유일한 정의
#define ERROR_LIST(X) X(A) X(B) X(C)
// enum, 문자열, 로그 포맷 모두 여기서 파생
2. 생성 파일 명명 규칙
*.generated.cpp
*.pb.cc / *.pb.h
*_generated.h
빌드 시스템에서 *.generated.*를 정리 대상에서 제외하고, .gitignore에 넣거나 생성물만 별도 디렉터리에 둡니다.
3. 생성 코드 검증
생성된 코드에 대한 단위 테스트를 두어, 스키마·정의 변경 시 회귀를 방지합니다.
// generated_test.cpp
TEST(ErrorCode, AllCodesHaveMessages) {
for (int i = 0; i < static_cast<int>(ErrorCode::Count); ++i) {
auto msg = to_string(static_cast<ErrorCode>(i));
ASSERT_FALSE(msg.empty());
}
}
4. 매크로 사용 최소화
X-Macros가 복잡해지면, Python·Clang 등으로 전환하는 것을 고려합니다. 매크로는 단순한 목록에만 사용하는 것이 유지보수에 유리합니다.
5. 문서화
어떤 도구가 어떤 파일을 생성하는지, 빌드 순서가 어떻게 되는지 README·CMake 주석에 명시합니다.
# 1. protoc로 user.pb.cc 생성
# 2. gen_reflection으로 reflection.generated.cpp 생성
# 3. myapp 링크
6. 템플릿 인스턴스화 비용 관리
템플릿을 과도하게 사용하면 컴파일 시간과 바이너리 크기가 급증합니다. 명시적 인스턴스화로 필요한 타입만 제한합니다.
// ✅ 사용하는 타입만 명시적 인스턴스화
// serializer_impl.cpp
#include "json_serializer.h"
#include "user.h"
#include "order.h"
template struct JsonSerializer<User>;
template struct JsonSerializer<Order>;
// Config, Product 등 사용하지 않으면 인스턴스화 안 함
7. 생성 도구 버전 고정
protoc, flatc, Python 스크립트의 버전이 바뀌면 생성 결과가 달라질 수 있습니다. Docker·vcpkg·Conan으로 버전을 고정합니다.
# Dockerfile.build
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y protobuf-compiler=3.21.12-1
10. 프로덕션 패턴
패턴 1: CMake에서 코드 생성 자동화
# CMakeLists.txt
find_package(Protobuf REQUIRED)
set(PROTO_FILES proto/user.proto proto/config.proto)
protobuf_generate_cpp(PROTO_SRCS PROTO_HDRS ${PROTO_FILES})
add_executable(myapp
main.cpp
${PROTO_SRCS}
)
target_include_directories(myapp PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
target_link_libraries(myapp PRIVATE ${Protobuf_LIBRARIES})
패턴 2: 스키마 버전 관리
// user.proto
syntax = "proto3";
package myapp.v2;
message User {
int32 id = 1;
string name = 2;
string email = 3;
int32 version = 4; // 스키마 버전
}
생성 코드와 런타임 버전을 맞추기 위해, version 필드로 호환성을 검사합니다.
패턴 3: 점진적 코드 생성 도입
기존 수동 코드를 한 번에 바꾸지 말고, 새 타입부터 생성 방식을 적용하고, 점진적으로 마이그레이션합니다.
// 기존
struct LegacyPacket { /* ... */ };
std::string to_json_manual(const LegacyPacket&);
// 신규 — protobuf 생성
#include "new_packet.pb.h"
// User, Config 등은 생성 코드 사용
패턴 4: CI에서 생성 검증
# .github/workflows/ci.yml
- name: Generate and verify
run: |
python scripts/gen_enums.py
diff enums.generated.hpp enums.generated.hpp.baseline || (echo "Regenerate!" && exit 1)
정의 변경 시 생성 결과가 바뀌면 CI가 실패하도록 해, 수동 누락을 방지합니다.
패턴 5: 캐시 활용
protobuf·flatbuffers는 입력 파일 타임스탬프를 보고 변경 시에만 재생성합니다. CMake add_custom_command의 DEPENDS를 올바르게 설정하면 불필요한 재생성을 줄일 수 있습니다.
add_custom_command(
OUTPUT ${GEN_DIR}/user.pb.cc
COMMAND protoc ...
DEPENDS proto/user.proto
COMMENT "Generating user.pb.cc"
)
11. 정리
| 방식 | 시점 | 적합한 용도 |
|---|---|---|
| 템플릿 | 컴파일 타임 | 타입별 로직, 직렬화 특수화 |
| X-Macros | 전처리 | enum·에러 코드·패킷 목록 |
| Clang | 빌드 전 | 리플렉션, 리팩터링, 커스텀 분석 |
| protobuf/flatbuffers | 빌드 전 | 직렬화, RPC, 프로토콜 |
| Python/스크립트 | 빌드 전 | 커스텀 생성, 빠른 프로토타입 |
핵심 원칙:
- 정의는 한 곳: 단일 소스에서 파생
- 생성 파일 명명:
*.generated.*등 일관된 규칙 - 검증: 생성 결과에 대한 테스트
- 점진적 도입: 기존 코드와 병행 후 마이그레이션
구현 체크리스트
- 코드 생성이 필요한 반복 패턴 식별
- 방식 선택 (템플릿 / 매크로 / protobuf / Clang / 스크립트)
- 단일 소스 정의 확립
- CMake/빌드에 생성 단계 통합
- 생성 파일 include·링크 설정
- 생성 결과 검증 테스트
- 문서화 (생성 도구, 빌드 순서)
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. 직렬화, RPC 스텁, 에러 코드 매핑, 리플렉션 등록, 프로토콜 정의 등에 활용합니다. 본문의 문제 시나리오와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 템플릿 기초, 메타프로그래밍 고급, 리플렉션을 먼저 읽으면 이해가 쉽습니다.
Q. 더 깊이 공부하려면?
A. Clang libTooling, Protocol Buffers, FlatBuffers 공식 문서를 참고하세요.
참고 자료
- Clang LibTooling
- Protocol Buffers C++ Tutorial
- FlatBuffers C++
- X-Macros (Wikipedia)
- 리플렉션 #55-1
- 메타프로그래밍 고급 #55-5
한 줄 요약: 템플릿·매크로·Clang·protobuf로 반복 코드를 자동 생성해 유지보수 부담을 줄일 수 있습니다.
관련 글
- C++ 커스텀 컴파일러 패스 | Clang 플러그인·AST 변환·커스텀 진단 [#55-6]
- C++ 메타프로그래밍 고급 | SFINAE·Concepts·constexpr·타입 트레이트 가이드