[Effective C++]자원 관리 클래스에서 관리되는 자원은 외부에서 접근할 수 있도록 하자

자원 관리, 세 번째 이야기

Posted by SungBeom on December 03, 2019 · 9 mins read

실제 자원을 직접 접근해야 하는 API에서의 자원 관리 클래스

자원 관리 클래스는 실수로 터질 수 있는 자원 누출을 튼튼히 막아 주는 보호벽 역할을 해 주는 듬직한 클래스입니다. 그런데 수많은 API들이 자원을 직접 참조하도록 만들어져 있어서, 자원 관리 객체에서 실제 자원을 직접 넘겨야할 일이 있을 것입니다.

std::tr1::shared_ptr<Investment> pInv(createInvestment());
createInvestment란 팩토리 함수를 호출한 결과(포인터)를 담기 위해 auto_ptr 혹은 tr1::shared_ptr과 같은 스마트 포인터를 사용하는 예제가 있습니다.

int daysHeld(const Investment *pi);  // 투자금이 유입된 이후로 경과한 날수
이때 어떤 Investment 객체를 사용하는 함수로서 여러분이 사용하려고 하는 것이 다음과 같다고 가정해 봅시다.

int days = daysHeld(pInv);  // 에러!
그리고 이렇게 호출하고 싶을 텐데요. 애석하게도 이 코드는 컴파일이 안 됩니다. daysHeld 함수는 Investment* 타입의 실제 포인터를 원하는데, 여러분은 tr1::shared_ptr<Investment> 타입의 객체를 넘기고 있기 때문입니다.

사정이 이렇다 보니, RAII 클래스의 객체를 그 객체가 감싸고 있는 실제 자원으로 변환할 방법이 필요해집니다. 이런 목적에 일반적인 방법을 쓴다면 두 가지가 있는데, 하나는 명시적 변환(explicit conversion)이고 또 다른 하나는 암시적 변환(implicit conversion)입니다.

명시적 변환

int days = daysHeld(pInv.get());  // 실제 포인터를 넘기므로 문제 없습니다.
tr1::shared_ptr 및 auto_ptr은 명시적 변환을 수행하는 get이라는 멤버 함수를 제공합니다. 이 함수를 사용하면 각 타입으로 만든 스마트 포인터 객체에 들어 있는 실제 포인터(의 사본)를 얻어낼 수 있습니다.

제대로 만들어진 스마트 포인터 클래스라면 거의 모두가 그렇듯, tr1::shared_ptr과 auto_ptr은 포인터 역참조 연산자(operator-> 및 operator*)도 오버로딩하고 있습니다. 따라서 자신이 관리하는 실제 포인터에 대한 암시적 변환도 쉽게 할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Investment {
public:                                 // 여러 형태의 투자를 모델링한
    bool isTaxFree() const;             // 투자 클래스들의 최상위 클래스
    ...
};
 
Investment* createInvestment();         // 팩토리 함수
 
std::tr1::shared_ptr<Investment>        // tr1::shared_ptr이 자원
    pi1(createInvestment());            // 관리를 맡도록 합니다.
 
bool taxable1 = !(pi1->isTaxFree());    // operator->를 써서
...                                     // 자원에 접근합니다.
 
std::auto_ptr<Investment>               // auto_ptr로 하여금
    pi2(createInvestment());            // 자원 관리를 맡도록 합니다.
 
bool taxable2 = !((*pi2).isTaxFree());  // operator*를 써서
                                        // 자원에 접근합니다.
cs

RAII 객체 안에 들어 있는 실제 자원을 얻어낼 필요가 종종 생기기 때문에, RAII 클래스 중에는 암시적 변환 함수를 제공하여 자원 접근을 매끄럽게 할 수 있도록 만드는 경우도 있습니다. 예를 들어, 어떤 하부 수준 C API로 직접 조작이 가능한 폰트를 RAII 클래스로 둘러싸서 쓰는 경우를 생각해 보죠.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
FontHandle getFont();             // C API에서 가져온 함수
 
void releaseFont(FontHandle fh);  // C API에서 가져온 함수
 
class Font {                      // RAII 클래스
public:
    explicit Font(FontHandle fh)  // 자원을 획득합니다.
        : f(fh)                   // 자원 해제를 C API로 하기 때문에
    { }                           // 값에 의한 전달이 수행됩니다.
    ~Font() { releaseFont(f); }
 
private:
    FontHandle f;                 // 실제 폰트 자원
};
cs

하부 수준 C API는 FontHandle을 사용하도록 만들어져 있으며 규모도 무척 크다고 가정하면, Font 객체를 FontHandle로 변환해야 할 경우도 적지 않을 것입니다. Font 클래스는 이를 위한 명시적 변환 함수로 get을 제공할 수 있을 것입니다.

1
2
3
4
5
6
class Font {
public:
    ...
    FontHandle get() const { return f; }  // 명시적 반환 
    ...
};
cs

이렇게 해 두면 어쨌든 쓸 수 있긴 한데, 사용자는 하부 수준 API를 쓰고 싶을 때마다 get을 호출해야 할 것입니다.

1
2
3
4
5
6
7
8
void chageFontSize(FontHandle f, int newSize);  // 폰트 API의 일부
 
Font f(getFont());
int newFontSize;
...
 
changeFontSize(f.get(), newFontSize);  // Font에서 FontHandle로
                                       // 명시적으로 바꾼 후에 넘깁니다.
cs

암시적 변환

변환할 때마다 함수를 호출해 주여야 한다는 번거로움과, 폰트 자원이 누출될 가능성이 늘어난다면 애석할 것입니다. 대안으로 FontHandle로의 암시적 변환 함수를 Font에서 제공하도록 하면 됩니다.

1
2
3
4
5
6
7
class Font {
public:
    ...
    Operator FontHandle() const  // 암시적 변환 함수
        { return f; }
    ...
};
cs

암시적 변환 덕택에 C API를 사용하기가 훨씬 쉬워지고 자연스러워집니다.

1
2
3
4
5
6
Font f(getFont());
int newFontSize;
...
 
changeFontSize(f, newFontSize);  // Font에서 FontHandle로
                                 // 암시적 변환을 수행합니다.
cs

그렇다고 마냥 좋은 것만은 아닙니다. 암시적 변환이 들어가면 실수를 저지를 여지가 많아집니다.

1
2
3
4
Font f1(getFont());
...
FontHandle f2 = f1;  // 원래 의도는 Font 객체를 복사하는 것이었는데,
                     // f1이 FontHandle로 바뀌고 나서 복사되었습니다.
cs

RAII 클래스를 실제 자원으로 바꾸는 방법으로서 명시적 변환을 제공할 것인지(get 멤버 함수 등) 아니면 암시적 변환을 허용할 것인지에 대한 결정은 그 RAII 클래스만의 특정한 용도와 사용 환경에 따라 달라집니다. 어쨌든 가장 잘 설계한 클래스라면 "맞게 쓰기에는 쉽게, 틀리게 쓰기에는 어렵게" 만들어져야 할 것입니다. 늘 그런 것은 아니지만, 암시적 변환보다는 get 등의 명시적 변환 함수를 제공하는 쪽이 나을 때가 많습니다. 원하지 않은 타입 변환이 일어날 여지를 줄여주는 것은 확실하니까요. 하지만 암시적 타입 변환에서 생기는 사용 시의 자연스러움이 빛을 발하는 경우도 있습니다.


정리

실제 자원을 직접 접근해야 하는 기존 API들도 많기 때문에, RAII 클래스를 만들 때는 그 클래스가 관리하는 자원을 얻을 수 있는 방법을 열어 주어야 합니다.
자원 접근은 명시적 변환 혹은 암시적 변환을 통해 가능합니다. 안전성만 따지면 명시적 변환이 대체적으로 더 낫지만, 고객 편의성을 놓고 보면 암시적 변환이 괜찮습니다.