Search

Effective C++ 7: 다형성을 가진 기본 클래스에서는 소멸자를 반드시 가상 소멸자로 선언하자

개요

C++에서 객체 지향 프로그래밍을 할 때, 다형성(polymorphism)은 핵심적인 개념 중 하나입니다. 그러나 다형성을 활용할 때 주의해야 할 점들이 있으며, 그 중 하나가 바로 소멸자(destructor)의 처리입니다. 특히 다형성을 가진 기본 클래스(base class)에서 소멸자를 어떻게 선언하느냐에 따라 예기치 않은 메모리 누수가 발생할 수 있습니다. 이번 글에서는 Effective C++의 일곱 번째 항목을 통해 다형성을 가진 기본 클래스에서 가상 소멸자(virtual destructor)를 사용해야 하는 이유와 그 방법에 대해 알아보겠습니다.

본문

비가상 소멸자의 문제점

먼저 비가상 소멸자를 사용했을 때 발생할 수 있는 문제를 살펴보겠습니다. 다음과 같은 클래스 계층 구조를 가정해봅시다:
class TimeKeeper { public: TimeKeeper() { std::cout << "TimeKeeper 생성자\\n"; } ~TimeKeeper() { std::cout << "TimeKeeper 소멸자\\n"; } }; class AtomicClock : public TimeKeeper { public: AtomicClock() { std::cout << "AtomicClock 생성자\\n"; } ~AtomicClock() { std::cout << "AtomicClock 소멸자\\n"; } }; TimeKeeper* getTimeKeeper() { return new AtomicClock(); }
C++
복사
이제 이 클래스들을 사용하는 코드를 보겠습니다:
int main() { TimeKeeper* ptk = getTimeKeeper(); // ... 객체 사용 ... delete ptk; // 문제 발생! return 0; }
C++
복사
이 코드를 실행하면 AtomicClock의 소멸자가 호출되지 않고, TimeKeeper의 소멸자만 호출됩니다. 이는 TimeKeeper의 소멸자가 가상 소멸자가 아니기 때문입니다. 결과적으로 AtomicClock 객체의 일부가 제대로 정리되지 않아 메모리 누수가 발생할 수 있습니다.

가상 소멸자의 해결책

이 문제를 해결하기 위해서는 기본 클래스의 소멸자를 가상 소멸자로 선언해야 합니다:
class TimeKeeper { public: TimeKeeper() { std::cout << "TimeKeeper 생성자\\n"; } virtual ~TimeKeeper() { std::cout << "TimeKeeper 소멸자\\n"; } };
C++
복사
이렇게 하면 delete ptk;를 호출했을 때 AtomicClock의 소멸자가 먼저 호출되고, 그 다음 TimeKeeper의 소멸자가 호출되어 객체가 올바르게 정리됩니다.

가상 함수 테이블과 그 영향

가상 소멸자를 사용하면 클래스에 가상 함수 테이블(vtable)이 추가됩니다. 이 테이블은 실행 시간에 올바른 함수를 호출하기 위해 사용됩니다.
class Point { public: Point(int x, int y) : x(x), y(y) {} virtual ~Point() {} // 가상 소멸자 추가 private: int x, y; };
C++
복사
Point 클래스에 가상 소멸자를 추가하면, 객체의 크기가 증가합니다. 32비트 시스템에서는 8바이트(x와 y)에서 12바이트(x, y, vptr)로, 64비트 시스템에서는 8바이트에서 16바이트로 증가할 수 있습니다. 이는 메모리 사용량 증가와 성능에 영향을 줄 수 있으므로, 다형성이 필요 없는 클래스에서는 가상 소멸자를 사용하지 않는 것이 좋습니다.

순수 가상 소멸자의 사용

때로는 추상 클래스(abstract class)를 만들고 싶지만, 순수 가상 함수를 넣을 만한 것이 없을 때가 있습니다. 이런 경우 순수 가상 소멸자를 사용할 수 있습니다:
class AWOV { // Abstract Without Virtuals public: virtual ~AWOV() = 0; // 순수 가상 소멸자 }; AWOV::~AWOV() {} // 순수 가상 소멸자의 정의
C++
복사
이렇게 하면 AWOV 클래스는 추상 클래스가 되며, 동시에 가상 소멸자를 가지게 됩니다. 주의할 점은 순수 가상 소멸자도 반드시 정의를 제공해야 한다는 것입니다.

다형성을 가진 기본 클래스와 그렇지 않은 클래스

모든 기본 클래스가 다형성을 가져야 하는 것은 아닙니다. 예를 들어, 표준 라이브러리의 string이나 STL 컨테이너들은 다형성을 위해 설계되지 않았습니다. 이런 클래스들은 가상 소멸자를 가지고 있지 않으며, 이들을 상속받아 사용하는 것은 좋지 않습니다.
// 권장하지 않는 사용 방법 class SpecialString : public std::string { // ... };
C++
복사
대신, 이런 경우에는 컴포지션(composition)을 사용하는 것이 더 적절합니다:
class SpecialString { private: std::string str; // ... };
C++
복사

개인적 견해

C++의 가상 소멸자 개념은 가비지 컬렉션 언어를 주로 사용해온 제게는 상당히 새로운 도전이었습니다. 이러한 언어들에서는 메모리 관리를 크게 신경 쓰지 않아도 되기 때문에, C++의 세세한 메모리 관리 방식에 적응하는 데 시간이 걸렸습니다.
특히 가상 함수 테이블의 개념은 아직도 완벽히 이해하기 어려운 부분입니다. 객체의 메모리 구조가 어떻게 변하고, 이것이 성능에 어떤 영향을 미치는지 더 깊이 공부해야 할 것 같습니다.
그럼에도 불구하고, 이러한 세밀한 제어가 C++의 강력한 성능의 원천이라는 점은 분명해 보입니다. 게임 개발과 같은 고성능 애플리케이션 개발에서 이러한 지식이 큰 도움이 될 것 같습니다.

결론

다형성을 가진 기본 클래스에서 가상 소멸자를 사용하는 것은 C++에서 안전한 객체 지향 프로그래밍을 위한 핵심 규칙입니다. 이를 통해 메모리 누수를 방지하고 객체의 올바른 소멸을 보장할 수 있습니다.
하지만 모든 상황에서 가상 소멸자가 필요한 것은 아니며, 클래스의 설계 의도에 따라 적절히 사용해야 합니다. 이러한 세심한 주의가 C++ 프로그래밍에서 높은 성능과 안정성을 동시에 달성하는 핵심이 될 것입니다.