개발하는 리프터 꽃게맨입니다.

[디자인 패턴] RAII (Resource Acquisition Is Initialization) 본문

기타 개발 지식

[디자인 패턴] RAII (Resource Acquisition Is Initialization)

파워꽃게맨 2023. 12. 26. 11:24

📕 개요

RAII는 C++ 에서 메모리 누수를 막기위해 탄생한 '디자인 패턴'입니다.

 

Resource Acquisition Is Initialization

줄여서 RAII라고 부릅니다.

 

'자원의 할당은 객체 초기화시, 자원의 반환은 객체 소멸시한다.'

라는 원칙이라고 이해하시면 되겠습니다.

 

자원의 할당은 생성자에서 보장해주고

자원의 반환은 소멸자에서 보장해주는 것이죠.

 

C++에서 '힙(heap)' 메모리를 사용하고자 할 때, 우리는 직접 자원을 할당하고

직접 해제해야 합니다.

	//힙 메모리를 할당
	int* number = new int;

	//number 포인터가 가리키는 힙 메모리 할당 해제
	delete number;

 

그런데 조금 큰 프로그램을 개발하는 과정에서 필히 발생하는 것이 '메모리 누수' 문제죠.

스택과 달리 힙 메모리에 할당한 메모리는 개발자가 직접 해제해야하는 번거로움 때문에

메모리 해제를 까먹고, 메모리 누수 현상이 발생하는 것은 매우 흔한 현상입니다.

 

📘 문제가 될 수 있는 코드

class vector
{
public:
	void Init()
	{
		_elements = new int[100];
	}

	void Destroy()
	{
		delete[] _elements;
	}

	void push_back(int item) { /*...*/ }
	int front() { return _elements[0]; }

private:
	int* _elements;
	int  _size;
};

void func()
{
	vector vec;
	vec.Init();
	vec.push_back(rand() % 100);

	if (vec.front() % 2 == 0)
		return;

	vec.Destroy();
}

해당 코드는 자칫하면 메모리 누수가 일어날 수 있는 코드입니다.

한 번 살펴보겠습니다.

 

1. vector 클래스는 마치 int 동적배열처럼 작동합니다.

2. Init()에서 자원을 할당하고 Destroy()에서 자원을 반환합니다.

3. func() 함수는 랜덤한 int를 밀어넣습니다.

4. 함수가 끝나갈 때 쯤 Destroy를 호출해서 메모리를 해제합니다.

단, 중간에 '특정 조건'에 의해 함수를 강제로 종료합니다.

 

여기서 문제는 뭘까요?

특정 상황에서 vec의 _element 메모리를 제대로 해제하지 않고 바로 함수를 탈출했다는 것 입니다.

굉장히 쉬운 상황을 예시로 들었지만, 이런 상황은 큰 프로그램을 만들다보면 흔하게 발생합니다.

'그냥 제때 해제해주면 되는거 아냐?' 라고 쉽게 생각할 수 있지만, 개발하다보면 99.9% 놓치는 경우가 많죠.

 

📕 해결방법

그러면 안전하게 힙 메모리를 할당하는 방법은 무엇일까요?

바로 그냥 컴퓨터가 자동으로 해제할 수 있도록 설계해주면 된다는 것이죠.

 

바로 '스택 메모리'를 이용하는 방법입니다.

스택 메모리의 가장 큰 특징은 컴퓨터가 알아서 할당/해제를 해준다는 것이죠.

 

class vector
{
public:
	
	// 추가된 부분
	vector() { Init(); }
	~vector() { Destroy(); }
	//

	void Init()
	{
		_elements = new int[100];
	}

	void Destroy()
	{
		delete[] _elements;
	}

	void push_back(int item) { /*...*/ }
	int front() { return _elements[0]; }

private:
	int* _elements;
	int  _size;
};

 

주석 부분을 추가해서 위 문제를 해결할 수 있습니다.

이것이 RAII의 핵심이죠.

생성자에서 리소스의 할당을

소멸자에서 리소스의 해제를 보장하는 것 입니다.

 

void func()
{
	vector vec;
	vec.Init();
	vec.push_back(rand() % 100);

	if (vec.front() % 2 == 0)
		return;

	vec.Destroy();
}

vec 내부에는 동적 할당된 메모리가 존재하지만

vec은 스택에 저장된 지역 변수입니다.

 

그래서 선언이 될 경우 '생성자'로 인해 '보장된 메모리 할당'이 발생할 것이고

블럭이나 함수를 종료할 경우 '보장된 메모리 해제'가 발생하는 것이죠.

 

중간에 return으로 함수를 강제로 종료시킨다 해도, vec은 지역변수이기에

스택 메모리에서 해제가 될 것이고, 소멸자를 호출하면서 힙에 할당된 _elements 의 메모리도 자동으로

해제될 것 입니다.

 

📘 정리

RAII는 '자원의 할당은 객체 초기화시, 자원의 반환은 객체 소멸시한다.'

라는 디자인 원칙이다.

class MyClass
{
public:
	MyClass() {/*자원 할당 보장*/ }
	~MyClass() {/*자원 해제 보장*/}
	
private:

};

 

 

📗 결론 

RAII에 대해서 공부를 하면서 느낀 것인데, 단순히 자원의 할당과 해제를 보장하는 것을 넘어

조금 더 다양하고 안전하게 프로그래밍 할 수 있도록 영감을 주는 디자인 패턴 개념인 것 같습니다.

 

디자인 패턴을 구세대의 유산이라고 말하는 사람도 있지만, 저는 예전 프로그래머들이 개발하면서

느낀 실수와 어려움을 극복하기 위한 시도들을 보면서 배울 점이 참 많다고 생각합니다.

 

후에 싱글톤 패턴, 스테이트 패턴 등 아직까지도 유용한 디자인 패턴들에 대해서도 추가적으로 다뤄보겠습니다.