RAII

RAII (Resource Acquisition Is Initialization)

객체가 메모리를 할당받거나, 스레드가 시작되거나, 파일이 열리고, 뮤텍스가 잠기면 — 그 자원은 반드시 나중에 해제돼야 해요. 그런데 코드에서 unlock()을 빠뜨리면 자원이 새고, 예외가 터지면 해제 코드 자체가 실행조차 안 될 수도 있어요. RAII는 "자원의 수명을 객체의 수명에 묶어서" 이런 문제를 원천적으로 해결하는 C++ 기법이에요.

출처: cppreference

본문

RAII(Resource Acquisition Is Initialization)는 C++ 프로그래밍 기법으로 [1] [2], 사용하기 전에 반드시 획득해야 하는 자원(할당된 힙 메모리, 실행 스레드, 열린 소켓, 열린 파일, 잠긴 뮤텍스, 디스크 공간, 데이터베이스 연결 — 제한된 공급으로 존재하는 모든 것)의 생명 주기를 객체의 수명에 묶어요.

RAII는 그 자원이 객체에 접근할 수 있는 어떤 함수에서든 사용 가능함을 보장해요 (자원 가용성은 클래스 불변식(class invariant)이라서 불필요한 런타임 검사를 없애줘요). 또한 모든 자원이 제어 객체의 수명이 끝날 때 획득한 역순으로 해제된다는 것도 보장해요. 마찬가지로 자원 획득이 실패하면(생성자가 예외로 종료하면), 완전히 구성된 모든 멤버와 기반 하위 객체가 획득한 자원은 초기화 역순으로 해제돼요. 이 기법은 핵심 언어 기능(객체 수명, 스코프 종료, 초기화 순서, 스택 풀기)을 활용해 자원 누수를 없애고 예외 안전성을 보장해요. 이 기법의 또 다른 이름은 SBRM(Scope-Bound Resource Management)이에요. RAII 객체의 수명이 스코프 종료 때문에 끝나는 기본 사용 사례에서 따온 이름이죠.

RAII는 다음과 같이 요약할 수 있어요.

  • 각 자원을 클래스로 캡슐화하는데,

    • 생성자는 자원을 획득하고 모든 클래스 불변식을 세우거나, 그것이 불가능하면 예외를 던져요.
    • 소멸자는 자원을 해제하고 절대 예외를 던지지 않아요.
  • 항상 RAII 클래스의 인스턴스를 통해 자원을 사용하는데, 그 인스턴스는

    • 자동 저장 기간(automatic storage duration)을 갖거나 임시 수명 자체를 갖거나,
    • 자동 또는 임시 객체의 수명에 의해 수명이 묶여 있어요.
  • 이동 의미론(move semantics)은 객체 사이, 컨테이너 안팎, 스레드 간에 자원과 소유권을 옮기면서도 자원 안전성을 보장해요. (C++11부터)

open()/close(), lock()/unlock(), init()/copyFrom()/destroy() 멤버 함수를 가진 클래스는 전형적인 비-RAII 클래스의 예예요.

std::mutex m;

void bad()
{
    m.lock();             // acquire the mutex
    f();                  // if f() throws an exception, the mutex is never released
    if (!everything_ok())
        return;           // early return, the mutex is never released
    m.unlock();           // if bad() reaches this statement, the mutex is released
}

void good()
{
    std::lock_guard<std::mutex> lk(m); // RAII class: mutex acquisition is initialization
    f();                               // if f() throws an exception, the mutex is released
    if (!everything_ok())
        return;                        // early return, the mutex is released
}                                      // if good() returns normally, the mutex is released

bad()에서는 unlock()을 빠뜨리는 경로가 있어요. f()가 예외를 던지거나 중간에 return을 하면 뮤텍스는 영영 잠긴 채로 남죠. good()std::lock_guard라는 RAII 클래스 덕분에, 함수가 어떻게 나가든(정상 반환이든 예외든) 소멸자가 반드시 뮤텍스를 해제해요.

표준 라이브러리 (The standard library)

자기 자원을 관리하는 C++ 라이브러리 클래스는 RAII를 따라요. std::string, std::vector, std::jthread(C++20부터)를 비롯한 많은 클래스가 생성자에서 자원을 획득하고(오류 시 예외를 던져요), 소멸자에서 해제하며(절대 던지지 않아요), 명시적 정리를 요구하지 않아요.

  • 게다가 표준 라이브러리는 사용자 제공 자원을 관리하는 RAII 래퍼도 여럿 제공해요. std::unique_ptrstd::shared_ptrstd::make_uniquestd::make_shared를 통해 동적으로 할당된 메모리를 관리하고, std::lock_guard, std::unique_lock, std::shared_lock은 뮤텍스를 관리해요. (C++11부터)

참고 (Notes)

RAII는 사용하기 전에 획득하지 않는 자원의 관리에는 적용되지 않아요. CPU 시간, 코어 가용성, 캐시 용량, 엔트로피 풀 용량, 네트워크 대역폭, 전력 소비, 스택 메모리가 그렇죠. 그런 자원에 대해서는 C++ 클래스 생성자가 객체 수명 동안 자원 가용성을 보장할 수 없으므로, 다른 수단의 자원 관리를 사용해야 해요.

  1. RAII in Stroustrup's C++ FAQ
  2. C++ Core Guidelines E.6 "Use RAII to prevent leaks"

더 알아보기