C++ RAII, 소멸자에서 자원을 묶는 이유

RAII는 자원 획득을 객체 수명에 묶어 소멸자에서 해제하는 방식입니다. 따라서 수동 해제 코드를 모든 경로에 반복하지 않아도 누수, 조기 반환, 예외로 인한 해제 누락을 줄일 수 있습니다.

이 글은 cppreference의 RAII·lock_guard 설명과 C++ Core Guidelines R 절을 2026-08-27 기준으로 정리한 일반 설명이며, 컴파일러와 예외 설정에 따라 동작은 달라질 수 있습니다.

RAII는 무엇을 해결하나?

한 줄 답: 사용 전에 확보해야 하는 자원의 수명을 객체 수명에 연결해, 스코프가 끝날 때 획득의 역순으로 해제하도록 합니다.

Resource Acquisition Is Initialization은 사용 전에 확보해야 하는 자원의 생명 주기를 객체의 생명 주기에 연결하는 기법입니다. 여기서 자원은 힙 메모리, 실행 스레드, 열린 소켓과 파일, 잠긴 뮤텍스, 디스크 공간, 데이터베이스 연결처럼 수량이 제한된 대상을 포함합니다. 자원 가용성을 클래스 불변식으로 표현하면 사용 전에 별도 검사를 반복하는 부담을 줄일 수 있습니다.

생성자는 자원을 획득해 클래스 불변식을 성립시키거나 획득에 실패하면 예외를 던지며, 소멸자는 자원을 해제하고 예외를 던지지 않는 역할을 맡습니다. open()close(), lock()unlock(), init()destroy()처럼 호출자가 여러 경로에서 해제를 직접 맞추는 형태와 구분되는 이유가 여기에 있습니다.

cppreference의 bad()good() 예시는 잠금 해제를 스코프에 연결하는 차이를 보여 줍니다. bad()에서는 f()가 예외를 던지거나 everything_ok()가 거짓일 때 m.unlock()에 도달하지 못할 수 있습니다. good()에서는 이름 있는 lk가 블록 안에서 생성되고 블록을 벗어날 때 소멸하므로 두 경로에서도 뮤텍스 해제가 이어집니다.

std::mutex m;

void bad()
{
    m.lock();
    f();
    if (!everything_ok())
        return;
    m.unlock();
}

void good()
{
    std::lock_guard<std::mutex> lk(m);
    f();
    if (!everything_ok())
        return;
}

lock_guard와 unique_ptr는 같은 패턴인가?

한 줄 답: 둘은 관리 대상은 다르지만, 생성과 수명 종료에 맞춰 자원의 획득과 해제를 짝짓는 같은 구조입니다.

std::lock_guard<Mutex><mutex>에 정의된 비복사 가능 뮤텍스 래퍼입니다. 객체를 생성하면 전달받은 뮤텍스의 소유를 시도하고, 객체가 만들어진 스코프를 벗어나 소멸할 때 소유한 뮤텍스를 해제합니다. 기본 생성자는 사실상 m.lock()을 호출하며, 그 호출에서 발생한 예외를 전파합니다.

adopt_lock 생성자는 이미 소유한 뮤텍스의 소유권을 취하는 형태이므로 잠금을 시도하지 않습니다. 현재 스레드가 해당 뮤텍스를 이미 비공유 잠금 상태로 보유하지 않았다면 정의되지 않은 동작이 되며, 전달한 뮤텍스가 래퍼보다 먼저 파괴되어도 정의되지 않은 동작이 됩니다.

std::unique_ptr<memory>에 정의된 스마트 포인터로, 포인터가 가리키는 객체를 소유하고 관리합니다. 관리 객체가 파괴되거나 다른 포인터를 대입하거나 reset()을 호출하면 관리 대상 객체를 폐기합니다. 이동 생성과 이동 대입은 가능하지만 복사는 불가능하며, 동적 수명을 다루는 코드에서 정상 종료와 예외 종료 모두 삭제를 보장하는 메모리 관리 사례로 사용됩니다.

두 클래스의 공통점은 자원 종류가 아니라 해제 책임의 위치에 있습니다. 하나는 뮤텍스의 잠금과 해제를 맡고 다른 하나는 동적 객체의 소유와 폐기를 맡지만, 둘 다 이름 있는 객체의 수명을 기준으로 정리 시점을 결정합니다.

lock_guard가 보호할 뮤텍스를 고르는 기준은 뮤텍스와 세마포어, 면접에서 고르는 기준에서 별도로 확인할 수 있습니다.

예외가 나도 해제가 되는 이유는?

한 줄 답: 자동 저장 기간 객체는 예외로 스택을 풀 때 생성의 역순으로 파괴되므로, 소멸자에 둔 해제 작업도 실행됩니다.

자동 저장 기간 객체는 정상적인 블록 종료나 return에서 수명을 마치며, 예외가 발생하면 스택 언와인딩 과정에서 파괴됩니다. 파괴 순서는 생성의 역순이므로 먼저 얻은 자원보다 나중에 얻은 자원이 먼저 해제됩니다. 이 흐름이 누수 방지와 예외 안전성의 기반이 됩니다.

앞의 good()에서 f()가 예외를 던지면 스택 언와인딩이 이름 있는 lk의 소멸자를 호출해 뮤텍스를 해제합니다. 함수가 return으로 끝나는 경우에도 블록을 벗어나는 시점에 같은 소멸자가 호출되므로 수동 unlock()을 각 경로에 배치할 필요가 없습니다.

생성자가 자원 획득에 실패해 예외로 끝나면, 완전히 생성된 멤버와 기반 하위 객체가 초기화의 역순으로 파괴됩니다. C++ Core Guidelines R.1은 fopen()fclose(), lock()unlock(), newdelete처럼 획득과 해제가 짝을 이루는 작업을 자원 핸들로 감싸라고 설명합니다. 소멸자는 자원을 해제하며 예외를 던지지 않도록 해야 합니다.

임베디드에서 쓸 때 주의점은?

한 줄 답: 이 패턴은 힙 할당의 동의어가 아니므로 스코프 객체를 우선하되, 동적 메모리·예외·인터럽트 환경의 제약은 대상 환경에서 따로 확인해야 합니다.

scoped object는 지역 객체, 전역 객체 또는 멤버처럼 정해진 범위에 수명이 묶인 객체를 뜻합니다. C++ Core Guidelines R.5는 불필요한 힙 할당을 피하고 이런 객체를 우선하라고 설명합니다. 따라서 자원 정리를 위해 반드시 newdelete가 필요하다는 뜻은 아닙니다. R.11도 애플리케이션 코드에서 명시적으로 newdelete를 호출하는 일을 피하도록 안내합니다.

하드 실시간 프로그래밍에서는 자유 저장소를 자유롭게 사용하지 못할 수 있어 라이브러리 선택이 제한될 수 있습니다. 그러나 이것은 소멸자에 해제를 맡기는 클래스까지 배제하라는 뜻은 아니며, 스택 프레임의 수명에 맞출 수 있는 파일·잠금 래퍼는 대상 환경의 규칙 안에서 검토할 수 있습니다.

이 방식은 스택 메모리 자체, CPU 시간, 코어 가용성, 캐시 용량, 엔트로피 풀 용량, 네트워크 대역폭, 전력 소비를 객체 수명으로 보장하지 않습니다. 예외를 사용하지 않는 구성에서는 throw에 따른 스택 언와인딩은 일어나지 않지만 정상적인 return과 블록 종료에서는 소멸자가 실행됩니다. 따라서 예외 설정과 동적 메모리 정책 및 인터럽트 맥락의 사용 가능성은 대상 환경에서 확인해야 합니다.

FAQ

한 줄 답: 자원의 획득·해제 짝, 객체 수명, 예외 및 힙 제약을 나누어 확인하면 적용 범위를 판단할 수 있습니다.

핵심은 자원 종류보다 획득과 해제의 짝이 객체 수명 안에서 관리되는지 확인하는 데 있습니다. 아래 질문은 잠금, 동적 객체, 예외 설정, 임베디드 제약을 구분해 살펴보는 기준을 정리합니다.

질문답변
관리할 수 있는 자원에는 무엇이 있습니까?메모리, 파일 핸들, 소켓, 잠금처럼 사용 전에 획득하고 이후 해제하는 자원을 클래스에 묶습니다.
lock_guard는 왜 이름 있는 변수여야 합니까?이름 없는 임시는 곧바로 파괴되어 블록 전체에 걸친 잠금을 유지하지 못합니다. 따라서 스코프 동안 살아 있어야 하는 객체에 이름을 부여해야 합니다.
unique_ptr는 언제 관리 대상을 폐기합니까?관리 객체가 파괴되거나 다른 포인터를 대입하거나 reset()을 호출할 때 관리 대상 객체를 폐기합니다.
예외를 사용하지 않는 구성에서도 의미가 있습니까?정상적인 return과 블록 종료에서는 소멸자 기반 해제가 작동합니다. 다만 throw에 따른 스택 언와인딩은 없으므로 구성별 흐름을 확인해야 합니다.
임베디드에서는 사용하면 안 됩니까?아닙니다. 스코프 객체를 우선하는 원칙은 적용할 수 있지만, 힙·예외·실시간 및 인터럽트 관련 제약은 대상 환경별로 확인해야 합니다.

출처

한 줄 답: 본문 사실과 코드는 cppreference 및 C++ Core Guidelines의 공식 페이지에서 확인할 수 있습니다.

아래 문서는 자원 수명, 뮤텍스 래퍼, 소유 포인터, 자원 관리 규칙을 확인하기 위한 공식 출처입니다. 생성자와 소멸자 동작을 나누어 확인할 수 있도록 관련 페이지를 함께 제시합니다.