본문 바로가기

RTTI와 RAII

@iamrain2025. 11. 20. 19:51

1. RTTI와 RAII

RTTI와 RAII는 C++의 중요한 개념으로, RTTI는 객체의 타입을 식별하는 데 중점을 두고, RAII는 자원의 생명주기를 관리하는 데 중점을 둡니다.

1.1. RTTI (Run-Time Type Information)

  • 목적: 프로그램 실행 중에 다형성을 가진 객체의 실제 동적 타입(dynamic type)이 무엇인지 식별하는 메커니즘입니다.
  • 주요 기능: 기반 클래스 포인터나 참조를 통해 파생 클래스 객체를 가리킬 때, 해당 객체의 원래 타입을 알아낼 수 있습니다.
  • 핵심 연산자: dynamic_cast (안전한 다운캐스팅), typeid (객체의 타입 정보 획득).
  • 핵심 문제: "이 객체의 실제 타입은 무엇인가?"

1.2. RAII (Resource Acquisition Is Initialization)

  • 목적: 자원의 생명주기를 객체의 생명주기에 바인딩하여 자원 관리를 자동화하고, 자원 누수를 방지하는 프로그래밍 기법(idiom)입니다.
  • 주요 기능: 객체가 생성될 때(생성자) 자원을 획득하고, 객체가 소멸될 때(소멸자) 자원을 자동으로 해제합니다. 예외가 발생해도 소멸자는 반드시 호출되므로, 예외 안전성을 보장하는 데 핵심적인 역할을 합니다.
  • 핵심 구현: 클래스의 생성자/소멸자, 스마트 포인터(std::unique_ptr, std::shared_ptr).
  • 핵심 문제: "어떻게 하면 자원을 절대 잊지 않고, 예외가 발생해도 안전하게 해제할 수 있는가?"

2. RTTI와 RAII 상세 비교

두 개념은 문제 영역과 해결 방식이 근본적으로 다릅니다.

구분 RTTI (Run-Time Type Information) RAII (Resource Acquisition Is Initialization)
주된 목적 객체의 타입 식별 (Identification) 자원의 생명주기 관리 (Management)
동작 시점 실행 시간 (Run-time). dynamic_cast 등은 실행 중에 타입을 검사한다. 컴파일 시간 및 실행 시간. 객체의 생명주기는 컴파일 시점에 결정되며(스코프 기반), 소멸자는 실행 중 스코프를 벗어날 때 호출된다.
관련 C++ 기능 dynamic_cast, typeid, 가상 함수(virtual functions), vtable 생성자(Constructor), 소멸자(Destructor), 스마트 포인터(unique_ptr, shared_ptr)
문제 해결 영역 다형성(Polymorphism)을 활용하는 객체지향 설계에서 특정 파생 클래스의 고유 기능에 접근해야 할 때 사용한다. 메모리, 파일 핸들, 소켓, 뮤텍스 등 모든 종류의 한정된 자원을 다룰 때 사용한다.
성능 영향 dynamic_cast는 런타임에 상속 계층을 탐색하므로 약간의 성능 오버헤드가 발생할 수 있다. 일반적으로 제로 코스트 추상화(Zero-cost abstraction)에 가깝다. 컴파일러 최적화를 통해 인라인 처리되는 경우가 많아 성능 저하가 거의 없다.
설계적 관점 남용할 경우, 가상 함수로 풀어야 할 문제를 잘못된 방식으로 접근하고 있다는 설계 문제의 신호일 수 있다. C++에서 자원을 관리하는 가장 표준적이고 권장되는 핵심 패턴이다.

계층 구조에서의 동작

  • RTTI: 사용자 코드(dynamic_cast) -> C++ 런타임(vtable 조회) -> 컴파일러가 생성한 타입 정보. 주로 C++ 런타임 시스템 수준에서 동작합니다.
  • RAII: 사용자 코드(객체 선언) -> C++ 컴파일러/런타임(스코프에 따른 생성자/소멸자 호출) -> 소멸자 코드(자원 해제 API 호출) -> OS(자원 반납). 언어의 핵심 규칙(객체 생명주기)을 활용하여 OS 자원까지 안정적으로 관리합니다.

3. 실습 코드

#include <iostream>
#include <vector>
#include <string>
#include <memory> // 스마트 포인터를 위해 포함

// 게임 세계의 모든 객체를 나타내는 기반 클래스
class GameEntity {
public:
    GameEntity(const std::string& name) : name(name) {}
    virtual ~GameEntity() {
        std::cout << name << "(GameEntity) 소멸자 호출" << std::endl;
    }

    virtual void interact() {
        std::cout << name << "과(와) 상호작용했지만 특별한 일은 없었다." << std::endl;
    }

    std::string getName() const { return name; }

protected:
    std::string name;
};

// 플레이어 클래스
class Player : public GameEntity {
public:
    Player(const std::string& name, int level) : GameEntity(name), level(level) {}
    ~Player() override {
        std::cout << name << "(Player) 소멸자 호출" << std::endl;
    }

    void levelUp() {
        level++;
        std::cout << name << "의 레벨이 " << level << "이 되었습니다!" << std::endl;
    }

private:
    int level;
};

// 보물 상자 클래스
class TreasureChest : public GameEntity {
public:
    TreasureChest(const std::string& name, bool is_locked) : GameEntity(name), is_locked(is_locked) {}
    ~TreasureChest() override {
        std::cout << name << "(TreasureChest) 소멸자 호출" << std::endl;
    }

    void open() {
        if (is_locked) {
            std::cout << name << "은(는) 잠겨있다." << std::endl;
        } else {
            std::cout << name << "을(를) 열어 보물을 획득했다!" << std::endl;
        }
    }

private:
    bool is_locked;
};

// 게임 월드: 엔티티들을 관리
void game_world() {
    std::vector<std::unique_ptr<GameEntity>> entities;

    // RAII: unique_ptr을 사용해 동적 할당된 객체의 생명주기를 관리
    entities.push_back(std::make_unique<Player>("용사", 10));
    entities.push_back(std::make_unique<TreasureChest>("오래된 상자", true));
    entities.push_back(std::make_unique<TreasureChest>("새로운 상자", false));

    std::cout << "\n[ 상호작용 시작 ]\n";
    for (const auto& entity_ptr : entities) {
        // RTTI: dynamic_cast를 사용해 entity_ptr이 실제로 Player를 가리키는지 확인
        if (Player* player = dynamic_cast<Player*>(entity_ptr.get())) {
            std::cout << player->getName() << "의 차례: ";
            player->levelUp();
        } 
        // RTTI: dynamic_cast를 사용해 TreasureChest 타입인지 확인
        else if (TreasureChest* chest = dynamic_cast<TreasureChest*>(entity_ptr.get())) {
            std::cout << chest->getName() << " 발견: ";
            chest->open();
        }
    }
    std::cout << "\n[ game_world 함수 종료, RAII로 인한 자동 소멸 시작 ]\n";
    // 함수가 종료되면 'entities' 벡터가 소멸되고, 벡터 안의 모든 unique_ptr들이 소멸자를 호출한다.
    // 결과적으로 new로 할당했던 모든 객체들이 delete 키워드 없이도 자동으로 해제된다. (RAII)
}

int main() {
    game_world();
    return 0;
}

4. 요약

RTTI와 RAII는 C++에서 완전히 다른 목적을 가진 개념입니다.

 RTTI는 타입에 관한 것으로, "Run-Time Type Information"의 약자입니다. 프로그램 실행 중에 객체의 실제 타입이 무엇인지 확인해야 할 때, 예를 들어 부모 클래스 포인터로 자식 객체를 가리키고 있을 때 dynamic_cast를 써서 "이 객체가 정말 Player 타입이 맞아?"라고 물어보는 데 사용합니다. 즉, 객체의 정체를 파악하는 역할을 합니다.

 RAII는 자원 관리에 관한 프로그래밍 기법으로, "Resource Acquisition Is Initialization"의 약자입니다. 메모리, 파일, 락과 같은 자원을 객체의 생명주기에 묶는 것입니다. 객체가 생성될 때 자원을 할당하고, 객체가 스코프를 벗어나 소멸될 때 자원을 자동으로 해제하도록 만들어, 자원 누수를 원천적으로 방지하고 예외가 발생해도 안전하게 코드를 작성할 수 있게 해줍니다. std::unique_ptrstd::shared_ptr 같은 스마트 포인터가 대표적인 RAII의 예시입니다.

iamrain
@iamrain :: Annals of Unreal

iamrain 님의 블로그 입니다.

공감하셨다면 ❤️ 구독도 환영합니다! 🤗

목차