
프로그래밍 분야에서 리소스란, 사용을 하고난 후에는 시스템에 돌려주어야하는 모든 것을 말합니다. 돌려주지 않는 순간부터 문제가 하나둘씩 생겨날 수 있습니다. C++ 프로그램에서 가장 흔하게 사용되는 리소스로 동적 할당된 메모리를 말할 수 있는데, 이 메모리를 할당하고서 해제하지 않으면 메모리가 누수됩니다.
사실 메모리는 프로그램에서 관리해야되는 많은 리소스 중에 한 가지일 뿐입니다. 리소스에는 File Descriptor(파일 서술자), 뮤텍스(Mutex), GUI에서 사용되는 폰트와 브러시도 리소스의 일부입니다. 또한, 데이터베이스 연결, 네트워크 소켓도 리소스에 해당됩니다. 중요한 것은 이 리소스를 점유해서 사용했으면, 다시 놓아주어야 한다는 것입니다.
C++ 이후에 나온 많은 언어들은 대부분 가비지 컬렉터(Garbage Collector - GC)라 불리는 것들이 기본적으로 내장되어 있습니다. GC는 프로그램에서 더 이상 사용되지 않는 리소스들을 자동으로 해제해주는 역할을 합니다. 하지만, C++의 경우에는 수작업으로 해제해주지 않으면 프로그램이 종료되기 전까지 영원히 남아있게 됩니다. (프로그램이 종료되면 OS에 의해서 해제됩니다.)
아래의 예시를 살펴보겠습니다.
#include <iostream>
class Resource {
int* data;
public:
Resource() {
data = new int[100];
std::cout << "리소스 획득\n";
}
~Resource() {
std::cout << "소멸자 호출\n";
delete[] data;
}
};
void f() {
Resource* pRsc = new Resource();
}
int main(void) {
f();
return 0;
}
위 코드를 컴파일하고 실행시키면, 아래와 같은 출력이 나옵니다.
리소스 회득
즉, 자원만 획득하고, 소멸자가 호출되지 않아서 할당된 메모리 data를 해제하지 못하고 있습니다.
그 이유는 f 함수에서 delete pRsc 를 해주지 않았기 때문이죠.
만약 delete를 f 함수 내에서 해주지 않는다면, 생성된 객체를 가리키던 pRsc는 메모리에서 사라지게 됩니다. 따라서 Heap 메모리 영역 어딘가에서 Resource 클래스의 객체는 남아있지만, 그 주소값을 가지고 있는 포인터가 사라지게 되는 것입니다. Resource 객체는 프로그램이 종료되기 전까지 해제되지 못한 채 Heap에서 자리만 차지하고 있게 됩니다. 위 경우에는 총 400 바이트의 메모리 누수가 발생하게 됩니다.
void f() {
Resource pRsc = new Resource();
// do something...
delete pRsc;
}
그렇다면 위 코드처럼 delete만 빼먹지 않으면 모든 것이 해결될까요?
위의 f 함수는 아주 간단하게 구현되어 있기 때문에 delete 만 추가해주면 될 것 같지만, 프로그램의 크기가 커질수록 객체의 메모리 해제에 실패할 수 있는 경우가 발생해서 놓치기 쉽습니다.
do something 부분 어딘가에서 return문이 들어 있을 수도 있으며, 만약 delete가 어느 루프 안에 있는데, continue 혹은 goto 문에 의해서 갑자스럽게 루프를 빠져나왔을 때가 바로 그런 경우입니다. 또한, 함수가 수행되면서 예외를 던질 수 있다는 점도 고려해야 합니다. 예외가 던져지면 delete 문이 실행되지 않게 되죠.
void thrower() {
throw 1;
}
void f() {
Resource* pRsc = new Resource();
thrower();
delete pRsc;
}
int main(void) {
try {
f();
} catch (int i) {
std::cout << "예외 발생\n";
}
return 0;
}
리소스 획득
예외 발생
이렇게 delete 문을 건너뛰는 경우가 발생합니다.
물론 하나하나 따져 가면서 완벽하고 꼼꼼하게 프로그램을 만들면 이런 종류의 에러는 막을 수 있지만, 오랫동안 유지보수를 진행하다보면 언제든지 문제가 발생할 수 있습니다.
스마트 포인터(Smart Pointer)
그렇다면 이 상황을 어떻게 해결할 수 있을까요?
이렇게 f 함수를 통해 얻어낸 리소스가 항상 해제되도록 만드는 방법은, 자원을 객체에 넣고 그 자원 해제를 소멸자가 맡도록 하며, 그 소멸자는 f 함수에서 떠날 때 호출되도록 만드는 것입니다. 즉, 자원을 객체 안으로 넣음으로써, C++에서 자동으로 호출해주는 소멸자에 의해 해당 자원을 저절로 해제할 수 있습니다.
소프트웨어 개발에 쓰이는 상당수의 자원이 Heap에서 동적으로 할당되고, 하나의 블록 혹은 함수 안에서만 쓰이는 경우가 잦기 때문에 그 블록 혹은 함수로부터 빠져나올 때 해제되는 것이 올바릅니다.
위의 예시에서 포인터 pRsc의 경우에는 객체가 아니기 때문에 소멸자가 호출되지 않습니다. 포인터 대신 pRsc를 일반적인 포인터가 아닌, 포인터 객체로 만들어서 자신이 소멸될 때 소멸자가 호출되어 자원을 해제할 수 있도록 하면 됩니다.
이런 용도로 사용되는 포인터 객체를 스마트 포인터(Smart Pointer)라고 부르며, 이 객체는 가리키고 있는 대상에 대해 소멸자가 자동으로 delete를 불러주도록 설계되어 있습니다.
C++ 11 이전에는 이러한 용도로 auto_ptr 이 존재했지만, 많은 문제들이 있어서 사용을 거의 금지하고 있습니다.
auto_ptr은 C++ 17에서 아예 삭제되었습니다.
C++ 11에서는 auto_ptr를 보완한 unique_ptr과 shared_ptr을 제공하고 있습니다.
RAII
위와 같은 디자인 패턴은 RAII - Resource Acquisition Is Initialization, 즉, 리소스의 획득은 초기화이다라는 용어로 불리고 있습니다. 이는 리소스 관리를 스택에 할당한 객체를 통해 수행한다는 것이며, 리소스 획득하고 나서는 바로 리소스 관리 객체에 넘겨준다는 것을 의미합니다.
* Reference
'IT' 카테고리의 다른 글
| 실행파일과 어플리케이션 (0) | 2024.04.19 |
|---|---|
| 정적 라이브러리(static library)와 공유 라이브러리(shared library) (0) | 2024.04.19 |
| 핌플 이디엄(Pimpl Idiom) 디자인 패턴을 활용한 정적분석 결함 개선 (2) | 2023.11.28 |
| PCM 과 WAV 오디오 파일의 차이 (0) | 2022.06.12 |
| 아날로그(LP)와 디지털(CD)의 음질적 차이 (0) | 2022.06.11 |