Notice
Recent Posts
Recent Comments
Link
«   2026/05   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31
Archives
Today
Total
관리 메뉴

로또

15. 자원 관리 클래스에서 관리되는 자원은 외부에서 접근할 수 있도록 하자. 본문

책/Effective C++

15. 자원 관리 클래스에서 관리되는 자원은 외부에서 접근할 수 있도록 하자.

아롱로또 2023. 11. 13. 00:36

다루는 내용

자원 관리 클래스에서 자원을 외부에서 접근할 수 있도록 하는 이유 및 명시적 변환과 암시적 변환의 장단점

 

자원 관리 클래스에서 자원에 대해 외부 접근을 허용하는 이유

자원 관리 클래스는 분명 자원을 효율적으로 관리할 수 있지만, 현존하는 수많은 API들은 다음과 같이 자원을 직접 참조하도록 만들어져 있다. 

#include <iostream>
#include <memory>
using namespace std;

class Investment{ // 클래스
public:
    Investment(int _price): price(_price){}
    int price;
};

class InvestmentFactory{ // 팩토리 함수
public:
    static Investment* CreateInvestment(int price){
        return new Investment(price);
    }
};

int GetPrice(const Investment* pi){
	return pi->price;
}

int main(){
    shared_ptr<Investment> p_inv(InvestmentFactory::CreateInvestment(100)); 
    cout << GetPrice(p_inv) << endl; // 컴파일 에러
}

 

컴파일 에러

위 코드의 GetPrice 함수는 Investment*를 인자로 받아 동작하는 함수이지만, 우리가 사용하는 변수 p_inv는 shared_ptr<Investment> type이므로 위와 같이 컴파일 에러가 발생할 것이다.

 

때문에, RAII 클래스의 객체(shared_ptr)를 그 객체가 감싸고 있는 실제 자원(Investment*)으로 변환할 방법이 필요하다.

이러한 방법을 제공하기 위해서는 명시적 변환과 암시적 변환, 두 가지 방법이 존재한다.

 

명시적 변환

shared_ptr는 다음과 같이 명시적 변환을 수행하는 여러 멤버 함수와 연산자를 제공한다.

int main(){
    shared_ptr<Investment> p_inv(InvestmentFactory::CreateInvestment(100)); 
    cout << GetPrice(p_inv.get()) << endl; // shared_ptr가 갖는 실제 포인터의 사본 얻기
    cout << p_inv->price << endl; // shared_ptr를 통해 실제 포인터의 값에 접근하기
    cout << (*p_inv).price << endl; // shared_ptr를 통해 실제 포인터의 값에 접근하기
}

get() 함수를 통해 shared_ptr가 갖는 실제 포인터의 사본을 획득할 수 있다.

* 연산자나 -> 연산자를 사용해 실제 포인터를 사용하는 것처럼 스마트 포인터를 사용할 수 있다.

 

암시적 변환

명시적 변환 대신, 암시적 변환 함수를 제공할 수도 있다. 예를 들어보자.

하부 수준 C API는 FondHandle이라는 type을 사용한다.

FontHandle GetFont();
void ReleaseFont();
void ChangeFontSize(FontHandle fh, int new_size);

우리는 FondHandle 자원을 관리하기 위해 Font라는 이름을 갖는 RAII 클래스를 만들었다.

class Font{
public:
    explicit Font(FontHandle fh): f(fh){}
    ~Font(){
        ReleaseFont(f);
    }
private:
    FontHandle f;
};

물론, Font 클래스는 다음과 같이 명시적 변환 함수 get을 멤버 함수로 제공할 수도 있다.

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

int main()
    Font f(GetFont());
    ChangeFontSize(f.get(), 12); // 명시적 형변환
    ...
}

 

위처럼 명시적 변환 함수를 호출하는 것은 명확하지만 하부 수준 C API를 요구하는 함수를 사용할 때마다 get 함수를 호출해야 하는 불편함을 동시에 제공한다.

이를 해결할 수 있는 것이 암시적 변환 함수를 Font에서 제공하는 것이다.

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

int main()
    Font f(GetFont());
    ChangeFontSize(f, 12); // 암시적 형변환
    ...
}

 

물론, 암시적 변환 함수 또한 마냥 장점만을 갖는 것은 아니다. 다음과 같이 Font를 사용하려고 한 부분에서 원하지도 않는데 FontHandle을 사용하는 경우가 발생할 수 있다.

Font f1(GetFont());
FontHandle f2 = f1; // 본래 의도는 Font f2 = f1;

 

 

명시적 변환 vs 암시적 변환

RAII 클래스를 실제 자원으로 바꾸는 방법으로 명시적 변환을 제공할 것인지, 암시적 변환을 허용할 것인지에 대한 결정은 해당 RAII 클래스만의 특정한 용도와 사용 환경에 따라 달라진다. 늘 그런 것은 아니지만 대부분의 경우에서 원치 않는 타입 변환이 일어날 여지를 줄이기 위해 명시적 변환을 제공하는 쪽이 나을 때가 많다. 물론, 암시적 타입 변환에서 생기는 자연스러움이 빛을 발하는 경우도 존재한다.

 

RAII 클래스에서 자원 접근 함수를 제공하는 것이 캡슐화에 위배된다?

그렇다. 하지만 처음부터 RAII 클래스의 목적은 데이터 은닉이 아니라 자원 관리이다.

현존하는 RAII 클래스 중에는 이미 자원의 엄격한 캡슐화와 느슨한 캡슐화를 동시에 지원하는 것도 있다.

shared_ptr가 그 예시로, 해당 클래스는 참조 카운팅 메커니즘에 필요한 장치들은 모두 캡슐화하고 있지만 그와 동시에 자신이 관리하는 포인터를 쉽게 접근할 수 있는 통로도 여전히 제공하고 있다.

사용자가 볼 필요가 없는 데이터는 숨기지만, 사용자 차원에서 꼭 접근해야 하는 데이터는 열어주는 것이다.

 

이것만은 잊지말자!

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