Study/Effective C++

[effective C++] Chapter 03 : 13. 자원 관리에는 객체가 그만 !

코딩뚜벅이 2023. 8. 4. 23:09

13. 자원 관리에는 객체가 그만 !

  • 객체의 소멸 관리
  • auto_ptr
  • TR1-RCSP
  • 이것만은 잊지 말자 

객체의 소멸 관리

 투자를 모델링해 주는 클래스를 가지고 작업을 한다고 가정하자. Investment라는 최상위 클래스가 있고 그 밑에 구체적인 형태의 투자 클래스가 있다고 생각하고 Investment에서 파생된 클래스의 객체를 얻어내기 위한 용도로 CreateInvestment()라는 이름의 팩토리 함수가 정의된다고 하면 아래와 같은 코드가 될 것이다.

 

 

위 CreateInvestment 함수를 통해 얻어낸 객체의 삭제는 호출해준 쪽에서 해주어야 한다. 따라서 팩토리 함수의 호출과 메모리 해제를 하는 함수 f를 아래와 같이 구현해 보았다.

 

 

 하지만 위의 함수는 중간에 return을 만나거나 continue 혹은 goto문을 만나 루프를 빠져나와 delete 코드를 만나지 못하여서 객체의 삭제에 실패할 수도 있다.

 무조건적으로 메모리를 해제할 수 있도록 하는 방법은 자우너을 객체에 넣고 그 자원 해제를 소멸자가 하도록 하며, 그 소멸자는 실행 제어(위에서는 함수f)를 떠날 때 호출되도록 구현하는 것이다. 개발에 쓰이는 객체의 상당수가 힙에서 동적으로 할당되고 하나의 블록 혹은 함수 내에서만 쓰이는 경우가 많기에 그로부터 빠져나오면 해제되도록 구현하는 것이 맞다. 

 


 

auto_ptr

 표준 라이브러리에서 auto_ptr(스마트 포인터)를 제공하고 있는데 이는 위와 같은 용도로 사용할 수 있게 마련된 클래스이다.

 

 

 위의 코드는 이전보다 훨씬 간단하지만 auto_ptr이 소멸자를 호출하여 메모리를 해제하고 있다. 이 간단한 코드로 우리는 자원 관리에 객체를 사용하는 두 가지 특징을 알 수 있다.

 

 첫째, 자원을 획득한 후에 자원 관리 객체에게 넘긴다.

 위에서는 Investment 함수가 만들어준 자원이 그 자원을 관리할 auto_ptr 객체를 초기화하는데 사용되고 있다. 이렇게 자원 관리에 객체를 사용하는 업계 용어 중 하나가 '자원 획득 즉, 초기화(Resource Acquisition Is Initialization: RAII)' 이다. 자원 획득과 자원 관리 객체의 초기화가 한 문장에서 이루어지는 것을 매우 일상적이다.

 

 둘째, 자원 관리 객체는 자신의 소멸자를 사용해서 자원이 확실히 해제되도록 한다.  

 소멸자는 어떤 객체가 소멸될 때 자동적으로 호출되기 때문에 실행 제어가 어떤 경위로 떠나는가에 상관없이 자원 해제가 제대로 이루어지게 된다. 

 추가로 auto_ptr은 자신이 가리키는 대상에 자동적으로 delete를 해주나 만약 가리키는 대상이 둘 이상이라면 자원이 두 번 삭제되는 '미정의 동작' 결과를 야기한다. 이를 방지하기 위해 auto_ptr은 가리키는 대상이 변하면 그 이전의 대상을 NULL로 만드는 동작을 취한다. 아래는 앞선 설명을 이해하기 위한 코드이다.

 

 


 

TR1-RCSP

 auto_ptr은 매우 좋은 기능을 가지고 있으나 제한 사항이 있고 동적으로 할당되는 모든 객체를 관리하기 좋은 방법은 아니라는 생각이 든다. 만약 auto_ptr을 사용할 수 없는 상황이라면 대안으로 참조 카운팅 방식 스마트 포인터(reference-counting smart pointer: RCSP)가 유용하다. RCSP는 특정한 어떤 자원을 가리키는 외부 객체의 개수를 유지하다가 그 개수가 0이되면 해당 자원을 자동으로 삭제하는 스마트 포인터이다. 이는 가비지 컬렉션과 동작이 유사하지만, 참조 상태가 고리를 이루는 경우(다른 두 객체가 서로를 가리키거나) 삭제할 수 없다는 것은 차이점이다.

 

 RCSP는 TR1에서 제공되며 이를 통해 함수f를 다시 작성하면 아래와 같다.

 

 

RCSP는 복사 동작이 예상대로 이루어지기 때문에 괴상한 복사 동작으로 auto_ptr을 사용할 수 없는 STL에서 사용하기 적합하다. 

 

 한 가지 더, auto_ptr과 RCSP(shared_ptr)은 delete[] 연산자가 아닌 delete 연산자를 소멸자 내부에서 사용한다. 즉, 동적으로 할당한 배열에 대해 사용하면 컴파일 에러가 발생하지는 않지만 문제가 발생한다. 아래는 좋지 않은 예시이다.

 

 

C++에서는 동적으로 할당된 배열을 위해 auto_ptr이나 shared_ptr을 통해 제공해주지 않는다. 이유는 동적으로 할당된 배열을 vector 및 string으로 대체할 수 있기 때문이다. 


 

이것은 잊지 말자 !

1. 자원 누출을 막기 위해, 생성자 안에서 자원을 획득하고 소멸자에서 그것을 해제하는 RAII 객체를 사용하자.

2. 일반적으로 널리 쓰이는 RAII 클래스는 tr1::shared_ptr 그리고 auto_ptr이다. 이 둘 가운데 tr1::shared_ptr이 복사 시의 동작이 직관적이기 때문에 대개 더 좋다. 반면, auto_ptr은 복사되는 객체(원본 객체)를 null로 만들어 버린다.