RAII (Resource Acquisition Is Initialization : 자원 획득은 초기화이다)
- C++ 창시자인 비야네 스트로스트룹이 제안한 자원관리를 자동화하는 C++의 디자인 패턴
- 자원의 획득과 해제를 객체(객체의 포인터객체)의 수명과 연결 → 자동으로 효율적으로 자원(메모리, 파일 등 )관리, 메모리 누수를 해결
🧩 C++에서 기본적인 메모리자원 관리방식
획득한 자원은 프로그램이 종료되기전까지 메모리를 차지하며 존재한다. ( 프로그램이 종료시에는 운영체제가 해제 )
- delete작업 : 동적할당 객체의 소멸자 함수 호출 & 객체가 차지하는 메모리 반환
- stack에 할당된 변수(자동변수) : 범위( 함수, 블록 )를 벗어나는 경우 자동으로 소멸
- heap에 할당된 변수(동적 할당변수) : 명시적으로 delete호출해야 객체의 소멸자가 호출됨
- garbage collector : 프로그램 상에서 더 이상 쓰이지 않는 자원을 자동으로 해제 ( JAVA 등의 언어에서 지원, C++에서는 지원X )
#include <iostream>
class A {
int *data;
char c
public:
A(char _c) : c(_c) {
data = new int[100];
std::cout << "자원을 획득함!" << c << std::endl;
}
~A() {
std::cout << "소멸자 호출!" << c << std::endl;
delete[] data;
}
};
void do_something() {
A *pa = new A('A');
A b('B');
}
int main() {
do_something();
// 할당된 객체가 소멸되지 않음!
// 즉, 400 바이트 (4 * 100) 이상의 메모리 누수 발생
}
자원을 획득함! A
자원을 획득함! B
소멸자 호출! B
- stack에 할당된 객체 b → 함수가 종료되면서 자동으로(묵시적으로) 소멸자 호출, stack에서 제거
- heap에 동적할당된 객체 a → 포인터 변수 pa 자체는 함수가 종료되면서 stack에서 제거, 하지만 pa가 가르키는 객체 a는 heap에 여전히 존재한다. ⇒ 메모리 누수발생!!
해결방안 : 함수 내에서 익명객체(pa가 가르키는 A타입 객체)에 대한 소멸자를 명시적으로 호출 → 추후 사용하지 않는 익명객체와 그 내부의 data 배열을 적절히 해제
void do_something() {
A *pa = new A('A');
A b('B');
// b의 소멸자는 자동호출
delete pa;
}
🧩 예외적인 상황에서 메모리 관리의 한계
#include <iostream>
class A {
int *data;
char c
public:
A(char _c) : c(_c) {
data = new int[100];
std::cout << "자원을 획득함!" << c << std::endl;
}
~A() {
std::cout << "소멸자 호출!" << c << std::endl;
delete[] data;
}
};
void thrower(){
//의도적으로 예외발생
throw 1;
}
void do_something() {
A *pa = new A('A');
A b('B');
thrower();
delete pa;
}
int main() {
try{
do_something();
} catch( int i ){
std::cout << "예외발생!" << std::endl;
}
}
자원을 획득함! A
자원을 획득함! B
소멸자 호출! B
예외발생!
예외가 발생시 예외처리로 진행흐름이 넘어가며 소멸자 호출에 도달하지 못하는 경우가 발생
- stack unwinding : 예외가 발생해서 함수를 빠져나가더라도 스택에 정의되어 있는 모든 객체들에는 소멸자가 호출 ( catch로 예외처리가 제대로 진행된 경우 ) ( 기본타입X : 스택 프레임을 벗어나는 과정에서 별다른 작업이 필요없음 )
- heap에 동적할당된 객체 a는 heap에 잔존한다. ⇒ 메모리 누수발생!!
🧩 RAII패턴을 이용한 메모리자원 관리방식
기본 타입과 포인터는 메모리에서 단순히 사라질뿐 소멸자가 호출되지 않음
스마트 포인터(smart pointer) : 스택에 생성되는 포인터타입의 객체, 스코프를 벗어나 소멸 될 때 가리키는 메모리를 해제 & 가르키는 객체의 소멸자 호출
unique_ptr<T> 클래스 : T타입객체의 자원할당 및 관리를 unique_ptr객체(포인터)의 생명주기에 맞김
RAII : 스택의 객체(포인터 객체)를 통한 자원관리
- 자원관리를 객체의 생명주기와 연관 : 생성자에서 자원을 획득 & 소멸자에서 자원을 반환 ⇒ CADRe (Constructor Acquires, Destructor Releases)
- 자원관리를 위한 객체를 지역변수로 선언하여 사용, 객체가 Scope를 벗어나면 소멸자가 자동으로 호출, 자원이 반환 ⇒ SBRM (Scope-Based Resource Management)
#include <iostream>
#include <memory>
class A {
int *data;
char c
public:
A(char _c) : c(_c) {
data = new int[100];
std::cout << "자원을 획득함!" << c << std::endl;
}
void some(){
std::cout << "일반 포인터와 동일하게 사용가능!" << std::endl;
}
~A() {
std::cout << "소멸자 호출!" << c << std::endl;
delete[] data;
}
};
void do_something() {
std::unique_ptr<A> pa(new A('A'));
pa -> some();
}
int main() { do_something(); }
자원을 획득함! A
일반 포인터와 동일하게 사용가능!
소멸자 호출! A
스택에 생성된 포인터 pa는 힙에 생성된 A객체를 가르킴
pa가 scope를 벗어날때 소멸자가 호출 → 소멸자는 A객체에 대한 delete작업이 수행
#include <iostream>
#include <memory>
class A {
int *data;
char c
public:
A(char _c) : c(_c) {
data = new int[100];
std::cout << "자원을 획득함!" << c << std::endl;
}
~A() {
std::cout << "소멸자 호출!" << c << std::endl;
delete[] data;
}
};
void thrower(){
//의도적으로 예외발생
throw 1;
}
void do_something() {
std::unique_ptr<A> pa(new A('A'));
thrower();
}
int main() {
try{
do_something();
} catch( int i ){
std::cout << "예외발생!" << std::endl;
}
}
자원을 획득함! A
소멸자 호출! A
예외발생!
pa는 스택에 존재하는 객체이기에 예외발생시에도 stack unwinding에 의해 자원이 반환됨 = 소멸자가 호출됨 → 자원반환
🧩 참고 : Java의 try-with-resources 를 이용한 메모리 관리
유사하게 자바에서도 예외처리와 함께 자원관리하는 방식을 살펴보자!
파일입출력 처리에서 예외가 자주발생할 수 있고 이로 인해 파일입출력용 객체의 자원관리가 제대로 이루어지지 않을 위험이 존재한다.
- 전통방식 : try문으로 입출력처리를 수행 & 예외발생시 catch또는 throw 한뒤 마지막 finally블럭으로 close할것( 반환 )
- Java7이후의 방식 : finally 블럭없이 자원반환이 자동으로 이루어짐 ⇒ 자원의 자동관리가 컴파일러에 의해, 언어차원에서 제공된다.
[ 참고자료 ]
https://modoocode.com/229
'dev tech' 카테고리의 다른 글
Google Test framework의 기본개념 및 사용법 (0) | 2024.05.20 |
---|---|
Make 및 Makefile의 개념 및 사용 (0) | 2024.05.20 |
WSL(Windows Subsystem for Linux)의 개념과 기본명령 (0) | 2024.05.20 |
형상관리 Git 사용법 (0) | 2024.03.10 |