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

Jangmanbo·2023년 3월 28일
0

Effective C++

목록 보기
15/33

항목 13, 14에서 배운 것을 토대로 자원 관리 객체를 사용한다면, 직접 자원에 접근하지 않으면서 자원 누출에 대한 걱정을 지울 수 있다.

그러나 수많은 API들은 자원을 직접 참조하도록 만들어져 있다.
결국 자원 관리 객체의 보호를 넘어 실제 자원에 직접 접근해야 할 일이 생기는 것이다.

실제 자원에 접근해야 하는 경우

// 항목 13
std::shared_ptr<Investment> pInv(createInvestment());	// createInvestment()는 팩토리 함수

int daysHeld(const Investment *pi);	// 인자로 Investment*를 원함

int days = daysHeld(pInv);	// Error!

위 코드는 컴파일 에러가 발생한다. Investment*가 아닌 shared_ptr<Investment> 타입의 객체를 넘겼기 때문이다.

RAII 클래스의 객체(여기서는 shared_ptr<Investment>)를 그 객체가 감싸고 있는 실제 자원(Investment*)으로 변환해야 한다.


1. 스마트 포인터 -> 실제 자원 객체

  • RAII 클래스 중 스마트 포인터에서 실제 자원으로 변환하는 방법

A. 명시적 변환 (explicit conversion)

int days = dayHeld(pInv.get());	// 실제 포인터(Investment*)를 넘기므로 에러 X

unique_ptr와 shared_ptr는 명시적 변환을 수행하는 멤버함수 get을 제공한다.
get을 통해 스마트 포인터 객체에 들어있는 실제 포인터의 사본을 얻을 수 있다.

B. 암시적 변환 (implicit conversion)

class Investment {
public:
	bool isTaxFree() const;
    ...
};

Investment* createInvestment();	// 팩토리 함수

std::shared_ptr<Investment> pi1(createInvestment());	// shared_ptr가 자원 관리 수행

bool taxable1 = !(pi1->isTaxFree());	// operator->를 사용하여 자원에 접근

...
std::unique_ptr<Investment> pi2(createInvestment());	// unique_ptr가 자원 관리 수행

bool taxable2 = !((*pi2).isTaxFree());	// operator*를 사용하여 자원에 접근

unique_ptr와 shared_ptr는 포인터 역참조 연산자(operator->, operator*)도 오버로딩하여 제공한다.

2. 직접 만든 RAII 클래스 -> 실제 자원 객체

  • 직접 만든 RAII 클래스 객체에서 실제 자원으로 변환하는 방법
// C API가 제공하는 함수
FontHandle getFont();
void releaseFont(FontHandle fh);	// 폰트 자원 해제 함수
void changeFontSize(FontHandle f, int newSize);	// 기타 폰트 조작 함수

C API로 직접 조작 가능한 FontHandle 객체를 보호하기 위해 RAII 클래스인 Font 클래스를 직접 만들어보자.

// RAII 클래스
class Font {
public:
	explicit Font(FontHandle fh):f(fh) {}	// 자원 획득! 자원 해제를 C API로 하기 때문에 pass by value로 넘김
    ~Font() { releaseFont(f); }		// 자원 해제!
    
private:
	FontHandle f;	// 실제 폰트 자원
}

기존에 있는 C API는 FontHandle을 사용하도록 만들어져 있으므로 Font 객체를 FontHandle로 변환해야 할 경우가 종종 있을 것이라 예상할 수 있다.

A. 명시적 변환 (explicit conversion)

class Font {
public:
	...
    FontHandle get() const { return f; }	// 명시적 변환 함수
    ...
}

이제 get을 통해 명시적으로 Font에서 FontHandle로 변환할 수 있다.

Font f(getFont());	// Font 객체 생성자 호출
int newFontSize;
...
changeFontSize(f.get(), newFontSize);	// 명시적으로 변환 후 넘김

그러나 변환할 때마다 get함수를 호출해야 한다.

B. 암시적 변환 (implicit conversion)

class Font {
public:
	...
    operator FontHandle() const { return f; }	// 암시적 변환 함수
    ...
}

암시적 변환 함수를 제공하므로 이제는 명시적으로 get을 사용하지 않아도 Font에서 FontHandle로 변환할 수 있다.

Font f(getFont());	// Font 객체 생성자 호출
int newFontSize;
...
changeFontSize(f, newFontSize);	// 암시적으로 변환 후 넘김

하지만 '번거로움이 줄었으니 암시적 변환 함수를 만들어야지' 라고 단순하게 생각하면 안된다.
정말로 Font를 사용하려고 했는데 암시적 변환 함수 때문에 FontHandle로 바뀌어버릴 수 있기 때문이다.

Font f1(getFont());
...
FontHandle f2 = f1;	// f1이 관리하고 있던 FontHandle 객체가 f2로 복사되었다.

이제 f2를 통해서 f1이 관리하고 있는 폰트(FontHandle)를 직접 사용할 수 있게 되었다.
그러나 하나의 자원을 두 객체에서 사용할 수 있는 상황은 그다지 좋은 상황이 아니다.

f1이 소멸되는 시점(블락이나 함수가 끝남)부터 FontHandle객체는 해제될 것이고, f2는 이미 해제된 폰트를 잡고 있게 되는 것이다.


참고: 사용자 정의 타입변환과 explicit
암시적 변환에 대해 더 알고 싶다면 가볍게 볼 만한 포스팅인 것 같다.


무엇이 정답일까...

정답은 없다. RAII 클래스를 실제 자원으로 바꾸는 방법으로 명시적 변환을 제공할 것인지, 암시적 변환도 제공할 것인지는 해당 RAII 클래스의 용도와 사용 환경에 따라 다르다.

단, 대체적으로는 get 등의 명시적 변환 함수만들 제공하는 것이 좋다.


부록

이번 항목에서 RAII 클래스의 실제 자원을 참조하는 방법들에 대해 배우면서 캡슐화를 위반하는 것이 아닐까 라는 의문이 들었을 수 있다.

틀린 말은 아니지만, 애초에 RAII 클래스의 목적은 정보 은닉이 아니라 자원 해제이기 때문에 틀린 설계도 아니다.

캡슐화를 하고 싶다면, 자원 해제라는 기본 기능 위에 얹을 수는 있다.
실제로 shared_ptr의 경우 참조 카운팅 메커니즘에 필요한 장치들은 모두 캡슐화하지만, 실제 자원에는 쉽게 접근이 가능하다.



정리
1. 실제 자원에 접근해야 하는 기존 API들이 많기 때문에 RAII 클래스를 만들 때는 그 클래스가 관리하는 자원을 얻을 수 있는 방법(명시적/암시적 변환)을 제공해야 한다.
2. 안정성으로는 명시적 변환이, 고객 편의성으로는 암시적 변환이 낫다.

0개의 댓글