RAII
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_ptr
은 RAII가 적용된 클래스이고, 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)가 호출되도록 하고, 획득한 자원을 정상적으로 반납할 수 있게 합니다.