3장. 자원관리 👥
안녕하세요! 두두코딩 입니다 ✋
오늘은 Effective C++ 3장 자원관리 개념에 대해 알아보겠습니다.
🖇 소스코드에 마우스를 올리고 copy 버튼을 누를 경우 더 쉽게 복사할 수 있습니다!
궁금한 점, 보안점 남겨주시면 성실히 답변하겠습니다. 😁
+ 감상평 댓글로 남겨주시면 힘이됩니다. 🙇
Legacy 코드를 위한 자원 객체
자원관리 클래스는 자원 누출을 막아주는 보호재 역할을한다. 보통 잘 설계된 시스템의 경우 자원 관리 객체를 이용해 자원 누출에 대한 현상들을 미리 제거한다. 보통 우리가 살아가면서 느끼는 거지만 내 마음대로 모든 일이 순조롭게 풀리지 않는다 😰
자원관리 객체가 들어오기 전 작성된 수 많은 Legacy API 들이 자원을 직접 참조 하도록 되어져 있어서, 자원관리 객체가 무용지물이 된다. (물론.. 마음먹고 시스템의 모든 자원 사용하는 객체들에 대해 관리 객체를 만들어 준다면 상관없다.. 하지만 현실에선 그렇게 하기 어렵다ㅠㅠ 😬)
일례로, 우리가 이전포스팅 에서 다뤘던 createInvestment()
등의 결과를 auto_ptr
또는 tr1::shared_ptr
를 활용해 자원을 관리 했던 것을 기억할 것이다.
...
// 투자금이 유입된 이후로 경과한 날 수.
int dayHeld(const Investment *pi);
...
int main() {
std::tr1::shared_ptr<Investment> pInv(createInvestment());
// 연체일자를 확인하기 위해, 경과날을 파악해야함.
dayHeld(pInv);
}
위의 코드를 보면, 자원 관리 객체 shared_ptr
를 활용해 자원유출을 막고 있는 것을 확인할 수 있다. 만약 위와같이, 이전부터 연체일자를 계산하기 위해 투자금을 유입받은날로 부터 경과를 확인할 수 있는 함수 (dayHeld()
)가 있었다고 가정해보자. 우리는 pInv
라는 객체 즉, 투자금을 가리키는 포인터를 dayHeld
에 전달해 얼마나 경과되었는지 파악하려고한다.
결과는 어떻게 나올까?
애석하게도.. 해당 코드는 컴파일 도중 에러가 뜰 것이다.
dayHeld()
의 인자는 cosnt Investment*
즉, raw pointer로 구성된다. 하지만, 우리가 지금 넘기는 포인터는 관리 객체의 포인터 즉, raw pointer를 wrapping하고 있는 객체를 넘기기 때문에 타입 불일치로 컴파일러에서 에러로 간주해버린다.
위의 문제를 해결하기 위해선 RAII 객체 즉, 관리객체들을 실제 자원 (raw pointer)로 변환할 방법이 필요해진다.
변환
RAII 객체를 실제 raw pointer로 변경해서 사용해야 할 경우 변환작업을 거쳐야한다.
현재 C++에서 사용하고 있는 변환 방법은 명시적 변환 과 암시적 변환 두 가지를 사용하고 있다.
🌱 명시적 변환
tr1::shared_ptr
또는 auto_ptr
과 같은 관리객체에서는 명시적 변환을 수행하는 API인 get()
이라는 멤버함수를 제공한다. 다시 말해 이 함수를 사용하게 되면 각 타입으로 만든 스마트 포인터 객체에 들어 있는 실제 포인터를 얻을 수 있다. (포인터라고는 했지만, 실제로는 사본이다.)
...
// 포인터를 전달하게 될 경우 에러가 나지 않음
dayHeld(pInv.get());
...
위의 코드와 같이, 작성하게 될 경우, 실제 포인터를 전달하기 때문에 컴파일 에러없이 정상동작하게 된다. 보통 제대로 만들어진 관리객체라면 포인터 역참조 연산자 (operator-> 및 operator*) 를 오버로딩 하고 있으며, get()
도 보통 구현되어진다.
class Investment {
public:
bool isTaxFree() const;
...
};
// 팩토리 함수라 가정.
Investment* createInvestment();
std::tr1::shared_ptr<Investment>
pi1(createInvestment());
// operator-> 를 활용해 멤버함수에 접근이 가능하다.
bool texable1 = !(pi1->isTeaxFree());
...
std::auto_ptr<Investment> pi2(createInvestment());
// opeartor* 를 활용해 멤버함수 접근가능
bool texable2 = !((*p2).isTexFree());
위와 같이, 실제 자원에 접근할 수 있는 몇가지 연산자 혹은 함수가 잘 설계된 관리객체에서는 제공된다.
🌱 암시적 변환
위와 같이, 실제 포인터에 접근할 수 있는 방법이 다양하게 있기 때문에 우리는 암시적 변환도 쉽게 해볼 수 있다.
예시를 통해 왜 암시적 변환을 사용하려고 하는지 알아보고, 어떻게 만드는지 보도록 하자.
// C API에서 가져온 함수들
// Font 자원을 다루는 함수들이다.
FontHandle getFont();
void releaseFont(FontHandle fh);
...
// Font 자원을 관리하는 객체
class Font {
public:
explicit Font(FontHandle fh)
: f(fh)
{}
~Font() { releaseFont(f); }
private:
// 실제 font 자원
FontHandle f;
};
위의 코드는 C API를 활용하여 Font 를 얻어오고 반환하는 함수이다. 우리는 이를 편리하게 하기 위해 Font
라는 관리객체를 만들어 Font를 얻어오고, 소멸자에서 releaseFont
를 해제하도록 한다.
하부 C API들에서는 Font를 다룰일이 많고, 시스템이 아주 크다고 가정하면, Font라는 객체가 아닌 FontHandle
을 직접 다루는 코드가 많이 존재할 것이다. 이 경우에는 RAII 즉, 관리객체가 아닌 raw pointer를 넘겨야하는데, 만약 위에서 배운 get()
함수를 만들어 사용한다고 해보자.
class Font {
public:
...
FontHandle get() const { return f; }
...
};
...
// 하부 시스템에서 Font를 사용하는 함수
void changeFontSize(FontHandle f, int newSize);
Font f(getFont());
int newFontSize = 10;
...
changeFontSize(f.get(), newFontSize);
위와 같이, get()
라는 함수를 호출해 명시적으로 만들어 changeFontSize()
를 사용할 수 있다. 하지만, 우리는 어떤 민족인가.. 귀차니즘의 민족이 아니던가.. 변환 할떄마다 무슨 함수를 호출해 주어야 한다는 점이 짜증나서 Font 관리 객체를 안쓰겠다 라고 하는 사람들이 등장하기 시작한다..
Font 클래스의 설계 목적은 자원 누출을 못하게 막는것인데, 사용하는게 귀찮아서 자원 누출을 하겠다니 아주 아이러니 한 상황이 발생하게 된다. 이를 막기 위해 대안이 있는데, 바로 암시적 변환이다.
class Font {
public:
...
operator FontHandle() const
{
return f;
}
...
}
...
Font f(getFont());
int newFontSize = 10;
...
changeFontSize(f, newFontSize());
위와 같이, operator FontHandle()
이라는 함수를 구현해 둔다면, 암시적 변환이 가능해진다. 따라서 우리는 명시적으로 get()
요청하지 않고, Font를 FontHandle
처럼 사용할 수 있게 된다.
암시적 변환의 문제점
위의 암시적 변환은 귀차니즘을 막아주는 아주 유용한 도구이다. 하지만, 암시적 변환은 실수를 유발하기 때문에 마냥 좋은 대안은 아니다.
아래의 코드를 보자.
Font f1(getFont());
...
FontHandle f2 = f1;
우리가 Font
라는 객체를 만들어 Font
라는 객체를 복사해서 사용하고 싶다고 생각해보자. 하지만 개발자가 잠에 취해 Font
를 FontHandle
로 선언해버릴 경우 f1
의 자원은 FontHandle
로 변환돼 하나의 자원을 두개의 포인터가 가리키는 상황이 만들어지게 된다.
만약 이 상황에서 f1
이 소멸시점에 Font로 인해 releaseFont
함수가 불려 자원이 소멸 될텐데, f2
는 폰트에 매달려 제거되지 않는 상황이 연출된다. 혹여나 누군가 f2로 자원을 접근하게 된다면 심각한 문제가 유발 될 수도 있는것이다.. 😲
그렇다면 언제 암시적 변환을 써야하는거야?
그건 바로 RAII 클래스만의 사용용도 와 사용환경에 맞게 사용자가 정의해야한다. 늘 그런것만은 아니지만 암시적 변환 보다는 get()
활용한 명시적 변환을 제공하는 쪽이 나을 경우가 많이 존재한다. (왠만하면 명시적으로 가자..)
RAII 클래스에서 자원 접근을 열어주는 건?
우리가 내용을 배우면서 생각할 수 있는 부분이 RAII 관리 클래스는 자원에 대해 열어주는데 “이거 캡슐화 위반아닌가?” 라고 생각할 수 있다.
일반적으로 우리는 자원은 최대한 은닉하도록 즉, 캡슐화를 잘 지켜야한다고 이야기한다.
솔직히 해당 부분은 위반된 것은 맞다. 하지만, 해당 클래스의 탄생이유에 대해 생각해보자. 해당 클래스는 일반적으로 생성되는 객체 (데이터를 다루는 클래스)가 아니라 원하는 동작이 실수 없이 이루어 지도록 하는 클래스이다.
원하는 동작이라 함은, 자원이 잘 생성되고 잘 해제되는 것을 보장해주는 객체라는 것이다. (해당 클래스에서 너무 캡슐화를 따질 필요는 없다는 말이다 🙂)
실제로 이런 논리때문에 shared_ptr
에서는 캡슐화를 엄격한 캡슐화, 느슨한 캡슐화 두 부류로 나누어 정의하고 있다. 예를들어, shared_ptr
에서 사용하고 있는 참조 카운트 같은 경우 사용자가 접근할 필요가 없기 때문에 엄격한 캡슐화를 적용해 데이터에 접근을 막아버린다. 반면에 명시적 변환과 같이 사용자의 의도에 따라 변환이 필요할 때는 느슨한 캡슐화를 활용해 사용자에게 인터페이스를 열어주도록 설계되어져 있다.
위와 같이, 무조건 캡슐화를 따라가야해 라기 보다 좀 더 유연하게 필요에 맞게 관리 객체를 설계하도록 하자.
To Sum Up
👉 실제 자원을 직접 접근해야하는 기존 API들도 많다. 따라서, RAII 클래스를 만들 떄는 클래스가 관리하는 자원을 얻을 수 있는 방법을 만들어 줘야한다.
👉 자원 접근은 명시적 혹은 암시적 변환이 존재한다. 안전성만 따지면 명시적 변환이 낫지만, 편의성을 놓고 보면 암시적 변환이 더 낫다고 볼 수 있다.