[2026] C++ 리플렉션 구현 | 타입 정보·메타데이터·자동 직렬화 [#55-1]
이 글의 핵심
C++에는 Java나 C#처럼 리플렉션(실행 중에 타입·멤버 정보를 조회하는 기능)이 표준으로 없습니다. 그래서 직렬화, ORM, 에디터 프로퍼티 바인딩처럼 타입 구조를 모르는 상태에서 멤버를 순회해야 할 때마다 수동 반복 코드를 작성하거나 매크로·코드 생성에 의존하게 됩니다.
들어가며: 구조체가 늘어날수록 직렬화 코드가 폭발한다
”User, Order, Product… 매번 to_json·from_json을 손으로 짜기엔 한계가 있어요”
C++에는 Java나 C#처럼 리플렉션(실행 중에 타입·멤버 정보를 조회하는 기능)이 표준으로 없습니다. 그래서 직렬화, ORM, 에디터 프로퍼티 바인딩처럼 “타입 구조를 모르는 상태에서 멤버를 순회”해야 할 때마다 수동 반복 코드를 작성하거나 매크로·코드 생성에 의존하게 됩니다. 비유하면 “도서관에서 책 제목·저자·위치를 자동으로 조회하는 시스템”이 있는데, C++에는 그런 카탈로그가 없어서 책마다 직접 목록을 손으로 적어 두는 상황입니다. 구조체가 10개, 100개로 늘어나면 유지보수 부담이 급격히 커집니다. 다음은 mermaid를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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 등 실전 구현 방식을 선택할 수 있습니다.
- 자주 발생하는 에러와 워크어라운드를 알 수 있습니다.
- 프로덕션에서 활용할 수 있는 패턴을 익힐 수 있습니다.
실무 적용 경험: 이 글은 대규모 C++ 프로젝트에서 실제로 겪은 문제와 해결 과정을 바탕으로 작성되었습니다. 책이나 문서에서 다루지 않는 실전 함정과 디버깅 팁을 포함합니다.
추가 문제 시나리오
시나리오 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로 타입 이름만 얻을 수 있고, 멤버 목록·이름·타입을 런타임에 조회하는 표준 방법은 없습니다. 따라서 직접 메타데이터를 구축해야 합니다.
아래 코드는 mermaid를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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. 핵심 구현: 수동 등록 기반
타입 레지스트리 설계
가장 기본적인 방식은 타입별로 프로퍼티를 수동 등록하고, 문자열 이름으로 접근하는 레지스트리를 만드는 것입니다. 다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 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 구조체 등록 예시
아래 코드는 cpp를 사용한 구현 예제입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// user.h
#pragma once
#include <string>
struct User {
int id;
std::string name;
std::string email;
};
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 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 직렬화 (간단 버전)
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 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 키 → 멤버 매핑)
아래 코드는 cpp를 사용한 구현 예제입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// config.h
#pragma once
#include <string>
#include <vector>
struct GameConfig {
std::string title;
int screenWidth;
int screenHeight;
bool fullscreen;
std::vector<std::string> levels;
};
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 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: 프로퍼티 검증 (필수 필드 체크)
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 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에서 등록 및 사용
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 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", "alice@example.com"};
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. 매크로 기반 반복 제거
수동 등록이 반복적이므로, 매크로로 등록 코드를 줄일 수 있습니다. 다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 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 파일에서만 등록하면 됩니다. 아래 코드는 cpp를 사용한 구현 예제입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// user.h
#pragma once
#include <string>
struct User {
int id;
std::string name;
std::string email;
};
아래 코드는 cpp를 사용한 구현 예제입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 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<>();
}
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// main_rttr.cpp
#include "user.h"
#include <rttr/registration>
#include <rttr/type>
#include <iostream>
int main() {
User user{1, "Alice", "alice@example.com"};
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: “멤버 추가했는데 직렬화에 반영 안 됨”
원인: 수동 등록 시 새 멤버를 등록 목록에 추가하지 않음. 아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ❌ 잘못된 예 — User에 age 추가했지만 등록 누락
struct User {
int id;
std::string name;
std::string email;
int age; // 새로 추가 — 등록 깜빡함
};
// age가 JSON에 안 나옴
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ✅ 올바른 예 — 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처럼 생성자가 필요한 타입을 구분하지 않고 처리함.
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ❌ 잘못된 예 — std::string을 memcpy로 복사
p.getter = {
memcpy(out, &static_cast<const User*>(obj)->name, sizeof(std::string));
};
// 미정의 동작: std::string은 placement new 필요
다음은 간단한 cpp 코드 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ✅ 올바른 예 — placement new로 복사
p.getter = {
new (out) std::string(static_cast<const User*>(obj)->name);
};
에러 3: “등록 순서가 바뀌어서 잘못된 프로퍼티에 값이 들어감”
원인: JSON 키 순서와 등록 순서를 혼동하거나, 이름이 아닌 인덱스로 매칭함. 다음은 간단한 cpp 코드 예제입니다. 반복문으로 데이터를 처리합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ❌ 잘못된 예 — 인덱스로 매칭 (순서 의존)
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하면 등록이 중복 실행될 수 있음.
아래 코드는 cpp를 사용한 구현 예제입니다. 필요한 모듈을 import하고. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ❌ 잘못된 예 — header에서 호출
// reflection_init.h
#include "user_reflection.h"
#include "config_reflection.h"
registerUserType(); // 여러 TU에서 include 시 중복
registerConfigType();
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ✅ 올바른 예 — main 또는 단일 초기화 cpp에서만 호출
// main.cpp
int main() {
registerAllTypes(); // 한 곳에서만
// ...
}
에러 5: “상속된 클래스의 베이스 멤버가 조회 안 됨”
원인: 파생 클래스만 등록하고 베이스 멤버를 등록하지 않음. 다음은 간단한 cpp 코드 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ❌ 잘못된 예 — Derived만 등록
struct Base { int id; };
struct Derived : Base { std::string name; };
// id는 조회 안 됨
아래 코드는 cpp를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ✅ 올바른 예 — 베이스 멤버도 포함해 등록
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이면 수정 시도로 인한 미정의 동작.
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ❌ 잘못된 예
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 함수 또는 접근자 람다를 등록 시 제공합니다. 다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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” 같은 별칭으로 등록하거나, 타입 이름 생성기를 두는 것이 좋습니다.
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
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를 두고, 플러그인 로드/언로드 훅에서 호출합니다. 아래 코드는 cpp를 사용한 구현 예제입니다. 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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, 바이너리, 프로토콜 버퍼 등 포맷이 다르면, 방문자 패턴으로 포맷별 직렬화를 분리합니다. 다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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 힌트, 검증 규칙)
프로퍼티에 “범위”, “에디터 타입” 등을 메타데이터로 붙입니다. 아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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 코드를 생성합니다. 아래 코드는 cpp를 사용한 구현 예제입니다. 반복문으로 데이터를 처리합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
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)을 호출하지 않습니다.
아래 코드는 cpp를 사용한 구현 예제입니다. 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// 앱 초기화 시 한 번만
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)처럼 반사 연산자로 타입·멤버를 컴파일 타임에 조회하는 문법이 제안되어 있습니다.
다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 가상의 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++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.