C++ 리플렉션 구현 | 타입 정보·메타데이터·자동 직렬화

들어가며: 구조체가 늘어날수록 직렬화 코드가 폭발한다

“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 등 실전 구현 방식을 선택할 수 있습니다.
  • 자주 발생하는 에러와 워크어라운드를 알 수 있습니다.
  • 프로덕션에서 활용할 수 있는 패턴을 익힐 수 있습니다.

리플렉션이 없어서 생기는 다른 문제들

게임 에디터 프로퍼티 패널

엔진에서 Transform, RigidBody, Mesh 등 컴포넌트의 멤버를 에디터 UI에 자동으로 노출하려면, 각 타입의 프로퍼티 이름·타입·범위를 런타임에 조회해야 합니다. 리플렉션이 없으면 컴포넌트마다 registerProperties() 같은 수동 등록 코드를 반복 작성해야 합니다.

네트워크 프로토콜 직렬화

서버-클라이언트 간 패킷 구조체를 JSON/바이너리로 변환할 때, 패킷 타입이 50개 이상이면 수동 직렬화는 유지보수 불가에 가깝습니다. 리플렉션으로 “멤버 순회 → 직렬화”를 공통화하면 새 패킷 추가 시 선언만 하면 됩니다.

ORM/데이터베이스 매핑

User, Article 같은 엔티티를 DB 테이블과 매핑할 때, 컬럼 이름·타입을 런타임에 알아야 쿼리 생성·결과 바인딩이 가능합니다. 리플렉션이 있으면 엔티티 클래스 선언만으로 매핑을 자동화할 수 있습니다.

스크립트 바인딩 (Lua, JavaScript)

C++ 클래스를 스크립트에 노출할 때, 프로퍼티·메서드 목록을 동적으로 조회해야 합니다. 수동 바인딩은 클래스가 늘어날수록 코드가 폭발하며, 리플렉션으로 자동 바인딩을 구현할 수 있습니다.

설정 파일 로드

YAML/JSON 설정을 Config 구조체로 로드할 때, 키 이름과 멤버를 매칭하려면 멤버 이름을 런타임에 알아야 합니다. 리플렉션으로 “키 → 멤버” 매핑을 자동화할 수 있습니다.


리플렉션이란?

기본 개념

리플렉션(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 바인딩, 플러그인코드 생성, 직렬화 템플릿

수동 등록 기반 타입 레지스트리 구현

타입 레지스트리 설계

가장 기본적인 방식은 타입별로 프로퍼티를 수동 등록하며, 문자열 이름으로 접근하는 레지스트리를 만드는 것입니다.

// 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",
        [](const void* obj, void* out) {
            *static_cast<int*>(out) = static_cast<const User*>(obj)->id;
        },
        [](void* obj, const void* value) {
            static_cast<User*>(obj)->id = *static_cast<const int*>(value);
        }
    });
    // name 프로퍼티
    info.properties.push_back({
        "name", "std::string",
        [](const void* obj, void* out) {
            new (out) std::string(static_cast<const User*>(obj)->name);
        },
        [](void* obj, const void* value) {
            static_cast<User*>(obj)->name = *static_cast<const std::string*>(value);
        }
    });
    // email 프로퍼티
    info.properties.push_back({
        "email", "std::string",
        [](const void* obj, void* out) {
            new (out) std::string(static_cast<const User*>(obj)->email);
        },
        [](void* obj, const void* value) {
            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에 복사합니다.

설정 로더·필수 필드 검증 예제

설정 로더 (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",
         [](const GameConfig* c, void* out) { new (out) std::string(c->title); },
         [](GameConfig* c, const void* v) { c->title = *static_cast<const std::string*>(v); });
    add("screenWidth", "int",
         [](const GameConfig* c, void* out) { *static_cast<int*>(out) = c->screenWidth; },
         [](GameConfig* c, const void* v) { c->screenWidth = *static_cast<const int*>(v); });
    add("screenHeight", "int",
         [](const GameConfig* c, void* out) { *static_cast<int*>(out) = c->screenHeight; },
         [](GameConfig* c, const void* v) { c->screenHeight = *static_cast<const int*>(v); });
    add("fullscreen", "bool",
         [](const GameConfig* c, void* out) { *static_cast<bool*>(out) = c->fullscreen; },
         [](GameConfig* c, const void* v) { c->fullscreen = *static_cast<const bool*>(v); });
    info.defaultConstructor = []() { return new GameConfig(); };
    TypeRegistry::instance().registerType("GameConfig", std::move(info));
}

프로퍼티 검증 (필수 필드 체크)

// 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;
}

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;
}

매크로로 등록 코드 반복 줄이기

수동 등록이 반복적이므로, 매크로로 등록 코드를 줄일 수 있습니다.

// 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, \
        [](const void* o, void* out) { \
            *static_cast<decltype(std::declval<Type>().Member)*>(out) = \
                static_cast<const Type*>(o)->Member; \
        }, \
        [](void* o, const void* v) { \
            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 등 비트리비얼 타입은 별도 처리 필요.


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로 타입 소거된 값 전달
  • 플러그인/공유 라이브러리에서도 동작

등록 누락·ODR 위반·상속 멤버 조회 문제

”멤버 추가했는데 직렬화에 반영 안 됨”

원인: 수동 등록 시 새 멤버를 등록 목록에 추가하지 않음.

// ❌ 잘못된 예 — User에 age 추가했지만 등록 누락
struct User {
    int id;
    std::string name;
    std::string email;
    int age;  // 새로 추가 — 등록 깜빡함
};
// age가 JSON에 안 나옴
// ✅ 올바른 예 — age도 등록
info.properties.push_back({
    "age", "int",
    [](const void* obj, void* out) { *static_cast<int*>(out) = static_cast<const User*>(obj)->age; },
    [](void* obj, const void* value) { static_cast<User*>(obj)->age = *static_cast<const int*>(value); }
});

해결법: 구조체 변경 시 등록 코드도 함께 수정하는 체크리스트를 두거나, 단위 테스트로 “등록된 프로퍼티 수 == 구조체 멤버 수”를 검증합니다.

”비트리비얼 타입을 Getter에서 복사할 때 크래시”

원인: int처럼 memcpy로 복사 가능한 타입과 std::string처럼 생성자가 필요한 타입을 구분하지 않고 처리함.

// ❌ 잘못된 예 — std::string을 memcpy로 복사
p.getter = [](const void* obj, void* out) {
    memcpy(out, &static_cast<const User*>(obj)->name, sizeof(std::string));
};
// 미정의 동작: std::string은 placement new 필요
// ✅ 올바른 예 — placement new로 복사
p.getter = [](const void* obj, void* out) {
    new (out) std::string(static_cast<const User*>(obj)->name);
};

”등록 순서가 바뀌어서 잘못된 프로퍼티에 값이 들어감”

원인: 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);

”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();  // 한 곳에서만
    // ...
}

”상속된 클래스의 베이스 멤버가 조회 안 됨”

원인: 파생 클래스만 등록하고 베이스 멤버를 등록하지 않음.

// ❌ 잘못된 예 — 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", ...});
    // ...
}

”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);

private 멤버·템플릿 타입 등록 같은 한계 우회

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",
    [](const void* o, void* out) {
        *static_cast<int*>(out) = static_cast<const SecretData*>(o)->getSecret();
    },
    [](void* o, const void* v) {
        static_cast<SecretData*>(o)->setSecret(*static_cast<const int*>(v));
    }
});

템플릿 타입의 등록

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";
}

플러그인에서 동적 타입 등록

플러그인이 로드될 때 자신의 타입을 레지스트리에 등록하며, 언로드 시 제거해야 합니다. 레지스트리에 등록 해제 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);
    }
}

코드 생성으로 등록 자동화

구조체 정의를 파싱해서 등록 코드를 생성하는 도구(예: custom clang tool, Python 스크립트)를 두면, 수동 등록을 거의 제거할 수 있습니다. 빌드 단계에서 struct_parser.py User.h → user_reflection.generated.cpp를 생성하며, CMake에서 이 파일을 빌드에 포함합니다.


포맷별 직렬화·메타데이터·버전 호환 확장

타입별 직렬화 포맷 분리

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);
        }
    }
}

메타데이터 확장 (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"}}
});

스키마 생성 (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);
}

변경 감지 (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);

버전별 직렬화 (이전 버전 호환)

멤버 이름 변경·삭제 시 이전 포맷과 호환하려면, 메타데이터에 "serializedName" 또는 "sinceVersion"을 두며, 로드 시 버전에 따라 매핑합니다.

// "old_id" → "id" 매핑 (v1 호환)
info.metadata["alias"] = "old_id";  // 이전 필드 이름

레지스트리 조회와 Getter/Setter 오버헤드

레지스트리 조회

타입 이름으로 조회하는 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);
}

C++26 Reflection 전망

정적 리플렉션(P2996)은 2025년 6월 C++26에 채택되었고, 2026년 기준 GCC 16(-std=c++26 -freflection)과 Bloomberg의 clang-p2996 포크에서 쓸 수 있습니다. 이 글에서 만든 런타임 레지스트리와 달리 모든 조회가 컴파일 타임에 끝나므로, 등록 코드 없이 멤버를 순회할 수 있습니다.

#include <meta>
#include <string>
struct User {
    int id;
    std::string name;
};
template <typename T>
void serialize(const T& obj) {
    constexpr auto ctx = std::meta::access_context::unchecked();
    template for (constexpr auto m :
                  std::define_static_array(std::meta::nonstatic_data_members_of(^^T, ctx))) {
        auto name = std::meta::identifier_of(m);   // "id", "name"
        const auto& value = obj.[:m:];              // obj.id, obj.name 로 컴파일됨
        // write(name, value);
    }
}

문자열 이름으로 런타임에 타입을 찾는 기능(에디터·스크립트 바인딩)이 필요하면, 이 리플렉션으로 이 글의 TypeRegistry를 자동으로 채우는 방식이 현실적인 조합입니다. 문법과 주의점은 C++26 리플렉션 기초(#26-1)에 정리했습니다. 현재 대안과 비교하면:

방식장점단점
수동 등록명확, 의존성 없음반복 작업, 누락 위험
매크로반복 감소가독성 저하, 디버깅 어려움
RTTR기능 풍부, 검증됨외부 라이브러리 의존
코드 생성자동화빌드 단계 추가
C++26 Reflection표준, 선언적아직 제안 단계

리플렉션 접근법 요약

항목설명
리플렉션타입·멤버 정보를 런타임/컴파일 타임에 조회
수동 등록Getter/Setter를 레지스트리에 등록
매크로반복 등록 코드 감소
RTTRC++11 기반 오픈소스 라이브러리
에러비트리비얼 타입 복사, ODR, 상속 멤버 누락
워크어라운드friend, 타입 별칭, 코드 생성
프로덕션방문자 패턴, 메타데이터, 스키마 생성

핵심 원칙:

  1. 등록은 cpp에서, 헤더 오염 최소화
  2. 비트리비얼 타입은 placement new로 복사
  3. 이름 기반 매칭, 인덱스 의존 금지
  4. 프로덕션에서는 메타데이터·버전 관리 고려

리플렉션 도입 점검 목록

  • 타입 레지스트리 설계 (Getter/Setter, TypeInfo)
  • 등록은 cpp 파일에서만 (헤더 오염 방지)
  • 비트리비얼 타입(std::string 등) placement new 처리
  • 프로퍼티 이름으로 매칭 (순서 의존 금지)
  • ODR 방지 — 등록 초기화는 단일 진입점
  • 상속 시 베이스 멤버도 등록
  • (선택) RTTR 또는 코드 생성 도구 도입
  • 단위 테스트로 등록 누락 검증

자주 묻는 질문 (FAQ)

Q. 타입 등록 코드가 여러 번 실행되는 문제는 왜 생기고 어떻게 막나요?

A. registerAllTypes() 같은 등록 호출을 헤더에 두면 그 헤더를 include한 모든 번역 단위에서 등록이 실행되어 같은 타입이 중복 등록될 수 있습니다. 등록 코드는 하나의 .cpp 파일에만 두고, 여러 곳에서 호출될 수 있다면 함수 내부의 static 지역 변수나 std::call_once로 한 번만 실행되도록 보장해야 합니다. 정적 초기화 순서에 의존하는 자동 등록 방식을 쓴다면 레지스트리 자체도 함수 안의 static 객체로 만들어 초기화 순서 문제를 피하는 것이 좋습니다.

Q. 구조체에 멤버를 추가했는데 JSON 직렬화 결과에 나오지 않으면 어떻게 하나요?

A. 수동 등록 방식의 리플렉션은 등록 목록에 있는 프로퍼티만 직렬화하므로, 구조체에 age를 추가하고 등록 코드를 고치지 않으면 JSON에 빠집니다. 상속 구조에서 파생 클래스만 등록하고 베이스 멤버를 빠뜨려도 같은 증상이 납니다. 구조체 변경 시 등록 코드도 함께 수정하도록 체크리스트를 두거나, 등록된 프로퍼티 수와 멤버 수를 비교하는 단위 테스트로 막을 수 있습니다. 타입 정보·메타데이터·자동 직렬화를 마스터할 수 있습니다.


같이 보면 좋은 글