[C++] 리플렉션(Reflection) 구현하기 #1 - 리플렉션이 필요한 이유?

연하·4일 전
0

C++

목록 보기
1/2
post-thumbnail

리플렉션이란, 프로그램이 런타임 혹은 컴파일타임에 자기 자신의 구조(타입, 멤버, 함수 등)에 대한 정보를 얻고, 경우에 따라 이를 조작할 수도 있는 능력을 의미합니다.

나는 정말 리플렉션이 와닿지 않았다. 엔진을 개발하면서도, 언리얼을 공부하면서도 한번씩 찾아봤지만, "와 신기하고 무슨소린지 모르겠다 어렵다!" 하고 넘어갔었다. 그땐 이걸 구현하게 될 줄 꿈에도 몰랐지. 정리된 글을 아무리 읽어봐도 모르겠고, 이게 왜 중요한건지도 모르겠고... 아무튼 언젠가 누군가에게 도움이 되길 바라며, 내가 리플렉션을 이해하고 구현해나가는 과정을 기록해보려 한다.

그래서 리플렉션이 왜 필요한걸까

엔진이 개발자가 작성한 C++ 클래스와 변수, 함수를 스스로 파악하고 활용하는게 도대체 왜 중요한걸까? 우리는 유니티와 언리얼같은 게임 엔진에서, 에디터를 통해 캐릭터 클래스가 가지고 있는 속성(체력, 이동속도)을 한 눈에 확인하고, 숫자를 바꾸거나 체크박스를 켜고 끄는 식으로 손쉽게 수정할 수 있다.

만약 리플렉션이 없다면?

class Character
{
public:
	int HP;
    float Speed;
    bool bCanJump;
};

우리가 만든 캐릭터 클래스가 있고, 이걸 에디터에서 슬라이더나 체크박스 형태로 표시하고 싶다고 가정해보자.

CreateLabel("HP");
hpSlider = CreateSlider(0, 100);

CreateLabel("Speed");
speedSlider = CreateSlider(0.f, 10.f);

CreateLabel("Can Jump");
canJumpCheckBox = CreateCheckBox();

먼저 UI를 생성해보자. CreateLabel 이라는 가상의 함수를 통해 이름이 HP인 라벨(텍스트)를 UI상에 생성하고, HP를 바꾸는 슬라이더를 만들어야 한다.

hpSlider->setValue(character->HP);
speedSlider->setValue(character->Speed);
canJumpCheckBox->setChecked(character->bCanJump);

그리고 인스펙터에서 캐릭터 오브젝트를 열 때마다, 캐릭터의 HP, Speed, bCanJump의 값을 읽어서 위에서 만든 슬라이더/체크박스에 세팅해주어야 한다.

character->HP = hpSlider->getValue();
character->Speed = speedSlider->getValue();
character->bCanJump = canJumpCheckbox->isChecked();

마지막으로 사용자가 슬라이더나 체크박스를 바꿨을 때 캐릭터의 HP, Speed, bCanJump 값을 업데이트하는 코드를 작성해주자.

자, 만약 캐릭터 클래스에 새로운 변수가 생긴다면..? 우리는 UI를 생성하고 동기화하는 코드를 추가해야 한다. 변수 이름, 타입, 표시 방식에 대한 정보를 에디터와 캐릭터 클래스 사이에서 사람이 매번 수작업으로 맞춰줘야 하게 되므로, 클래스가 많아질수록 매우 번거로워진다.

리플렉션이 있다면?

UCLASS()
class AMyCharacter : public AActor
{
    GENERATED_BODY()

public:
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Stats")
    int HP;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Stats")
    float Speed;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Stats")
    bool bCanJump;
};

언리얼 엔진을 예로 들어보자. 언리얼 엔진은 변수마다 UPROPERTY(EditAnywhere) 같은 매크로를 붙인다. 매크로는 리플렉션을 위한 힌트(메타데이터)이다.

"HP라는 변수는 인스펙터에서 편집 가능(EditAnywhere)하고, 카테고리는 Stats다"

이렇게 사용자가 제시한 정보를 엔진이 자동으로 읽어들이고, 언리얼 에디터에서 캐릭터 클래스를 선택했을 때 디테일 패널에 HP, Speed, bCanJump가 스스로 생기고, 적절히 숫자 입력 칸이나 체크박스로 표시되게 된다.

별도의 코드 추가 없이, 매크로만 달아주면 에디터에 자동으로 변수들이 추가되는 것이다..! 이제 새 변수를 추가해도, 에디터가 자동으로 UI를 만들어준다.

엔진이 클래스에 있는 변수 목록, 타입, 메타데이터 등을 자동으로 파악해서, 에디터에 알맞게 UI를 만들어주고 동기화까지 처리하게 되는 것!

좋은점이 또 뭐가 있을까?

언리얼 엔진의 블루프린트에서, C++로 만든 함수를 블루프린트에서 호출해야 하는 상황

엔진이 함수 이름, 파라미터, 리턴 값을 전혀 모른다면?
=> 블루프린트에 그 함수를 노드로 나타낼 수 없게 된다. 결국, 매번 C++로 된 함수를 "이건 이렇게 호출하는 거다"라고 연결하는 코드를 수작업으로 작성해야 한다.

리플렉션을 사용한다면, 언리얼이 클래스와 함수 정보를 자동으로 읽어내어 "이 함수는 입력으로 문자열이 하나 들어가고, 리턴으로 bool을 준다" 와 같은 정보를 얻을 수 있게 된다. 이러한 정보를 활용해 함수 노드를 자동으로 생성하고 시각화해준다.

멀티플레이 게임에서 플레이어의 HP를 서버와 클라이언트 간에 동기화해야 하는 상황

엔진이 HP 변수가 뭔지 모른다면?
=> 위에서 봤던 UI의 사례처럼, 일일이 코드로 HP 동기화 로직을 작성해야 한다.

리플렉션을 사용한다면, 엔진은 "이 클래스는 HP라는 변수를 가지고 있고, 네트워크 복제(Replicate) 옵션이 설정되어 있구나" 라고 자동으로 파악한다.

게임을 저장 및 로드하는 상황

마찬가지로, 어느 멤버 변수를 저장해야 하는지 리플렉션으로 확인해서 자동으로 직렬화(파일이나 네트워크로 전송하기 적합한 데이터 형태로 변환하는 과정)해줄 수 있게 된다.

결론

결국, 리플렉션이 없으면 C++ 코드와 에디터/블루프린트/네트워크/세이브로드 등을 사람이 일일이 잇는 작업이 필요해지고, 엄청난 반복 코드를 작성해야 하며, 실수도 잦아질 것이다.


아직 C++은 자바나 C#처럼 언어 자체에서 리플렉션을 제공하지 않는다. 그래서 우회적인 기법을 사용해 자체적으로 구현해주어야 한다. 나는 언리얼 엔진에서도 사용하고 있고, 비교적 구현이 간단한 편인 매크로 기반 리플렉션을 구현해보기로 했다.

0개의 댓글