🚀 들어가며
C++을 공부하다 보면 한 번쯤 이런 상황을 만나게 됩니다.
Base* obj = GetObject();
Derived* derived = dynamic_cast<Derived*>(obj);
if (derived)
{
// Derived 타입일 때만 처리
}
이 코드는 너무 익숙하지만,
면접에서 이런 질문을 받으면 답변하기가 막막합니다.
❓ dynamic_cast는 내부적으로 어떻게 동작할까?
❓ 비용은 어느 정도일까?
❓ 상용 게임 엔진은 왜 dynamic_cast를 사용하지 않고 커스텀 RTTI를 사용할까?
이번 글에서는 C++의 RTTI의 시작점인 C++ dynamic_cast의 개념과 동작 원리, 그리고 한계까지 정리합니다.
🧠 RTTI란 무엇인가?
RTTI는 Run-Time Type Information의 약자입니다.
👉 쉽게 말하면:
“프로그램 실행 중에 객체의 실제 타입을 알아내는 기능” 입니다.
C++에서는 대표적으로 아래 기능들이 RTTI를 사용합니다:
🔍 dynamic_cast 기본 사용
가장 기본적인 사용 예제를 보겠습니다.
#include <iostream>
class Base
{
public:
virtual ~Base() {}
};
class Derived : public Base
{
public:
void Foo() { std::cout << "Derived::Foo\n"; }
};
int main()
{
Base* base = new Derived();
Derived* derived = dynamic_cast<Derived*>(base);
if (derived)
{
derived->Foo();
}
delete base;
}
✔ 핵심 포인트
- 업캐스팅(Base ← Derived): 항상 안전 (RTTI 필요 없음)
- 다운캐스팅(Derived ← Base): RTTI 필요
👉 dynamic_cast는 다운캐스팅 시 타입 검증을 수행합니다.
⚙️ dynamic_cast가 필요한 이유
아래 예제와 같이 그냥 static_cast를 쓰면 안 될까요?
Derived* derived = static_cast<Derived*>(base);
👉 이 코드는 컴파일은 되지만 위험합니다.
만약 실제 객체가 Derived가 아니라면?
👉 정의되지 않은 동작 (Undefined Behavior)이 발생합니다.
반면 dynamic_cast는:
Derived* derived = dynamic_cast<Derived*>(base);
👉 형변환 실패 시:
- 포인터:
nullptr반환 - 참조:
std::bad_cast예외 발생
👉 즉, 안전한 캐스팅이 가능합니다.
🧩 dynamic_cast 내부 동작 (핵심)
dynamic_cast는 단순한 캐스팅이 아닙니다.
👉 내부적으로는:
- 객체의 vtable을 통해
- type_info 구조체에 접근하고
- 현재 타입과 대상 타입을 비교합니다
즉: “실행 중에 타입을 확인하는 과정”이이 반드시 들어갑니다.”
⚠️ dynamic_cast의 비용은 언제 문제가 될까?
dynamic_cast는 안전한 캐스팅을 위해 런타임 검사를 수행합니다.
내부적으로는 객체의 실제 타입 정보를 확인하고,
필요한 경우 상속 계층을 따라 타입 변환이 가능한지 검사합니다.
이러한 런타임 타입 검사는 성능에 영향을 줄 수 있습니다.
물론 dynamic_cast가 “항상 느리기 때문에 쓰면 안 되는 기능”은 아닙니다.
dynamic_cast가 처음 나올 당시에는 어느 정도 맞는 얘기였지만,
컴파일러의 발전으로 일반적인 애플리케이션 코드에서
가끔 타입을 확인하는 정도라면 dynamic_cast의 비용은 크게 문제가 되지 않는 경우가 많습니다.
문제가 되는 경우는 주로 게임 엔진처럼
매 프레임 수백, 수천 개의 객체를 반복 처리해야 하는 경우입니다.
예를 들어 Tick, Update, Collision, Render 준비 단계처럼
프레임마다 반복 호출되는 함수에서 dynamic_cast가 계속 사용된다면
그 비용이 누적될 수 있습니다.
즉, dynamic_cast의 문제는 “한 번 호출했을 때 너무 느리다”가 아니라,
“반복 호출되는 엔진 루프 안에서 사용되면 성능에 문제를 줄 수 있다”는 점에 가깝습니다.
🧪 간단한 성능 감각
아래와 같은 상황을 생각해봅시다:
for (int i = 0; i < 1000; ++i)
{
Derived* derived = dynamic_cast<Derived*>(base);
// Derived 타입 활용 코드..
}
이 코드는 dynamic_cast 자체가 절대적으로 나쁘다는 의미로 보여주는 코드는 아닙니다.
다만 게임 엔진에서는 이런 형태의 반복이 생각보다 쉽게 등장합니다.
예를 들어 수많은 Actor, Component, Object를 매 프레임 순회하면서
“이 객체가 특정 타입인가?”를 계속 확인한다면, 타입 확인 비용은 누적됩니다.
그래서 엔진 코드에서는 dynamic_cast를 무조건 금지한다기보다,
반복적으로 호출되는 핵심 루프에서는 더 예측 가능하고 제어 가능한 타입 시스템을 직접 두는 경우가 많습니다.
🎮 그래서 게임 엔진에서는?
게임 엔진에서는 dynamic_cast 자체가 “나쁜 기능”이라서 피하는 것이 아닙니다.
핵심은 엔진이 타입 시스템을 직접 제어하고 싶어 한다는 점입니다.
대표적으로 언리얼 엔진은 기본 C++ RTTI에 의존하기보다
UClass 기반의 자체 리플렉션 시스템을 사용합니다.
그 이유는 단순히 성능 때문만은 아닙니다.
– 타입 정보를 엔진이 직접 관리하기 위해
– 직렬화, 에디터 노출, GC와 연결하기 위해
– 플랫폼/빌드 설정에 따른 예측 가능성을 높이기 위해
– 런타임 타입 체크를 엔진 구조와 통합하기 위해
– 타입 정보를 저장해두고 필요할 때 객체로 생성하기 위해
즉, 게임 엔진에서 커스텀 RTTI를 구현하는 이유는
dynamic_cast가 무조건 느려서가 아니라,
타입 정보를 엔진의 핵심 시스템으로 확장해야 하기 때문입니다.
💡 핵심 한 줄 정리
dynamic_cast는 안전한 기능이며, 일반적인 코드에서는 충분히 실용적입니다.
다만 게임 엔진처럼 반복 호출이 많은 구조에서는
타입 정보를 더 직접적으로 제어하기 위해 커스텀 RTTI를 설계합니다.
또한, 커스텀 RTTI를 설계해 타입을 직접 관리할 수 있는 구조를 만들면
다양한 기능에서 타입을 활용할 수 있게 됩니다.
그렇다면 이런 질문이 자연스럽게 나옵니다.
❓ dynamic_cast 없이 타입을 확인할 수는 없을까?
다음 글에서는:
👉 커스텀 RTTI를 구현해보기에 앞서 변수의 다운 캐스팅이 왜 위험한지에 대해 포인터와 메모리 관점에서 알아봅니다.
– 업캐스팅 vs 다운 캐스팅
– 다운캐스팅을 잘못했을 때 발생하는 문제점