Search

Effective C++ 49: new 처리자의 동작 원리를 제대로 이해하자

개요

C++ 프로그래밍에서 메모리 관리는 핵심적인 요소 중 하나입니다. 특히 대규모 애플리케이션이나 리소스가 제한된 환경에서 동적 메모리 할당은 중요한 이슈가 될 수 있습니다. 이번 항목에서는 new 연산자가 메모리 할당에 실패했을 때 호출되는 'new 처리자(new-handler)'의 개념과 동작 원리를 살펴보겠습니다. new 처리자를 이해하고 적절히 활용함으로써, 우리는 메모리 할당 실패 상황을 더욱 유연하고 강건하게 대처할 수 있습니다.

new 처리자의 작동 원리

C++에서 new 연산자를 사용하여 메모리를 할당할 때, 시스템의 가용 메모리가 부족하면 어떤 일이 일어날까요? 대부분의 개발자들은 단순히 예외가 발생한다고 생각할 것입니다. 하지만 실제로는 좀 더 복잡한 메커니즘이 작동합니다.
new 연산자가 요청된 메모리를 할당하지 못하면, 먼저 new 처리자라고 불리는 함수를 호출합니다. 이 함수는 개발자가 직접 정의하여 설정할 수 있으며, 메모리 부족 상황에 대한 커스텀 처리 로직을 구현할 수 있게 해줍니다.

set_new_handler 함수 사용하기

C++ 표준 라이브러리는 set_new_handler 함수를 제공하여 new 처리자를 설정할 수 있게 합니다. 이 함수는 <new> 헤더에 선언되어 있습니다.
namespace std { typedef void (*new_handler)(); new_handler set_new_handler(new_handler p) throw(); }
C++
복사
set_new_handler 함수는 새로운 new 처리자 함수의 포인터를 인자로 받아 설정하고, 이전에 설정되어 있던 처리자의 포인터를 반환합니다.
다음은 new 처리자를 설정하고 사용하는 간단한 예제입니다:
void outOfMem() { std::cerr << "메모리 할당 실패!" << std::endl; std::abort(); } int main() { std::set_new_handler(outOfMem); int* pBigDataArray = new int[1000000000L]; // 대량의 메모리 할당 시도 }
C++
복사
이 예제에서 outOfMem 함수가 new 처리자로 설정되었습니다. 만약 new 연산자가 메모리 할당에 실패하면 outOfMem 함수가 호출되어 에러 메시지를 출력하고 프로그램을 종료합니다.

효과적인 new 처리자 구현하기

new 처리자는 다음 동작 중 하나를 수행해야 합니다:
1.
더 많은 메모리를 확보
2.
다른 new 처리자 설치
3.
new 처리자 제거 (null 포인터 설정)
4.
예외 발생 (일반적으로 std::bad_alloc)
5.
프로그램 종료 (std::abort() 또는 std::exit() 호출)
예를 들어, 메모리 확보를 시도하는 new 처리자를 구현해 보겠습니다:
void* reservedMemory = nullptr; constexpr size_t MEMORY_SIZE = 1024 * 1024; // 1MB void customNewHandler() { if (reservedMemory == nullptr) { std::cout << "예약 메모리 사용 시도" << std::endl; reservedMemory = std::malloc(MEMORY_SIZE); if (reservedMemory != nullptr) { std::free(reservedMemory); reservedMemory = nullptr; return; } } std::set_new_handler(nullptr); // new 처리자 제거 } int main() { std::set_new_handler(customNewHandler); // ... 메모리 할당 코드 ... }
C++
복사
이 예제에서 customNewHandler는 먼저 예약된 메모리 블록을 해제하여 메모리를 확보하려 시도합니다. 이 방법이 실패하면 new 처리자를 제거하여 표준 bad_alloc 예외가 발생하도록 합니다.

클래스별 new 처리자 구현

때로는 특정 클래스에 대해서만 커스텀 new 처리자를 사용하고 싶을 수 있습니다. C++는 이를 직접 지원하지 않지만, 우리는 이를 구현할 수 있습니다. 다음은 클래스별 new 처리자를 구현하는 방법입니다:
class Widget { public: static std::new_handler set_new_handler(std::new_handler p) throw(); static void* operator new(std::size_t size) throw(std::bad_alloc); private: static std::new_handler currentHandler; }; std::new_handler Widget::currentHandler = nullptr; std::new_handler Widget::set_new_handler(std::new_handler p) throw() { std::new_handler oldHandler = currentHandler; currentHandler = p; return oldHandler; } void* Widget::operator new(std::size_t size) throw(std::bad_alloc) { std::new_handler globalHandler = std::set_new_handler(currentHandler); void* memory; try { memory = ::operator new(size); } catch (...) { std::set_new_handler(globalHandler); throw; } std::set_new_handler(globalHandler); return memory; }
C++
복사
이 구현에서 Widget 클래스는 자체적인 set_new_handleroperator new를 제공합니다. operator new는 임시로 Widget의 new 처리자를 전역 new 처리자로 설정하고, 메모리 할당을 시도한 후 원래의 전역 new 처리자를 복원합니다.

CRTP를 활용한 재사용 가능한 new 처리자 구현

위의 구현을 템플릿과 CRTP(Curiously Recurring Template Pattern)를 사용하여 재사용 가능하게 만들 수 있습니다:
template<typename T> class NewHandlerSupport { public: static std::new_handler set_new_handler(std::new_handler p) throw(); static void* operator new(std::size_t size) throw(std::bad_alloc); private: static std::new_handler currentHandler; }; template<typename T> std::new_handler NewHandlerSupport<T>::currentHandler = nullptr; template<typename T> std::new_handler NewHandlerSupport<T>::set_new_handler(std::new_handler p) throw() { std::new_handler oldHandler = currentHandler; currentHandler = p; return oldHandler; } template<typename T> void* NewHandlerSupport<T>::operator new(std::size_t size) throw(std::bad_alloc) { std::new_handler globalHandler = std::set_new_handler(currentHandler); void* memory; try { memory = ::operator new(size); } catch (...) { std::set_new_handler(globalHandler); throw; } std::set_new_handler(globalHandler); return memory; } class Widget : public NewHandlerSupport<Widget> { // Widget 클래스 구현 };
C++
복사
이제 Widget 클래스는 단순히 NewHandlerSupport<Widget>을 상속받음으로써 클래스별 new 처리자 기능을 얻을 수 있습니다.

nothrow new의 이해

C++에서는 "nothrow" 버전의 new도 제공합니다. 이는 메모리 할당 실패 시 예외를 던지는 대신 널 포인터를 반환합니다:
Widget* pw = new (std::nothrow) Widget; if (pw == nullptr) { // 메모리 할당 실패 처리 }
C++
복사
하지만 주의해야 할 점은, nothrow new가 제공하는 보장은 생각보다 제한적이라는 것입니다. 메모리 할당 자체는 예외를 던지지 않지만, 객체의 생성자에서 여전히 예외가 발생할 수 있습니다.

개인적 견해

new 처리자는 C++의 메모리 관리에 있어 강력한 도구이지만, 양날의 검과 같다고 생각합니다. 특히 서버 애플리케이션에서 이 기능은 매우 유용할 수 있습니다. 메모리 부족 상황에서 캐시를 정리하거나, 일부 연결을 종료하는 등의 작업을 수행하여 서버의 안정성을 높일 수 있기 때문입니다.
하지만 new 처리자를 부적절하게 사용하면 오히려 더 큰 문제를 야기할 수 있습니다. 예를 들어, new 처리자 내에서 복잡한 로직을 수행하거나 또 다른 메모리 할당을 시도한다면, 이는 시스템을 더욱 불안정하게 만들 수 있습니다. 따라서 new 처리자는 신중하게 설계되고 테스트되어야 합니다.
결국, new 처리자의 효과적인 사용은 개발자의 역량과 시스템에 대한 깊은 이해에 달려 있습니다. 이 기능을 활용할 때는 항상 전체 시스템의 안정성과 성능에 미치는 영향을 고려해야 합니다. 적절히 사용된다면 new 처리자는 프로그램의 견고성을 크게 향상시킬 수 있지만, 그렇지 않다면 오히려 디버깅하기 어려운 문제의 원인이 될 수 있음을 명심해야 합니다.

결론

new 처리자는 C++의 메모리 관리에 있어 강력하면서도 섬세한 도구입니다. 이를 제대로 이해하고 활용함으로써, 우리는 더욱 안정적이고 리소스 효율적인 프로그램을 작성할 수 있습니다. 그러나 이 기능의 잘못된 사용은 오히려 시스템을 불안정하게 만들 수 있습니다. 따라서 개발자는 new 처리자의 동작 원리를 깊이 이해하고, 시스템의 전반적인 안정성을 고려하며 신중하게 사용해야 합니다. 적절히 활용된다면 new 처리자는 특히 서버 애플리케이션이나 리소스 제한적인 환경에서 프로그램의 견고성과 신뢰성을 크게 향상시킬 수 있을 것입니다.