3장. 자원관리 👥

안녕하세요! 두두코딩 입니다 ✋
오늘은 항목 17 new로 생성한 객체를 별도의 코드로 두는 개념에 대해 알아보겠습니다.

🖇 소스코드에 마우스를 올리고 copy 버튼을 누를 경우 더 쉽게 복사할 수 있습니다!

궁금한 점, 보안점 남겨주시면 성실히 답변하겠습니다. 😁
+ 감상평 댓글로 남겨주시면 힘이됩니다. 🙇

스마트 포인터를 인자로 전달받을 경우

처리에 대한 우선순위를 결정하는 함수와 동적으로 할당한 Widget 객체에 대한 우선순위 별 처리하는 함수가 있다고 가정해보자. 함수의 이름은 다음과 같다.

// 우선순위를 결정하는 함수
int priority();

// Widget 객체에 대한 우선순위 별 처리하는 함수
void processWidget(std::tr1::shared_ptr<Widget> pw, int priority);

자원 관리 객체를 사용하는 것이 좋다는 이전포스팅에 따라 processWidget()에서 우리는 shared_ptr을 활용해 포인터를 관리하도록 만들었다.

processWidget(new Widget, priority());

위의 코드와 같이 processWidget을 호출하게 된다면 당연 컴파일에러가 발생한다. 다알겠지만, new Widget부분 때문에 컴파일 자체가 되지 않는다. (shared_ptr로 전달받아야한다. 암시적변환은 안될 것이다.)

Tip shared_ptr 의 생성자는 explicit으로 생성되어져있다. 해당 키워드가 들어가 있을 경우 생성자에서 암시적 형변환을 허용하지 않는다. 따라서, 컴파일 자체가 이뤄지지 않게된다.

위의 코드를 정상화 시키기 위해서는 아래와 같이 바꿔줘야한다.

processWidget(std::tr1::shared_ptr<Widget>(new Widget), priority());

위와 같이, 명시적으로 형변환을 해서 Widget 객체를 생성해 넘겨주게 될 경우 컴파일 에러가 나지 않고 잘 동작하게 된다.

그런데 이 코드는 자원 누수를 막기 위해 관리객체를 활용했지만, 어디선가 자원 누수가 발생하게 된다. 어디서 발생할까? 찾아보자.

스마트 포인터 생성하는 부분은 별도로 둬야한다

우리가 “컴파일러” 가 됐다고 가정해보자.

컴파일러는 processWidget을 호출할 때, 매개변수로 넘겨지는 인자들에 대한 평가작업을 먼저 진행한다. priority같은 경우 일반함수 호출이지만, 첫 번째 인자인 std::tr1::shared_ptr<Widget>(new Widget) 같은 경우 두 부분으로 나누어진다.

  1. “new Widget” 이 수행되는 부분
  2. tr1::shared_ptr의 생성자가 호출 되는 부분

따라서, 컴파일러는 processWidget()가 호출될 때 3가지 연산을 수행하게 되도록 해야한다.

  1. priority() 를 호출
  2. “new Widget”연산을 수행
  3. tr1::shared_ptr 생성자를 호출함.

위의 연산이 순서대로 작동을 하면 문제없이 코드는 동작하게 된다. 하지만, C++ 컴파일러 규정에는 해당 순서를 정하는 룰이 없다. 즉, 컴파일러를 만드는 제작사마다 순서가 다를 수도 있다는 의미이다. C++에서는 컴파일러 제작자들에게 자유도를 제공하기 때문에 이런 문제가 생길 수 있다.

Tip C++ 아닌 다른 객체지향 언어, 예를들면 Java or C# 등, 과 같은 언어에서는 순서가 지정되어져 있다. 따라서, 이런 문제를 고민할 필요가 없다.

순서가 바뀌게 될 경우 어떤 문제가 유발될지 고민해보자.

우선 순서가 바뀔 수 있는 케이스는 한가지이다. 구체적으로, tr1::shared_ptr은 항상 new Widget연산 이후에 와야하기 때문에 하나의 세트로 볼 수 있다. 해당 세트와 priority() 호출 간에 순서가 변경될 수도 있다.

예를들어 아래와 같은 순서로 동작했다고 생각해보자.

  1. “new Widget” 연산을 수행
  2. priority() 호출
  3. tr1::shared_ptr 생성자를 호출함.

위와 같은 순서로 호출되게 될 경우 어떻게 보면 성능 효율적인 측면에서는 좋을 수 있다. 하지만, priority() 호출하는 과정에서 예외가 발생했다고 해보자. new Widget 같은 경우 shared_ptr에서 자원을 해제 해주겠다는 것만 믿고 있었는데, shared_ptr에 저장되기도 전에 예외가 발생하게 됐기 때문에 delete연산을 호출하지 못하게 된다. 따라서, 자원 누수가 발생하게 된다.

위와 같은 문제를 해결하는 방법은 너무 간단한다. 실제 생성한 객체가 자원관리 객체로 넘어가기 전에 예외가 발생하는 경우를 제거하면 된다.

// new 생성하는 객체를 스마트 포인터에 담는 코드는
// 한 문장으로 만들자.
std::tr1::shared_ptr<Widget> pw(new Widget);

processWidget(pw, priority());

위의 코드를 보면, new를 활용해 객체를 만들고, 자원관리 객체에 넘기는 코드를 processWidget()가 수행되기 전에 별도의 한 문장으로 만들었다. 따라서, 자원 관리 객체에 실제 포인터를 저장하는 과정에서 발생할 수 있는 예외 즉, priority() 호출 과정이 끼어드는 문제를 제거함으로 문제를 해결할 수 있다.

문장과 문장 사이에 있는 연산들은 컴파일러의 재조정을 받을 여지가 적다. 따라서, 독립된 문장으로 선언하면 priority()호출 과정이 끼어들 일이 없어진다.

기억하자.

💎 객체 생성후 포인터를 관리객체에 넣어줄 때 발생할 수 있는 예외를 줄이기 위해 독립된 한 문장으로 선언하는 것이 좋다

To Sum Up

👉 new로 생성한 객체를 스마트 포인터로 넣는 코드는 별도의 한 문장으로 만들자. 예외가 발생할 경우 디버깅이 힘든 자원 누수 현상을 초래하는걸 사전에 막도록 하자!