C++ 리플렉션 구현 | 타입 정보·메타데이터·자동 직렬화 [#55-1]
이 글의 핵심
C++에는 Java나 C#처럼 리플렉션(실행 중에 타입·멤버 정보를 조회하는 기능)이 표준으로 없습니다. 그래서 직렬화, ORM, 에디터 프로퍼티 바인딩처럼 "타입 구조를 모르는 상태에서 멤버를 순회"해야 할 때마다 수동 반복 코드를 작성하거나 매크로·코드 생성에 의존하게 됩니다.
들어가며: 구조체가 늘어날수록 직렬화 코드가 폭발한다
"User, Order, Product… 매번 to_json·from_json을 손으로 짜기엔 한계가 있어요"
C++에는 Java나 C#처럼 리플렉션(실행 중에 타입·멤버 정보를 조회하는 기능)이 표준으로 없습니다. 그래서 직렬화, ORM, 에디터 프로퍼티 바인딩처럼 "타입 구조를 모르는 상태에서 멤버를 순회"해야 할 때마다 수동 반복 코드를 작성하거나 매크로·코드 생성에 의존하게 됩니다.
비유하면 "도서관에서 책 제목·저자·위치를 자동으로 조회하는 시스템"이 있는데, C++에는 그런 카탈로그가 없어서 책마다 직접 목록을 손으로 적어 두는 상황입니다. 구조체가 10개, 100개로 늘어나면 유지보수 부담이 급격히 커집니다.
flowchart LR
subgraph problem["문제 상황"]
P1[User 구조체] --> P2[to_json 수동 작성]
P3[Order 구조체] --> P4[to_json 수동 작성]
P5[Product 구조체] --> P6[to_json 수동 작성]
P2 --> P7[멤버 추가 시 누락 위험]
P4 --> P7
P6 --> P7
end
subgraph solution["리플렉션 해결"]
S1[타입 등록 1회] --> S2[멤버 메타데이터]
S2 --> S3[자동 직렬화]
S3 --> S4[선언만으로 처리]
end
**문제의 코드 (수동 직렬화)**에서는 User, Order, Product 등 구조체가 늘어날 때마다 to_json, from_json을 각각 작성해야 합니다. 멤버를 추가·삭제·이름 변경할 때마다 직렬화 코드도 함께 수정해야 하고, 누락 시 런타임 버그로 이어집니다.
리플렉션으로 해결하면 타입과 멤버를 한 번 등록해 두고, 메타데이터 기반으로 직렬화·바인딩·검증을 자동화할 수 있습니다. C++26에 Reflection 제안이 논의 중이지만, 현재는 수동 등록·매크로·서드파티 라이브러리로 구현해야 합니다.
이 글을 읽으면:
- C++ 리플렉션의 개념과 한계를 이해할 수 있습니다.
- 수동 등록·매크로·RTTR 등 실전 구현 방식을 선택할 수 있습니다.
- 자주 발생하는 에러와 워크어라운드를 알 수 있습니다.
- 프로덕션에서 활용할 수 있는 패턴을 익힐 수 있습니다.
추가 문제 시나리오
시나리오 1: 게임 에디터 프로퍼티 패널
엔진에서 Transform, RigidBody, Mesh 등 컴포넌트의 멤버를 에디터 UI에 자동으로 노출하려면, 각 타입의 프로퍼티 이름·타입·범위를 런타임에 조회해야 합니다. 리플렉션이 없으면 컴포넌트마다 registerProperties() 같은 수동 등록 코드를 반복 작성해야 합니다.
시나리오 2: 네트워크 프로토콜 직렬화
서버-클라이언트 간 패킷 구조체를 JSON/바이너리로 변환할 때, 패킷 타입이 50개 이상이면 수동 직렬화는 유지보수 불가에 가깝습니다. 리플렉션으로 "멤버 순회 → 직렬화"를 공통화하면 새 패킷 추가 시 선언만 하면 됩니다.
시나리오 3: ORM/데이터베이스 매핑User, Article 같은 엔티티를 DB 테이블과 매핑할 때, 컬럼 이름·타입을 런타임에 알아야 쿼리 생성·결과 바인딩이 가능합니다. 리플렉션이 있으면 엔티티 클래스 선언만으로 매핑을 자동화할 수 있습니다.
시나리오 4: 스크립트 바인딩 (Lua, JavaScript)
C++ 클래스를 스크립트에 노출할 때, 프로퍼티·메서드 목록을 동적으로 조회해야 합니다. 수동 바인딩은 클래스가 늘어날수록 코드가 폭발하고, 리플렉션으로 자동 바인딩을 구현할 수 있습니다.
시나리오 5: 설정 파일 로드
YAML/JSON 설정을 Config 구조체로 로드할 때, 키 이름과 멤버를 매칭하려면 멤버 이름을 런타임에 알아야 합니다. 리플렉션으로 "키 → 멤버" 매핑을 자동화할 수 있습니다.
목차
- 리플렉션이란?
- 핵심 구현: 수동 등록 기반
- 완전한 리플렉션 예제
- 매크로 기반 반복 제거
- RTTR 라이브러리 활용
- 자주 발생하는 에러와 해결법
- 워크어라운드와 한계 극복
- 프로덕션 패턴
- 성능 고려사항
- C++26 Reflection 전망
1. 리플렉션이란?
기본 개념
**리플렉션(Reflection)**은 프로그램이 실행 중 또는 컴파일 타임에 자신의 타입·멤버·메서드 정보를 조회·순회할 수 있는 기능입니다. Java의 Class, C#의 Type, Python의 getattr 등이 이에 해당합니다.
C++에는 표준 리플렉션이 없습니다. typeid로 타입 이름만 얻을 수 있고, 멤버 목록·이름·타입을 런타임에 조회하는 표준 방법은 없습니다. 따라서 직접 메타데이터를 구축해야 합니다.
flowchart TB
subgraph cpp["C++ 현재"]
A1[구조체 정의] --> A2[수동 등록 또는 매크로]
A2 --> A3[타입 레지스트리]
A3 --> A4[런타임 조회]
end
subgraph future["C++26 제안"]
B1[구조체 정의] --> B2[std::meta::members_of]
B2 --> B3[컴파일 타임 반사]
B3 --> B4[자동 메타데이터]
end
런타임 vs 컴파일 타임 리플렉션
| 구분 | 런타임 리플렉션 | 컴파일 타임 리플렉션 |
|---|---|---|
| 시점 | 실행 중 | 컴파일 시 |
| 정보 | 타입 이름, 멤버 목록, 값 접근 | 타입, 멤버, 이름 등 |
| C++ 현재 | 수동 등록, RTTR 등 | 템플릿, constexpr, 매크로 |
| C++26 제안 | - | std::meta::* |
| 용도 | 직렬화, UI 바인딩, 플러그인 | 코드 생성, 직렬화 템플릿 |
2. 핵심 구현: 수동 등록 기반
타입 레지스트리 설계
가장 기본적인 방식은 타입별로 프로퍼티를 수동 등록하고, 문자열 이름으로 접근하는 레지스트리를 만드는 것입니다.
// reflection_core.h — 최소한의 리플렉션 코어
#pragma once
#include <functional>
#include <memory>
#include <string>
#include <unordered_map>
#include <vector>
#include <any>
// 프로퍼티 접근자: (객체*, 값) → get/set
using Getter = std::function<void(const void* obj, void* out)>;
using Setter = std::function<void(void* obj, const void* value)>;
struct PropertyInfo {
std::string name;
std::string typeName;
Getter getter;
Setter setter;
};
struct TypeInfo {
std::string name;
std::vector<PropertyInfo> properties;
std::function<void*()> defaultConstructor; // 인스턴스 생성
const PropertyInfo* findProperty(const std::string& name) const {
for (const auto& p : properties) {
if (p.name == name) return &p;
}
return nullptr;
}
};
// 전역 타입 레지스트리 (실무에서는 싱글톤 또는 DI로 관리)
class TypeRegistry {
public:
static TypeRegistry& instance() {
static TypeRegistry reg;
return reg;
}
void registerType(const std::string& name, TypeInfo info) {
types_[name] = std::move(info);
}
const TypeInfo* getType(const std::string& name) const {
auto it = types_.find(name);
return it != types_.end() ? &it->second : nullptr;
}
private:
std::unordered_map<std::string, TypeInfo> types_;
};
User 구조체 등록 예시
// user.h
#pragma once
#include <string>
struct User {
int id;
std::string name;
std::string email;
};
// user_reflection.cpp — 등록은 cpp에서 (헤더 오염 방지)
#include "user.h"
#include "reflection_core.h"
#include <cstring>
void registerUserType() {
TypeInfo info;
info.name = "User";
// id 프로퍼티
info.properties.push_back({
"id", "int",
{
*static_cast<int*>(out) = static_cast<const User*>(obj)->id;
},
{
static_cast<User*>(obj)->id = *static_cast<const int*>(value);
}
});
// name 프로퍼티
info.properties.push_back({
"name", "std::string",
{
new (out) std::string(static_cast<const User*>(obj)->name);
},
{
static_cast<User*>(obj)->name = *static_cast<const std::string*>(value);
}
});
// email 프로퍼티
info.properties.push_back({
"email", "std::string",
{
new (out) std::string(static_cast<const User*>(obj)->email);
},
{
static_cast<User*>(obj)->email = *static_cast<const std::string*>(value);
}
});
info.defaultConstructor = { return new User(); };
TypeRegistry::instance().registerType("User", std::move(info));
}
리플렉션을 이용한 JSON 직렬화 (간단 버전)
// json_serializer.h — 리플렉션 기반 직렬화
#pragma once
#include "reflection_core.h"
#include <sstream>
#include <string>
std::string toJsonReflection(const void* obj, const TypeInfo& type) {
std::ostringstream oss;
oss << "{";
for (size_t i = 0; i < type.properties.size(); ++i) {
if (i > 0) oss << ",";
const auto& p = type.properties[i];
oss << "\"" << p.name << "\":";
if (p.typeName == "int") {
int val;
p.getter(obj, &val);
oss << val;
} else if (p.typeName == "std::string") {
std::string val;
p.getter(obj, &val);
oss << "\"" << val << "\"";
}
}
oss << "}";
return oss.str();
}
// from_json — JSON에서 객체로 로드 (nlohmann/json 사용 시)
void fromJsonReflection(void* obj, const TypeInfo& type, const nlohmann::json& j) {
for (const auto& p : type.properties) {
if (!j.contains(p.name)) continue;
if (p.typeName == "int") {
int val = j[p.name].get<int>();
p.setter(obj, &val);
} else if (p.typeName == "std::string") {
std::string val = j[p.name].get<std::string>();
p.setter(obj, &val);
}
}
}
핵심 포인트:
- 등록은 cpp 파일에서 수행해 헤더를 오염시키지 않습니다.
Getter/Setter는std::function으로, 타입별로 다른 람다를 등록합니다.std::string등 비트리비얼 타입은 placement new로out에 복사합니다.
3. 완전한 리플렉션 예제
예제 1: 설정 로더 (YAML/JSON 키 → 멤버 매핑)
// config.h
#pragma once
#include <string>
#include <vector>
struct GameConfig {
std::string title;
int screenWidth;
int screenHeight;
bool fullscreen;
std::vector<std::string> levels;
};
// config_reflection.cpp
#include "config.h"
#include "reflection_core.h"
#include "json_serializer.h"
#include <nlohmann/json.hpp> // 또는 간단한 JSON 파서
void registerGameConfigType() {
TypeInfo info;
info.name = "GameConfig";
auto add = [&info](auto name, auto typeName, auto get, auto set) {
info.properties.push_back({
name, typeName,
[get](const void* o, void* out) { get(static_cast<const GameConfig*>(o), out); },
[set](void* o, const void* v) { set(static_cast<GameConfig*>(o), v); }
});
};
add("title", "std::string",
{ new (out) std::string(c->title); },
{ c->title = *static_cast<const std::string*>(v); });
add("screenWidth", "int",
{ *static_cast<int*>(out) = c->screenWidth; },
{ c->screenWidth = *static_cast<const int*>(v); });
add("screenHeight", "int",
{ *static_cast<int*>(out) = c->screenHeight; },
{ c->screenHeight = *static_cast<const int*>(v); });
add("fullscreen", "bool",
{ *static_cast<bool*>(out) = c->fullscreen; },
{ c->fullscreen = *static_cast<const bool*>(v); });
info.defaultConstructor = { return new GameConfig(); };
TypeRegistry::instance().registerType("GameConfig", std::move(info));
}
예제 2: 프로퍼티 검증 (필수 필드 체크)
// reflection_validate.h
#pragma once
#include "reflection_core.h"
#include <string>
#include <vector>
struct ValidationError {
std::string propertyName;
std::string message;
};
std::vector<ValidationError> validateRequired(const void* obj, const TypeInfo& type,
const std::vector<std::string>& required) {
std::vector<ValidationError> errors;
for (const auto& name : required) {
auto* prop = type.findProperty(name);
if (!prop) {
errors.push_back({name, "property not found"});
continue;
}
if (prop->typeName == "std::string") {
std::string val;
prop->getter(obj, &val);
if (val.empty()) {
errors.push_back({name, "required field is empty"});
}
}
}
return errors;
}
예제 3: main에서 등록 및 사용
// main.cpp
#include "user.h"
#include "config.h"
#include "reflection_core.h"
#include "json_serializer.h"
#include "reflection_validate.h"
#include <iostream>
void registerAllTypes() {
registerUserType();
registerGameConfigType();
}
int main() {
registerAllTypes();
User user{1, "Alice", "[email protected]"};
auto* type = TypeRegistry::instance().getType("User");
if (type) {
std::cout << toJsonReflection(&user, *type) << "\n";
}
auto errors = validateRequired(&user, *type, {"name", "email"});
for (const auto& e : errors) {
std::cerr << e.propertyName << ": " << e.message << "\n";
}
return 0;
}
4. 매크로 기반 반복 제거
수동 등록이 반복적이므로, 매크로로 등록 코드를 줄일 수 있습니다.
// reflection_macro.h
#pragma once
#include "reflection_core.h"
#define REFLECT_TYPE(TypeName) \
TypeInfo __reflect_##TypeName(); \
void __register_##TypeName() { \
TypeRegistry::instance().registerType(#TypeName, __reflect_##TypeName()); \
}
#define REFLECT_PROPERTY(Type, Member) \
info.properties.push_back({ \
#Member, #Type, \
{ \
*static_cast<decltype(std::declval<Type>().Member)*>(out) = \
static_cast<const Type*>(o)->Member; \
}, \
{ \
static_cast<Type*>(o)->Member = *static_cast<const decltype(std::declval<Type>().Member)*>(v); \
} \
});
// 사용 예
// user_reflection.cpp
#include "user.h"
#include "reflection_macro.h"
REFLECT_TYPE(User)
TypeInfo __reflect_User() {
TypeInfo info;
info.name = "User";
REFLECT_PROPERTY(User, id);
REFLECT_PROPERTY(User, name);
REFLECT_PROPERTY(User, email);
info.defaultConstructor = { return new User(); };
return info;
}
장점: 반복 코드 감소.
단점: 매크로 디버깅이 어렵고, std::string 등 비트리비얼 타입은 별도 처리 필요.
5. RTTR 라이브러리 활용
RTTR(Run Time Type Reflection)은 C++11 기반의 오픈소스 리플렉션 라이브러리입니다. 외부 전처리기 없이, cpp 파일에서만 등록하면 됩니다.
// user.h
#pragma once
#include <string>
struct User {
int id;
std::string name;
std::string email;
};
// user_rttr.cpp
#include "user.h"
#include <rttr/registration>
RTTR_REGISTRATION {
rttr::registration::class_<User>("User")
.property("id", &User::id)
.property("name", &User::name)
.property("email", &User::email)
.constructor<>();
}
// main_rttr.cpp
#include "user.h"
#include <rttr/registration>
#include <rttr/type>
#include <iostream>
int main() {
User user{1, "Alice", "[email protected]"};
auto type = rttr::type::get<User>();
for (auto& prop : type.get_properties()) {
auto value = prop.get_value(user);
std::cout << prop.get_name() << " = " << value.to_string() << "\n";
}
// 프로퍼티로 값 설정
type.get_property("name").set_value(user, std::string("Bob"));
std::cout << "name after set: " << user.name << "\n";
return 0;
}
RTTR 특징:
- 헤더 오염 없음 (등록은 cpp에서)
- 프로퍼티, 메서드, 생성자, enum 지원
rttr::variant로 타입 소거된 값 전달- 플러그인/공유 라이브러리에서도 동작
6. 자주 발생하는 에러와 해결법
에러 1: "멤버 추가했는데 직렬화에 반영 안 됨"
원인: 수동 등록 시 새 멤버를 등록 목록에 추가하지 않음.
// ❌ 잘못된 예 — User에 age 추가했지만 등록 누락
struct User {
int id;
std::string name;
std::string email;
int age; // 새로 추가 — 등록 깜빡함
};
// age가 JSON에 안 나옴
// ✅ 올바른 예 — age도 등록
info.properties.push_back({
"age", "int",
{ *static_cast<int*>(out) = static_cast<const User*>(obj)->age; },
{ static_cast<User*>(obj)->age = *static_cast<const int*>(value); }
});
해결법: 구조체 변경 시 등록 코드도 함께 수정하는 체크리스트를 두거나, 단위 테스트로 "등록된 프로퍼티 수 == 구조체 멤버 수"를 검증합니다.
에러 2: "비트리비얼 타입을 Getter에서 복사할 때 크래시"
원인: int처럼 memcpy로 복사 가능한 타입과 std::string처럼 생성자가 필요한 타입을 구분하지 않고 처리함.
// ❌ 잘못된 예 — std::string을 memcpy로 복사
p.getter = {
memcpy(out, &static_cast<const User*>(obj)->name, sizeof(std::string));
};
// 미정의 동작: std::string은 placement new 필요
// ✅ 올바른 예 — placement new로 복사
p.getter = {
new (out) std::string(static_cast<const User*>(obj)->name);
};
에러 3: "등록 순서가 바뀌어서 잘못된 프로퍼티에 값이 들어감"
원인: JSON 키 순서와 등록 순서를 혼동하거나, 이름이 아닌 인덱스로 매칭함.
// ❌ 잘못된 예 — 인덱스로 매칭 (순서 의존)
for (size_t i = 0; i < keys.size(); ++i) {
type.properties[i].setter(obj, &values[i]); // 순서 바뀌면 잘못된 매핑
}
// ✅ 올바른 예 — 이름으로 매칭
auto* prop = type.findProperty(key);
if (prop) prop->setter(obj, &value);
에러 4: "ODR 위반 — 등록이 여러 번 실행됨"
원인: 헤더에서 registerAllTypes()를 호출하고, 그 헤더를 여러 cpp에서 include하면 등록이 중복 실행될 수 있음.
// ❌ 잘못된 예 — header에서 호출
// reflection_init.h
#include "user_reflection.h"
#include "config_reflection.h"
registerUserType(); // 여러 TU에서 include 시 중복
registerConfigType();
// ✅ 올바른 예 — main 또는 단일 초기화 cpp에서만 호출
// main.cpp
int main() {
registerAllTypes(); // 한 곳에서만
// ...
}
에러 5: "상속된 클래스의 베이스 멤버가 조회 안 됨"
원인: 파생 클래스만 등록하고 베이스 멤버를 등록하지 않음.
// ❌ 잘못된 예 — Derived만 등록
struct Base { int id; };
struct Derived : Base { std::string name; };
// id는 조회 안 됨
// ✅ 올바른 예 — 베이스 멤버도 포함해 등록
void registerDerivedType() {
TypeInfo info;
info.name = "Derived";
// Base 멤버
info.properties.push_back({"id", "int", ...});
// Derived 멤버
info.properties.push_back({"name", "std::string", ...});
// ...
}
에러 6: "const 객체에 Setter 호출 시 컴파일 에러"
원인: Getter는 const void*를 받지만, Setter에서 void*로 캐스팅할 때 원본이 const이면 수정 시도로 인한 미정의 동작.
// ❌ 잘못된 예
void loadFromJson(const void* obj, const json& j) {
// obj가 const인데 setter 호출
prop.setter(const_cast<void*>(obj), &value); // 위험
}
// ✅ 올바른 예 — 로드용 API는 non-const 객체만 받음
void loadFromJson(void* obj, const TypeInfo& type, const json& j);
7. 워크어라운드와 한계 극복
워크어라운드 1: private 멤버 접근
리플렉션으로 private 멤버에 접근하려면, friend 함수 또는 접근자 람다를 등록 시 제공합니다.
class SecretData {
int secret_;
public:
int getSecret() const { return secret_; }
void setSecret(int v) { secret_ = v; }
};
// 등록 시 getter/setter를 public 메서드로 연결
info.properties.push_back({
"secret", "int",
{
*static_cast<int*>(out) = static_cast<const SecretData*>(o)->getSecret();
},
{
static_cast<SecretData*>(o)->setSecret(*static_cast<const int*>(v));
}
});
워크어라운드 2: 템플릿 타입의 등록
std::vector<int>, std::map<std::string, int> 등 템플릿 인스턴스는 타입 이름이 길어서, "vector_int" 같은 별칭으로 등록하거나, 타입 이름 생성기를 두는 것이 좋습니다.
template <typename T>
std::string typeName() {
if constexpr (std::is_same_v<T, int>) return "int";
else if constexpr (std::is_same_v<T, std::string>) return "std::string";
else if constexpr (std::is_same_v<T, std::vector<int>>) return "std::vector<int>";
// ...
return "unknown";
}
워크어라운드 3: 플러그인에서 동적 타입 등록
플러그인이 로드될 때 자신의 타입을 레지스트리에 등록하고, 언로드 시 제거해야 합니다. 레지스트리에 등록 해제 API를 두고, 플러그인 로드/언로드 훅에서 호출합니다.
void TypeRegistry::unregisterType(const std::string& name) {
types_.erase(name);
}
// 플러그인 언로드 시
void onPluginUnload(const std::string& pluginName) {
for (const auto& name : getTypesFromPlugin(pluginName)) {
TypeRegistry::instance().unregisterType(name);
}
}
워크어라운드 4: 코드 생성으로 등록 자동화
구조체 정의를 파싱해서 등록 코드를 생성하는 도구(예: custom clang tool, Python 스크립트)를 두면, 수동 등록을 거의 제거할 수 있습니다. 빌드 단계에서 struct_parser.py User.h → user_reflection.generated.cpp를 생성하고, CMake에서 이 파일을 빌드에 포함합니다.
8. 프로덕션 패턴
패턴 1: 타입별 직렬화 포맷 분리
JSON, 바이너리, 프로토콜 버퍼 등 포맷이 다르면, 방문자 패턴으로 포맷별 직렬화를 분리합니다.
struct SerializationVisitor {
virtual void visitInt(const std::string& name, int value) = 0;
virtual void visitString(const std::string& name, const std::string& value) = 0;
// ...
};
void serializeWithVisitor(const void* obj, const TypeInfo& type, SerializationVisitor& v) {
for (const auto& p : type.properties) {
if (p.typeName == "int") {
int val;
p.getter(obj, &val);
v.visitInt(p.name, val);
} else if (p.typeName == "std::string") {
std::string val;
p.getter(obj, &val);
v.visitString(p.name, val);
}
}
}
패턴 2: 메타데이터 확장 (UI 힌트, 검증 규칙)
프로퍼티에 "범위", "에디터 타입" 등을 메타데이터로 붙입니다.
struct PropertyInfo {
std::string name;
std::string typeName;
Getter getter;
Setter setter;
std::unordered_map<std::string, std::string> metadata; // "min", "max", "editor"
};
// 등록 시
info.properties.push_back({
"screenWidth", "int", getter, setter,
{{"min", "0"}, {"max", "7680"}, {"editor", "slider"}}
});
패턴 3: 스키마 생성 (API 문서, 클라이언트 코드 생성)
리플렉션 정보로 JSON Schema, OpenAPI 스펙, 또는 클라이언트 SDK 코드를 생성합니다.
std::string toJsonSchema(const TypeInfo& type) {
nlohmann::json schema;
schema["type"] = "object";
schema["properties"] = nlohmann::json::object();
for (const auto& p : type.properties) {
schema["properties"][p.name]["type"] = typeNameToJson(p.typeName);
}
return schema.dump(2);
}
패턴 4: 변경 감지 (Dirty 플래그)
에디터에서 "저장되지 않은 변경"을 추적할 때, 리플렉션으로 객체의 스냅샷을 저장하고 나중에 비교합니다.
std::vector<std::byte> takeSnapshot(const void* obj, const TypeInfo& type);
bool hasChanged(const void* obj, const TypeInfo& type, const std::vector<std::byte>& snapshot);
패턴 5: 버전별 직렬화 (이전 버전 호환)
멤버 이름 변경·삭제 시 이전 포맷과 호환하려면, 메타데이터에 "serializedName" 또는 "sinceVersion"을 두고, 로드 시 버전에 따라 매핑합니다.
// "old_id" → "id" 매핑 (v1 호환)
info.metadata["alias"] = "old_id"; // 이전 필드 이름
9. 성능 고려사항
레지스트리 조회
타입 이름으로 조회하는 getType(name)은 unordered_map이면 O(1)입니다. 프로퍼티 이름으로 조회하는 findProperty는 선형 검색이므로, 프로퍼티가 많으면 unordered_map으로 전환하는 것이 좋습니다.
// 프로퍼티가 10개 이상이면 map 사용
std::unordered_map<std::string, PropertyInfo> propertiesByName_;
Getter/Setter 오버헤드
std::function 호출은 가상 함수 호출 수준의 오버헤드가 있습니다. 직렬화가 병목이면, 타입별 전용 함수를 템플릿으로 두고, typeid 또는 타입 인덱스로 분기하는 방식으로 인라인 최적화를 노릴 수 있습니다.
직렬화 경로 최적화
자주 직렬화되는 타입은 캐시된 TypeInfo 포인터를 두고, 매번 getType(name)을 호출하지 않습니다.
// 앱 초기화 시 한 번만
const TypeInfo* userType = TypeRegistry::instance().getType("User");
// 직렬화 루프에서
for (const auto& user : users) {
toJsonReflection(&user, *userType);
}
10. C++26 Reflection 전망
C++26을 목표로 Reflection 제안이 논의 중입니다. std::meta::members_of(^T)처럼 반사 연산자로 타입·멤버를 컴파일 타임에 조회하는 문법이 제안되어 있습니다.
// 가상의 C++26 문법 (제안 단계)
struct User {
int id;
std::string name;
};
template <typename T>
void serialize(const T& obj) {
for (auto member : std::meta::members_of(^T)) {
auto name = std::meta::name_of(member);
auto value = std::meta::value_of(member, obj);
// ...
}
}
현재 대안과 비교하면:
| 방식 | 장점 | 단점 |
|---|---|---|
| 수동 등록 | 명확, 의존성 없음 | 반복 작업, 누락 위험 |
| 매크로 | 반복 감소 | 가독성 저하, 디버깅 어려움 |
| RTTR | 기능 풍부, 검증됨 | 외부 라이브러리 의존 |
| 코드 생성 | 자동화 | 빌드 단계 추가 |
| C++26 Reflection | 표준, 선언적 | 아직 제안 단계 |
정리
| 항목 | 설명 |
|---|---|
| 리플렉션 | 타입·멤버 정보를 런타임/컴파일 타임에 조회 |
| 수동 등록 | Getter/Setter를 레지스트리에 등록 |
| 매크로 | 반복 등록 코드 감소 |
| RTTR | C++11 기반 오픈소스 라이브러리 |
| 에러 | 비트리비얼 타입 복사, ODR, 상속 멤버 누락 |
| 워크어라운드 | friend, 타입 별칭, 코드 생성 |
| 프로덕션 | 방문자 패턴, 메타데이터, 스키마 생성 |
핵심 원칙:
- 등록은 cpp에서, 헤더 오염 최소화
- 비트리비얼 타입은 placement new로 복사
- 이름 기반 매칭, 인덱스 의존 금지
- 프로덕션에서는 메타데이터·버전 관리 고려
구현 체크리스트
- 타입 레지스트리 설계 (Getter/Setter, TypeInfo)
- 등록은 cpp 파일에서만 (헤더 오염 방지)
- 비트리비얼 타입(std::string 등) placement new 처리
- 프로퍼티 이름으로 매칭 (순서 의존 금지)
- ODR 방지 — 등록 초기화는 단일 진입점
- 상속 시 베이스 멤버도 등록
- (선택) RTTR 또는 코드 생성 도구 도입
- 단위 테스트로 등록 누락 검증
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. 게임 엔진 에디터, ORM 구현, 자동 직렬화, 스크립팅 바인딩 등에 활용합니다. 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference, RTTR 문서, Reflection TS 제안을 참고하세요.
한 줄 요약: 타입 정보·메타데이터·자동 직렬화를 마스터할 수 있습니다.