C++ 리플렉션 구현 | 타입 정보·메타데이터·자동 직렬화 [#55-1]

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 구조체로 로드할 때, 키 이름과 멤버를 매칭하려면 멤버 이름을 런타임에 알아야 합니다. 리플렉션으로 "키 → 멤버" 매핑을 자동화할 수 있습니다.


목차

  1. 리플렉션이란?
  2. 핵심 구현: 수동 등록 기반
  3. 완전한 리플렉션 예제
  4. 매크로 기반 반복 제거
  5. RTTR 라이브러리 활용
  6. 자주 발생하는 에러와 해결법
  7. 워크어라운드와 한계 극복
  8. 프로덕션 패턴
  9. 성능 고려사항
  10. 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/Setterstd::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.huser_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, 타입 별칭, 코드 생성
프로덕션 방문자 패턴, 메타데이터, 스키마 생성

핵심 원칙:

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

구현 체크리스트

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

자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. 게임 엔진 에디터, ORM 구현, 자동 직렬화, 스크립팅 바인딩 등에 활용합니다. 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference, RTTR 문서, Reflection TS 제안을 참고하세요.

한 줄 요약: 타입 정보·메타데이터·자동 직렬화를 마스터할 수 있습니다.


관련 글

---
... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3