실수를 잡아주는 XMacro 패턴
매크로를 굉장히 익스트림하게 쓰는 방식이라, 처음 봤을땐 이해하기가 꽤나 어렵고 가독성 측면에서 불호라 별로 좋아하지않았었다. 그런데 활용도가 너무 무궁무진하다보니, 실제로 해당 방식을 사용해서 코드 구조를 크게 갈아엎었고 전반적으로 구조가 괜찮아져서, 현재는 긍정적으로 보고있다.
솔직히 2중 매크로는 코드 로직 보기가 너무 힘든 부분은 어쩔 수 없는듯하다.
조합은 활용 방식에 따라서 무궁무진한데, 적당히 아래 방식을 조합해서 쓸 수 있다.
- enum, static_assert, template
아래 영상으로 컴파일 타임 매크로 튜플? 대충 이런 느낌으로 인지하고있었는데 이 방식에 대한 정식 명칭이 있다는걸 어제 알았다.
enum을 string으로 바꿀때 magic_enum 라이브러리를 쓸 수도 있지만, 사실 XMacro 패턴으로 커버가 가능하다. 내부 방식이 복잡한 템플릿 구조는 컴파일 속도에 악영향이 간다.
물론 매크로도 너무 많이 쓰면 컴파일 속도에 영향이 가는건 맞다…
개인적으로 느낌 장점은 이렇다.
- 실수 방지
- 자동화
- 작업 영역 축소
결론은 작업해야하는 영역 한번에 파악되고 compile time에 놓침 없이 잡아주는게 가장 큰 장점이다.
어떤 시스템에 대해 type추가로 enum에 추가하고, 관련하여 코드상에서 챙겨야하는 것들이 5가지가 있다고하자.
이럴때 enum에 추가하는 것만으로도, 나머지 챙겨야하는 5가지에 대해서 같이 작업을 하지않으면, 자동으로 컴파일 에러를 내거나, 아예 추가하는 macro 라인에 명시적으로 인자를 넣어주도록 되어있다면, compile time에 놓치는 부분을 잡게된다.
가장 큰 단점으로는 아무래도 실제 로직들은 코드가 굉장히 압축되다보니 해당 로직을 활용한 전반적인 로직 파악에 어려움이 있을 것이다.
아래는 실제 내용 정리이다.
XMacro 패턴
- XMacro 패턴은 C/C++ 전처리기를 이용해 “항목 목록을 한 곳에만” 정의하고, 이를 재정의된 매크로로 여러 형태(열거형, 문자열 테이블, switch, 생성/소멸 코드 등)로 재사용하는 방식이다.
현대 C/C++ 프로젝트에서 “한 곳에서 정의하고 여러 곳에서 활용”하는 구조가 생산성을 크게 올려준다. XMacro 패턴은 전처리기만으로 이 원칙을 구현해, 항목(타입/코드/옵션 등)을 한 번만 작성하고 그 정의를 열거형, 문자열 테이블, switch 처리, 생성/소멸 코드 등 다양한 산출물로 안전하게 재사용할 수 있게 해준다.
TL;DR
- 목록(.def)에 ITEM(…)만 적어두고, 필요할 때마다 #define ITEM(…)로 의미를 바꿔 #include 하면 enum/테이블/로직이 한 번에 맞춰진다.
- 누락/상충 같은 실수는 런타임이 아니라 컴파일 타임에 잡힌다.
- 복잡한 로직까지 끌고 가면 유지보수 지옥이니, 템플릿/코드생성으로 분리하는 게 낫다.
최소 예제: enum ↔ 문자열 매핑
목표는 항목을 하나의 목록에서만 정의하고, 그걸로 enum, 이름 테이블, to_string()을 자동 생성하는 것이다.
아래 예제는 단일 소스 오브 트루스(SSOT)인 colors.def 목록을 중심으로 돌아간다.
1
2
3
4
/* colors.def — 헤더 가드 없음, 오직 호출 목록만 */
ITEM(Red, "red", 1)
ITEM(Green, "green", 2)
ITEM(Blue, "blue", 3)
이제 이 목록을 재정의된 ITEM과 함께 포함해서 다양한 산출물을 만든다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/* color.h */
#pragma once
/* enum 생성 */
typedef enum Color {
#define ITEM(name, str, val) name = val,
#include "colors.def"
#undef ITEM
Color_Count
} Color;
/* 이름 테이블 생성 (인덱스 = enum 값) */
static const char* kColorName[Color_Count + 1] = {
#define ITEM(name, str, val) [name] = str,
#include "colors.def"
#undef ITEM
[Color_Count] = "unknown"
};
/* 안전한 문자열 변환 */
static inline const char* color_to_string(Color c) {
return (c >= 0 && c < Color_Count) ? kColorName[c] : kColorName[Color_Count];
}
/* 컴파일 타임 정합성 검사 */
#if defined(__cplusplus)
static_assert(Color_Count == 3, "Color_Count mismatch (update checks if list changes)");
#else
_Static_assert(Color_Count == 3, "Color_Count mismatch (update checks if list changes)");
#endif
- colors.def에 한 줄 더 넣으면 enum과 kColorName, color_to_string()이 같이 “자동”으로 맞춰진다.
- Color_Count 같은 센티널을 두면 배열 크기/범위 체크 같은 정적 검사가 쉬워진다.
switch와 Visitor 스타일 코드 생성
목표는 항목 추가 시 처리 로직도 누락 없이 같이 생성되도록 만드는 것이다.
1
2
3
4
5
6
7
8
9
10
11
12
/* color_print.c — switch 케이스를 목록으로부터 생성 */
#include <stdio.h>
#include "color.h"
void print_color(Color c) {
switch (c) {
#define ITEM(name, str, val) case name: printf("%s\n", str); break;
#include "colors.def"
#undef ITEM
default: printf("unknown\n"); break;
}
}
Visitor 스타일로 모든 항목에 대해 동일한 처리를 적용할 수도 있다.
1
2
3
4
5
6
7
8
9
10
/* color_visit.c — 각 항목에 공통 동작 적용 */
#include "color.h"
#define ITEM(name, str, val) void visit_##name(void);
#include "colors.def"
#undef ITEM
#define ITEM(name, str, val) void visit_##name(void) { /* 로깅/메트릭 등 */ }
#include "colors.def"
#undef ITEM
- 같은 목록으로 선언과 정의를 각각 만들 수 있다.
- 필요하면 매크로 인자에 필드를 더 추가해서 생성물을 확장하면 된다.
다필드 예제: 에러 테이블
여러 필드를 가진 항목에서 다양한 산출물을 뽑아내는 예다.
1
2
3
4
5
/* errors.def */
/* code, name, severity */
ITEM(1001, NetworkError, 2)
ITEM(1002, TimeoutError, 1)
ITEM(2001, PermissionError,3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/* error.h */
#pragma once
typedef enum Severity { Sev_Info=0, Sev_Warn=1, Sev_Error=2, Sev_Fatal=3 } Severity;
typedef enum ErrorCode {
#define ITEM(code, name, sev) EC_##name = (code),
#include "errors.def"
#undef ITEM
EC_Count
} ErrorCode;
typedef struct ErrorMeta { int code; const char* name; Severity sev; } ErrorMeta;
static const ErrorMeta kErrorTable[] = {
#define ITEM(code, name, sev) { (code), #name, (Severity)(sev) },
#include "errors.def"
#undef ITEM
};
/* 이름/심각도 조회 */
static inline const ErrorMeta* get_error_meta(ErrorCode ec) {
for (size_t i = 0; i < sizeof(kErrorTable)/sizeof(kErrorTable[0]); ++i) {
if (kErrorTable[i].code == (int)ec) return &kErrorTable[i];
}
return NULL;
}
- #name으로 식별자에서 문자열을 뽑는다.
- 목록이 진실의 원천이므로, 에러를 한 줄 추가하는 것만으로 enum/테이블/조회가 같이 늘어난다.
핵심 아이디어
- 공통 목록을 매크로 호출 형태로 정의(예: ITEM(name, value, …)).
- 사용할 때마다 ITEM을 서로 다른 의미로 #define하고 목록을 포함해 필요한 산출물을 만든다.
- 사용 후 #undef로 매크로 오염을 끊는다.
주요 활용 사례
- enum ↔ 문자열 이름(또는 설명) 매핑
- 에러 코드 테이블, 명령/옵션 정의, 직렬화/역직렬화 테이블
- Visitor 스타일 반복 코드 생성, 로깅/메트릭 항목 집합
장점
- 항목 추가/삭제 시 모든 생성물(열거형/테이블/처리 코드)이 자동으로 맞춰진다.
- 런타임 오버헤드 없이 컴파일 타임에 코드가 만들어진다.
- 중복 정의로 인한 버그가 줄고, 리뷰 범위도 명확해진다.
주의사항
- #define/#undef 범위를 블록 단위로 확실히 관리해서 전역 오염을 막는다.
- 목록 파일(.def 등)에는 헤더 가드를 두지 않는 게 보통이며, 포함 순서/횟수를 조심한다.
- 쉼표/세미콜론, 트레일링 콤마, 가변 인자 등 전처리기 문법에 민감하다.
- 디버깅할 때는 전개 결과를 보기 위해 컴파일러 전처리 출력(예: -E)이나 빌드 로그를 적극 활용한다.
- 복잡한 로직 생성에는 과하게 쓰지 말고 템플릿/코드생성 도구로 분리하는 게 낫다.
베스트 프랙티스
- 목록 매크로 이름은 대문자(예: ITEM, DEFINE_…)로 의도를 분명히 한다.
- 각 항목에 의미 있는 필드를 넣고 주석으로 문서화한다.
- 사용 블록마다 #define ITEM → #include list.def → #undef ITEM 패턴을 철저히 지킨다.
- 항목 개수를 enum 센티널로 노출해 배열 크기 같은 정적 검사에 써먹는다.
- 테스트/리뷰 시 목록 변경의 파급 범위를 꼭 확인한다(모든 생성물 재빌드).
대안
- C++ 템플릿/constexpr/메타프로그래밍, 리플렉션 라이브러리
- 빌드 단계 코드 생성(스크립트/DSL)로 전처리기 의존도 줄이기
요약
- XMacro는 “한 번 정의, 여러 번 활용”을 전처리기로 구현하는 패턴이고, 데이터/코드 동기화를 컴파일 타임에 강하게 보장한다.
- 작은 목록에서 시작해 enum/테이블/처리를 함께 생성해보면 효과가 바로 보인다.