내일 모의면접 주제는 “RTTI와 RAII에 대해서 설명해 주세요”다. 두 개념을 따로 외우기보다, RAII → 스마트 포인터 → virtual 소멸자 → vtable로 이어지는 꼬리질문 경로를 하나의 다리로 잡고 준비했다. 어제 vtable을 깊게 판 게 있어서, 어떤 꼬리질문이 와도 결국 vtable로 돌아오게 답변을 설계하는 게 목표다.
모의면접 답변
RTTI(Runtime Type Information)는 C++이 런타임에 객체의 실제 타입 정보를 얻을 수 있게 하는 메커니즘입니다. virtual 함수가 하나라도 있는 클래스는 vtable을 가지며, vtable 헤더에 type_info 포인터가 자동 삽입됩니다. typeid(*ptr)는 vptr을 통해 이 type_info에 접근해 실제 타입 이름을 반환하고, dynamic_cast는 RTTI를 이용해 다운캐스트를 시도한 뒤 실패하면 nullptr(포인터) 또는 std::bad_cast 예외(참조)를 반환해 안전하게 처리합니다. static_cast는 런타임 검사 없이 강제 변환하므로 타입이 틀리면 미정의 동작(UB)이 발생합니다.
RAII(Resource Acquisition Is Initialization)는 자원의 수명을 객체의 수명에 묶는 C++ 관용구입니다. 생성자에서 자원을 획득하고 소멸자에서 해제하면, 예외 발생 시 스택 해제(stack unwinding) 과정에서 소멸자가 자동 호출되어 누수 없이 정리됩니다. unique_ptr, shared_ptr, lock_guard, weak_ptr, ifstream이 모두 RAII 도구입니다. 단, 기반 클래스 포인터로 파생 객체를 delete할 때 virtual 소멸자가 없으면 파생 소멸자가 호출되지 않아 자원 누수가 발생하므로, 다형적 기반 클래스에는 반드시 virtual ~Base()를 선언해야 합니다.
핵심 개념
답변에 등장하는 용어를 면접 직전에 훑을 수 있게 목록으로 정리해 둔다.
- RTTI (Runtime Type Information) — 런타임에 객체의 실제 타입을 확인하는 메커니즘.
virtual 함수가 있는 클래스의 vtable 헤더에 type_info* 포인터가 자동 생성됨 - typeid — 타입 정보를 반환하는 연산자.
typeid(*ptr)은 vptr 경유 동적 타입(런타임), typeid(ptr)은 정적 타입(컴파일 타임) 반환 - dynamic_cast — RTTI를 사용해 런타임 안전 다운캐스트. 실패 시 포인터 →
nullptr, 참조 → std::bad_cast 예외 - static_cast — 컴파일 타임 타입 변환. 런타임 검사 없음 — 잘못된 캐스트 시 Undefined Behavior
- type_info — RTTI 데이터를 담는 구조체. vtable[-1] 위치에 포인터로 저장.
name() 메서드로 타입 이름 반환 - -fno-rtti — GCC/Clang에서 RTTI를 비활성화하는 컴파일 플래그.
dynamic_cast/typeid 사용 불가. 성능 중심 환경(게임 엔진 등)에서 활용 - RAII (Resource Acquisition Is Initialization) — 자원 수명을 객체 수명에 묶는 C++ 관용구. 생성자에서 획득, 소멸자에서 해제
- 스택 해제 (Stack Unwinding) — 예외 발생 또는 함수 종료 시 스택 프레임이 역순으로 소멸되면서 소멸자가 자동 호출되는 과정
- 예외 안전성 (Exception Safety) — 예외 발생 경로에서도 자원이 누수 없이 해제되는 속성. RAII로 보장
- unique_ptr — 단독 소유권 스마트 포인터. 복사 불가, move만 허용. 런타임 오버헤드 없음
- shared_ptr — 참조 카운팅 공유 소유권 스마트 포인터. 카운트가 0이 되면 자동
delete - weak_ptr — 소유권 없는 약한 참조.
shared_ptr 순환 참조 문제 해결에 사용 - lock_guard — 뮤텍스를 RAII로 감싼 래퍼. 생성자에서
lock(), 소멸자에서 unlock(). 예외 발생 시 데드락 방지 - ifstream — 파일 핸들을 RAII로 관리하는 파일 스트림. 생성자에서 파일 열기, 소멸자에서 자동
fclose. 예외 발생 시 파일 핸들 누수 방지 - virtual 소멸자 — RAII + 다형성 조합의 필수 조건. 기반 포인터
delete 시 파생 소멸자까지 올바르게 체인 호출됨
목차
- 핵심 요약 카드
- RTTI — Runtime Type Information
- RAII — Resource Acquisition Is Initialization
- 꼬리질문 예상 경로
- 언리얼에서의 RTTI / RAII
1. 핵심 요약 카드
RTTI 30초
1
2
3
4
5
| RTTI = 런타임 타입 정보 시스템
메커니즘: vtable 헤더의 type_info* 포인터
도구: typeid (타입 이름), dynamic_cast (안전 캐스팅)
비용: vtable 탐색 오버헤드 → 성능 중심 환경에서 -fno-rtti
언리얼: Cast<T>(), IsA<T>(), GetClass() (UClass 기반)
|
RAII 30초
1
2
3
4
5
| RAII = 자원 수명 = 객체 수명
생성자에서 획득, 소멸자에서 해제 → 예외 안전성 보장
도구: unique_ptr, shared_ptr, lock_guard, ifstream
언리얼: FScopeLock, TGuardValue, FString/TArray
함정: 기반 클래스 포인터로 delete 시 virtual 소멸자 필수
|
꼬리질문 연결 맵
1
2
3
4
5
6
7
8
9
10
11
12
13
| RTTI
├── typeid → 정적/동적 RTTI 구분
├── dynamic_cast → 실패 시 nullptr vs bad_cast
│ └── static_cast 비교 → UB 위험
└── -fno-rtti → 성능 트레이드오프
RAII
├── unique_ptr → 단독 소유, move semantics
├── shared_ptr → 참조 카운팅, 순환 참조 문제
│ └── weak_ptr → 순환 참조 해결
├── lock_guard → 뮤텍스 + RAII
└── virtual 소멸자 (★꼬리질문 연결!)
└── vtable → 동적 디스패치 (전날 주제 복습)
|
핵심 한 문장
RTTI는 C++이 런타임에 객체의 실제 타입 정보를 얻을 수 있게 하는 메커니즘으로, typeid와 dynamic_cast의 기반 인프라입니다.
동작 원리
virtual 함수가 있는 클래스는 vtable을 가지며, vtable 헤더에 type_info 포인터가 자동 삽입됩니다.
1
2
3
4
5
| vtable 구조:
[ type_info* ] ← RTTI 데이터 (vtable[-1])
[ offset_to_top ] ← 다중 상속용 포인터 조정값 (vtable[-2])
[ Func1* ] ← 가상 함수 [0]
[ Func2* ] ← 가상 함수 [1]
|
typeid — 타입 이름 확인
1
2
3
4
5
6
7
8
9
10
11
12
| #include <typeinfo>
class Animal { public: virtual ~Animal() {} };
class Dog : public Animal {};
Animal* a = new Dog();
// 정적 RTTI (컴파일 타임) — 포인터 타입 기준
std::cout << typeid(a).name(); // "Animal*"
// 동적 RTTI (런타임) — vptr → type_info 접근
std::cout << typeid(*a).name(); // "Dog" ← 실제 타입!
|
dynamic_cast — 안전한 다운캐스트
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| Animal* a = new Dog();
// 성공 케이스
Dog* d = dynamic_cast<Dog*>(a); // Dog 맞음 → 유효 포인터 반환
if (d) { d->Bark(); }
// 실패 케이스
Cat* c = dynamic_cast<Cat*>(a); // Dog이지 Cat이 아님 → nullptr 반환
if (!c) { /* 안전하게 처리 */ }
// 레퍼런스 버전 — 실패 시 std::bad_cast 예외 던짐
try {
Cat& cr = dynamic_cast<Cat&>(*a);
} catch (const std::bad_cast& e) { }
|
dynamic_cast vs static_cast
| | static_cast | dynamic_cast |
|---|
| 검사 시점 | 컴파일 타임 | 런타임 (RTTI 사용) |
| 실패 시 | UB (크래시 가능) | nullptr / bad_cast |
| 속도 | 빠름 | 느림 (vtable 탐색) |
| 조건 | 타입 확신할 때 | 타입 불확실할 때 안전하게 |
| RTTI 필요 | 불필요 | 필요 |
1
2
3
4
5
6
| // static_cast 잘못된 사용 → UB
Animal* a = new Animal();
Dog* d = static_cast<Dog*>(a); // 컴파일은 됨, 런타임 UB!
// dynamic_cast 안전한 사용
Dog* d = dynamic_cast<Dog*>(a); // nullptr 반환 → 안전
|
RTTI 비용과 비활성화
- 비용: type_info 구조체 크기 증가 + vtable 탐색 오버헤드
- 비활성화:
-fno-rtti (GCC/Clang) — 게임 엔진 등 성능 중심 환경 - 비활성화 시 불가:
dynamic_cast, typeid (컴파일 오류)
3. RAII — Resource Acquisition Is Initialization
핵심 한 문장
RAII는 자원의 수명을 객체의 수명에 묶는 C++ 관용구입니다. 생성자에서 자원을 획득하고, 소멸자에서 자원을 해제하여 예외가 발생해도 누수 없이 정리합니다.
문제: 예외 발생 시 자원 누수
1
2
3
4
5
6
| // RAII 미적용 — 위험한 코드
void BadFunction() {
int* data = new int[1000];
DoSomething(); // 예외 발생 시 →
delete[] data; // 이 줄 실행 안 됨! → 메모리 누수
}
|
해결: RAII 패턴
1
2
3
4
5
6
| // RAII 적용 — 스택 객체 소멸자가 자원 해제 보장
void GoodFunction() {
std::unique_ptr<int[]> data = std::make_unique<int[]>(1000);
DoSomething(); // 예외 발생해도 OK
// 함수 종료 또는 예외 발생 → data 소멸자 자동 호출 → delete[] 실행
}
|
RAII 동작 원리
1
2
3
4
5
| 스택 프레임 종료 (정상 / 예외 / return) 시 스택 객체 소멸자 자동 호출
↓
소멸자 안에서 자원 해제 (delete, fclose, unlock, ...)
↓
자원 누수 없음 — 예외 안전(Exception Safety) 보장
|
표준 라이브러리 RAII 도구
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // 1. 메모리 관리
std::unique_ptr<T> p = std::make_unique<T>(); // 단독 소유, 복사 불가
std::shared_ptr<T> p = std::make_shared<T>(); // 참조 카운팅 공유 소유
std::weak_ptr<T> w = shared; // 소유권 없는 약한 참조
// 2. 뮤텍스 잠금
std::mutex m;
{
std::lock_guard<std::mutex> lock(m); // 생성자: lock(), 소멸자: unlock()
// 임계 구역
} // 자동 unlock — 예외 발생해도 unlock 보장
// 3. 파일 핸들
std::ifstream file("data.txt"); // 생성자: fopen, 소멸자: fclose
// file이 스코프 종료 시 자동으로 닫힘
|
lock_guard — 뮤텍스 RAII 래퍼
멀티스레드 환경에서 공유 자원에 접근할 때 mutex로 잠금을 겁니다. 그런데 잠금 후 예외가 발생하면 unlock()이 호출되지 않아 데드락이 발생할 수 있습니다. lock_guard는 이걸 RAII로 해결합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| std::mutex m;
int shared_data = 0;
void Increment() {
// RAII 미적용 — 위험
m.lock();
shared_data++;
if (shared_data < 0) throw std::runtime_error("음수!"); // 예외 발생
m.unlock(); // 예외 발생 시 이 줄 실행 안 됨 → 데드락!
}
void SafeIncrement() {
std::lock_guard<std::mutex> lock(m); // 생성자: m.lock()
shared_data++;
if (shared_data < 0) throw std::runtime_error("음수!");
// 예외 발생해도 lock 소멸자에서 m.unlock() 자동 호출 → 데드락 없음
}
|
- 생성자:
mutex.lock() 호출 - 소멸자:
mutex.unlock() 호출 - 스코프를 벗어나면 (정상 종료 / 예외 / return) 항상 unlock 보장
ifstream — 파일 핸들 RAII 래퍼
파일을 fopen으로 열면 반드시 fclose로 닫아야 합니다. 예외 발생 시 닫지 못하면 파일 디스크립터가 누수됩니다. ifstream은 이걸 RAII로 처리합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // C 스타일 — 위험
FILE* f = fopen("data.txt", "r");
ProcessData(f); // 예외 발생 시
fclose(f); // 이 줄 실행 안 됨 → 파일 핸들 누수
// ifstream RAII — 안전
{
std::ifstream file("data.txt"); // 생성자: 파일 열기
if (!file.is_open()) { /* 에러 처리 */ }
std::string line;
while (std::getline(file, line)) {
ProcessLine(line); // 예외 발생해도 OK
}
} // 소멸자: 파일 자동 닫힘 — fclose 직접 호출 불필요
|
- 생성자: 파일 열기
- 소멸자: 파일 자동 닫기
ofstream(쓰기), fstream(읽기+쓰기)도 동일한 RAII 패턴
RAII 직접 구현
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| template<typename T>
class ScopedArray {
T* ptr;
public:
explicit ScopedArray(size_t n) : ptr(new T[n]) { }
~ScopedArray() { delete[] ptr; } // 소멸자에서 자원 해제
// 복사 금지 (이중 해제 방지)
ScopedArray(const ScopedArray&) = delete;
ScopedArray& operator=(const ScopedArray&) = delete;
T& operator[](size_t i) { return ptr[i]; }
};
void SafeFunction() {
ScopedArray<int> data(1000);
// 예외, return 무관 → data 소멸자가 delete[] 보장
}
|
RAII와 virtual 소멸자의 관계 (꼬리질문 연결!)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| // unique_ptr + 기반 클래스 포인터 사용 시
class Base {
public:
~Base() { } // virtual 아님!
};
class Derived : public Base {
int* data;
public:
Derived() { data = new int[100]; }
~Derived() { delete[] data; }
};
std::unique_ptr<Base> p = std::make_unique<Derived>();
// p 소멸 → Base::~Base() 만 호출 → Derived::~Derived() 미호출 → 누수!
// 해결:
class Base {
public:
virtual ~Base() { } // virtual 소멸자 필수!
};
// 이제: p 소멸 → Derived::~Derived() → Base::~Base() 순서 정상 호출
|
이것이 RAII → 스마트 포인터 → virtual 소멸자 → vtable 꼬리질문 연결 경로입니다.
4. 꼬리질문 예상 경로
메인 질문 답변 후 예상 흐름
1
2
3
4
5
6
7
8
9
10
11
12
| "RTTI와 RAII에 대해서 설명해 주세요"
│
├─ RTTI 설명
│ └─ "dynamic_cast와 static_cast 차이는?"
│ └─ "RTTI 비용은?" → "-fno-rtti 옵션"
│
└─ RAII 설명
└─ "스마트 포인터 종류와 차이는?"
├─ unique_ptr vs shared_ptr vs weak_ptr
└─ "왜 unique_ptr<Base>에서 virtual 소멸자가 필요한가요?"
└─ "virtual 소멸자가 없으면 어떻게 되나요?"
└─ "그럼 vtable이 어떤 역할을 하나요?" ← 전날 주제로 회귀!
|
각 꼬리질문 30초 답변
Q: 스마트 포인터 3종 차이?
1
2
3
| unique_ptr — 단독 소유, 복사 불가, move만 가능. 오버헤드 제로.
shared_ptr — 참조 카운팅 공유 소유. 카운트가 0이 되면 delete.
weak_ptr — 소유권 없는 관찰자. 순환 참조 방지에 사용.
|
Q: shared_ptr 순환 참조란?
1
2
3
4
5
6
7
| struct Node {
std::shared_ptr<Node> next; // 순환 참조 → 카운트 절대 0 안 됨 → 누수
};
// 해결: 한쪽을 weak_ptr로 교체
struct Node {
std::weak_ptr<Node> next; // 소유권 없음 → 순환 아님
};
|
Q: RAII와 예외 안전성의 관계?
스택 해제(stack unwinding) 시 소멸자가 반드시 호출되므로, 자원을 스택 객체(RAII 래퍼)로 관리하면 예외 발생 경로에서도 자원 해제가 보장됩니다.
5. 언리얼에서의 RTTI / RAII
언리얼 RTTI — Cast vs dynamic_cast
언리얼은 자체 리플렉션 시스템(UClass)을 사용하므로 dynamic_cast 대신 Cast<T>를 씁니다.
1
2
3
4
5
6
7
8
9
10
11
12
| AActor* Actor = GetOwner();
// 언리얼 방식 (UObject 리플렉션 사용)
AMyCharacter* Char = Cast<AMyCharacter>(Actor);
if (Char) { /* 성공 */ }
// IsA — 타입 체크만
if (Actor->IsA<AMyCharacter>()) { }
// GetClass — type_info 대신 UClass 반환
UClass* cls = Actor->GetClass();
FString name = cls->GetName(); // "MyCharacter"
|
**Cast vs dynamic_cast**:
| | dynamic_cast | Cast<T> (언리얼) |
|---|
| 기반 | C++ RTTI (vtable) | UObject 리플렉션 (UClass) |
| UObject 아닌 타입 | 사용 가능 | 사용 불가 |
| 성능 | vtable 탐색 | UClass 체인 탐색 |
| 실패 시 | nullptr | nullptr |
언리얼 RAII — FScopeLock, TGuardValue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| // FScopeLock — std::lock_guard 언리얼 버전
FCriticalSection CriticalSection;
{
FScopeLock Lock(&CriticalSection); // 생성자: Lock(), 소멸자: Unlock()
// 임계 구역
} // 자동 Unlock
// TGuardValue — 임시 값 변경 후 자동 복원
bool bFlag = false;
{
TGuardValue<bool> Guard(bFlag, true); // bFlag = true
DoSomething();
} // bFlag = false 자동 복원
// FString, TArray — 소멸자에서 힙 메모리 자동 해제 (RAII 적용)
FString Str = TEXT("Hello"); // 내부적으로 TArray<TCHAR> 사용
// Str 소멸 → TArray 소멸자 → FMemory::Free 호출
|
참고
핵심 요약 — RTTI와 RAII는 결국 vtable에서 만난다. typeid/dynamic_cast는 vtable 헤더의 type_info*를 읽고, RAII를 다형 클래스에 쓰려면 virtual 소멸자(= vtable)가 필수라서, 어떤 꼬리질문이 와도 vtable로 답변을 이어갈 수 있다.