모바일 환경에서 프로그램을 작성하면서 가장 신경 쓰이는 문제가 바로 메모리 관리입니다.
언제 할당하고, 언제 해제해야 할 지를 깊이 생각해서 프로그램을 작성하지 않으면
메모리 누수가 생기거나, 잘못된 주소를 참조했다고 에러가 발생하죠.
게다가 모바일 환경에서는 메모리 할당 실패가 발생할 확률이 매우 높습니다.
(PC환경에서는 거의 발생하지 않기 때문에 PC에서 잘 수행되는 프로그램이 모바일 환경에서
죽는 경우가 종종 발생합니다.)
boost 라이브러리는 프로그래머가 메모리 관리를 편하게 할 수 있도록 여러가지 pointer wrapper
클래스들을 제공합니다. 그 중에서 특히 shared_ptr<T>이 유용하기 때문에 이놈을 여기에서
설명합니다.
boost::shared_ptr<T> 클래스는 일반 C++ 포인터를 사용하는 것과 동일하게 사용합니다.
예를 들면,
#include <boost/smart_ptr.hpp>
boost::shared_ptr<Object> p(new Object());
p->methodA(); // methodA는 Object의 메소드.
(*p).methodA(); // 위 코드와 동일.
위 예제를 보면, Object 타입의 객체가 하나 생성되고, shared_ptr 오브젝트인 p가 그 오브젝트를
가리킵니다. 사용법은 일반 포인터와 같다는 것을 알 수 있습니다.
boost::shared_ptr<T>는 Reference Counting 메커니즘을 이용해서 메모리 객체를 관리합니다.
즉, 메모리 객체 하나에 대해서 몇 개의 shared_ptr들이 그 객체를 가리키는지 관리하는 겁니다.
shared_ptr 내부 구현을 살펴보면, 객체를 가리키는 포인터 변수와 그 포인터 변수를 감싸는
내부 데이터 구조를 볼 수 있습니다. 이 내부 데이터 구조에는 use_count 변수가 있어서
몇 개의 shared_ptr이 특정 메모리 객체를 가리키는지 관리합니다.
예를 들어보죠.
#include <boost/smart_ptr.hpp>
{
boost::shared_ptr<Object> p(new Object());
boost::shared_ptr<Object> q;
boost::shared_ptr<Object> r;
// 1. 최초 상태에서 p는 Object 객체를 가리키며, use_count = 1 입니다.
// q, r은 null 객체를 가리키며 use_count = 0 입니다.
q = p;
// p가 가리키는 Object를 q도 가리키게 됩니다. use_count = 2 입니다.
// r은 여전히 null을 가리키고, use_count = 0 입니다.
r = q;
// 이제 r도 p, q와 같은 오브젝트를 가리키게 됩니다. use_count = 3입니다.
// 이제 하나하나 지워봅시다.
r.reset();
// 이제 r은 더 이상 p, q와 같은 오브젝트를 가리키지 않습니다. use_count = 2로 감소합니다.
// 하지만 p, q가 여전히 오브젝트를 가리키고 있기 때문에 오브젝트가 지워지지는 않습니다.
q.reset();
// q도 더 이상 메모리 객체를 가리키지 않습니다. use_count = 1이 됩니다.
r.reset();
// 메모리 객체에 대한 참조가 모두 사라지면서, use_count = 0 이 됩니다.
// 이 시점에서 실제 메모리 객체가 삭제됩니다.
}
물론 위 예제와 같이 reset() 코드를 불러줄 필요는 없습니다. (사실 이게 중요한 점이죠)
왜냐하면 shared_ptr들이 삭제될 때, 파괴자가 불리게 되는데 이 때 Reference Count가 감소하게
됩니다. 위 예제에서는 마지막 "}"를 벗어나는 순간 p, q, r이 모두 삭제되고, 생성된 메모리 객체에
대한 참조가 모두 사라지기 때문에 자동으로 메모리 객체가 삭제됩니다. 즉, 프로그래머가 메모리
관리에 대해 신경을 좀 덜 써도 된다는 뜻이죠.
물론 boost::shared_ptr<T> 클래스가 모든 문제를 해결해주는 것은 아닙니다.
Reference Counting 방식은 Object간 순환 참조가 발생하면 무용지물이 됩니다.
(순환 참조를 없애기 위해서는 weak_ptr을 사용하거나, 코드에 Trace Garbage Collector를
적용해야 합니다. 이 부분은 설명하기에 너무 길기 때문에 나중에 따로 설명하도록 하죠.)
여튼 순환 참조시 문제가 왜 발생하는지 봅시다.
다음과 같이 ObjectA -> ObjectB -> ObjectA 순서로 참조가 일어난다고
가정하고 코드를 작성해 봅시다. 내부 참조는 모두 shared_ptr을 이용합니다.
#include <boost/smart_ptr.hpp>
{
boost::shared_ptr<Object> member;
};
boost::shared_ptr<Object> pObjectA(new Object()); // A 메모리 객체에 대한 참조
boost::shared_ptr<Object> pObjectB(new Object()); // B 메모리 객체에 대한 참조
// 1. 이 시점에서 pObjectA, pObjectB는 각각 use_count = 1입니다.
//
pObjectA->member = pObjectB;
// 2. 이 시점에서 pObjectB의 use_count = 2가 됩니다. (B가 2번 참조됨)
pObjectB->member = pObjectA;
// 3. 이 시점에서 pObjectA의 use_count = 2가 됩니다. (A가 2번 참조됨)
}
이제 코드 수행이 끝나고, 종료되는 과정을 생각해봅시다.
마지막 "}"를 만나면 pObjectA, pObjectB 객체는 삭제됩니다 (로컬 변수이기 때문이죠.)
pObjectA가 먼저 삭제된다고 해봅시다. 이 때, use_count는 2에서 1로 감소합니다. 하지만
여전히 use_count = 1이기 때문에 할당된 메모리 객체(A)는 삭제되지 않습니다.
그 다음 pObjectB가 삭제됩니다. 이 경우에도 use_count가 2에서 1로 감소하기 때문에
메모리 객체 (B)는 삭제되지 않습니다. 메모리 객체 A가 삭제되려면, 객체 B가 먼저
삭제되어야 합니다. 그런데 B가 삭제되려면 A가 먼저 삭제되어야 합니다.
닭이 먼저냐 달걀이 먼저냐의 문제인 거죠.
결과적으로, A, B 객체는 메모리 누수로 남게 됩니다.
간단히 다이어그램을 그려보면,
종료 직전: pObjectA -> A <--> B <- pObjectB
종료 직후: A <--> B