RAII

2 분 소요

RAII

RAII (;Resource acquisition is initialization)

C++에서 자주 사용되는 RAII는 자원의 안전한 사용을 위해 객체가 선언된 스코프를 벗어나면 자원을 해제해주는 기법입니다.

직역하면 ‘자원 획득은 초기화이다.’라고 표현되는데, 본질적으로는 자원을 획득할 때 초기화가 이루어지면서 객체 형태로 반환이 되어야 하고, 객체가 사라질 때는 소멸자에 의해 획득한 자원을 모두 반납하는 것을 의미합니다.

자원 할당과 해제

C++에서는 다음과 같이 A 클래스를 동적 할당하는 코드가 있다면 delete 키워드를 사용하여 자원을 반납해야 합니다.

#include <iostream>

class A {
private:
    int* num;
public:
    A() : {
        num = new int[100];
        std::cout << "A()\n";
    }
    ~A() {
        delete[] num;
        std::cout << "~A()\n";
    }
    void print() {
        std::cout << "Hello World\n";
    }
};

void function() {
    A* a = new A();
    a->print();
    delete a;
}

int main() {
    function();
    return 0;
}
A()
Hello World
~A()

이처럼 자원을 반납하는 것을 잊지 않고 코드를 작성하면 별다른 문제가 없는 것처럼 보일 수 있지만, 사실 획득한 모든 자원을 정상적으로 반납하는 것은 생각보다 까다롭습니다.

만약 위 코드에서 delete가 수행되기 전에 예외가 발생하면 어떨까요?

#include <iostream>

class A {
private:
    int* num;
public:
    A() : {
        num = new int[100];
        std::cout << "A()\n";
    }
    ~A() {
        delete[] num;
        std::cout << "~A()\n";
    }
    void print() {
        std::cout << "Hello World\n";
    }
};

void function() {
    A* a = new A();
    a->print();

    throw "error";

    delete a;
}

int main() {
    try {
        function();
    }
    catch (const char* msg) {
        std::cout << msg << '\n';
    }
    return 0;
}
A()
Hello World
error

이 경우에는 소멸자가 호출되지 않았습니다. 이처럼 자원을 해제하기 위해 명시적으로 코드를 작성하였지만 의도와는 다르게 생각하지 못했던 부분에서 획득한 자원을 반납하지 않는 경우가 발생할 수 있고, 시스템의 규모가 클수록 자원을 반납하지 못하게 될 가능성이 있는 부분은 늘어날 것입니다.

자바의 경우는 try-catch-finally를 지원하기 때문에 try 구문을 수행 중에 문제가 발생하더라도 finally 영역에서 명시적으로 close() 등을 호출하여 자원을 반납할 수 있겠지만 C++에서 finally 구문은 존재하지 않습니다.

  • Java7부터는 try(...)에서 선언된 객체에 대해 try 구문이 종료될 때 자동적으로 자원을 해제해주는 Try-with-resources를 지원합니다.

std::unique_ptr

지금까지 위에서 보았던 코드에서 A*(raw pointer)는 객체가 아니므로 소멸자가 호출되지 않습니다.

하지만 RAII 개념을 적용하여 클래스 멤버 변수로 포인터를 갖고 소멸자에서 자원을 반납하게 하면, 스코프를 벗어날 때 소멸자가 호출된다는 점을 이용하여 자원을 반납할 수 있습니다.

std::unique_ptrRAII가 적용된 클래스이고, Modern C++에서는 이것을 smart pointer라고 부릅니다.

#include <iostream>

class A {
private:
    int* num;
public:
    A() : num(new int[100]) {
        std::cout << "A()\n";
    }
    ~A() {
        delete[] num;
        std::cout << "~A()\n";
    }
    void print() {
        std::cout << "Hello World\n";
    }
};

void function() {
    std::unique_ptr<A> a = std::make_unique<A>();
    a->print();

    throw "error";
}

int main() {
    try {
        function();
    }
    catch (const char* msg) {
        std::cout << msg << '\n';
    }
    return 0;
}
A()
Hello World
~A()
error

이처럼 RAII를 활용한 코드는 스택에 할당된 변수가 스코프 범위를 벗어날 때 라이프 타임이 끝나는 점을 활용하여 객체의 소멸자(destructor)가 호출되도록 하고, 획득한 자원을 정상적으로 반납할 수 있게 합니다.