24시간 무중단으로 수많은 주문과 시세 데이터를 처리해야 하는 증권사 HTS/MTS 서버나 클라이언트 시스템에서 메모리 누수(Memory Leak)는 단순한 버그를 넘어 서비스 중단으로 이어질 수 있는 치명적인 결함입니다.

과거 C++03 시절에는 new로 동적 할당한 후 예외가 발생하거나 복잡한 조건문分岐 속에서 delete를 누락하여 메모리가 새어나가는 일이 흔했습니다.

하지만 모던 C++(C++11 이상)에서는 RAII(Resource Acquisition Is Initialization) 원칙을 기반으로 하는 스마트 포인터(Smart Pointer)를 활용해 '메모리 누수 제로'를 원칙적으로 보장할 수 있습니다. 오늘은 HTS/MTS 실무 관점에서 각 스마트 포인터의 활용법과 주의사항을 정리합니다.


1. 스마트 포인터의 대전제: RAII 원칙

📌 RAII (Resource Acquisition Is Initialization) 원칙이란?
자원(힙 메모리, 소켓, File Handle 등)의 소유권을 스택(Stack) 기반 객체에 귀속시키는 패턴입니다. 스택 객체가 자신이 속한 스코프(Scope)를 벗어나 소멸될 때, 디스트럭터(Destructor)가 자동으로 호출되어 힙 메모리를 안전하게 해제합니다.

2. std::unique_ptr: 독점적 소유권 (Exclusive Ownership)

실무에서 가장 먼저 고려해야 할 기본 스마트 포인터는 std::unique_ptr입니다. 말 그대로 특정 힙 메모리 자원에 대해 **단 하나의 객체만이 소유권**을 갖습니다.

⚡ Zero-Overhead Abstraction:
std::unique_ptr는 날포인터(Raw Pointer)와 동일한 메모리 크기(64bit 환경에서 8byte)와 성능을 가집니다. 오버헤드가 전혀 없으므로 기본 스마트 포인터로 적극 활용해야 합니다.

기본 사용 및 이동 시맨틱 (Move Semantics)

// [unique_ptr 활용 예시]
struct OrderPacket {
    long long orderId;
    int price;
};

void ProcessOrder() {
    // 1. std::make_unique를 사용한 안전한 생성 (C++14)
    auto pOrder = std::make_unique<OrderPacket>();
    pOrder->orderId = 100234;
    pOrder->price = 75000;

    // 2. 복사 금지 (컴파일 에러)
    // std::unique_ptr<OrderPacket> pCopy = pOrder; 

    // 3. std::move를 통한 소유권 이전 (Ownership Transfer)
    std::vector<std::unique_ptr<OrderPacket>> orderQueue;
    orderQueue.push_back(std::move(pOrder)); // pOrder는 이제 nullptr가 됨
} // 함수 스코프 종료 시 orderQueue 내부의 객체들이 자동으로 delete됨!

3. std::shared_ptr & std::weak_ptr: 공유 소유권 및 순환 참조 방지

여러 모듈(예: 체결 관리자, 화면 UI 렌더러, 통신 모듈)이 동일한 자원을 동시에 참조해야 할 때는 참조 카운팅(Reference Counting) 기반의 std::shared_ptr를 사용합니다.

std::make_shared를 써야 하는 이유

// ❌ 나쁜 예: 동적 객체 메모리 할당과 제어 블록(Control Block) 메모리 할당이 2번 일어남
std::shared_ptr<Session> pSession(new Session());

// ⭕ 좋은 예: 객체 메모리와 제어 블록을 단 1회의 메모리 할당으로 처리 (속도 및 캐시 효율 우수)
auto pSession = std::make_shared<Session>();

순환 참조(Circular Reference) 문제와 std::weak_ptr

두 객체가 서로를 std::shared_ptr로 참조하면, 참조 카운트가 영원히 0이 되지 않아 **메모리 누수가 발생**합니다. 이를 해결하기 위해 약한 참조(Weak Reference)인 std::weak_ptr를 사용합니다.

🚨 순환 참조 해결 구조:
부모 ➔ 자식 방향은 std::shared_ptr로 소유권을 유지하고,
자식 ➔ 부모 방향은 std::weak_ptr로 관찰권만 갖도록 설계해야 합니다.
// [weak_ptr 안전한 접근 방법]
std::weak_ptr<Session> wSession = g_pSharedSession;

// weak_ptr는 직접 접근 불가능 -> lock()을 호출해 temporary shared_ptr 취득
if (auto pSession = wSession.lock()) {
    // pSession이 유효한 동안 안전하게 메모리 접근 가능
    pSession->SendHeartbeat();
} else {
    // 이미 대상 메모리가 소멸됨
}

4. HTS/MTS 성능을 위한 스마트 포인터 가이드라인

📋 실무 가이드라인 요약:
  1. 기본 선택은 std::unique_ptr: 오버헤드가 없으므로 특별한 이유가 없다면 독점 소유권을 우선 적용합니다.
  2. 함수 인자 전달 시: 소유권 자체를 넘길 때만 std::unique_ptr를 가치로 전달(Move)하고, 단지 참조만 할 때는 날 포인터(Raw Pointer)나 C++ 참조자(Reference)를 인자로 전달합니다. (불필요한 shared_ptr 복사는 원자적 참조 카운트 증감 연산 오버헤드를 발생시킵니다.)
  3. 생성자 사용 금지: new T() 대신 항상 std::make_unique 및 std::make_shared를 사용합니다.

5. 마무리

스마트 포인터는 C++ 개발자가 수동 delete를 잊어버려 발생하는 메모리 누수와 Dangling Pointer 위험을 깔끔하게 차단해 줍니다.

소유권의 주체를 명확히 구분하여 `unique_ptr`와 `shared_ptr`, 그리고 `weak_ptr`를 적재적소에 사용하는 습관을 들이면 무결점 HTS/MTS 시스템을 구축하실 수 있습니다.