C++ RAII 완벽 가이드 | "Too many open files" 장애 원인과 리소스 자동 관리

C++ RAII 완벽 가이드 | "Too many open files" 장애 원인과 리소스 자동 관리

이 글의 핵심

파일·소켓·뮤텍스 누수로 서버가 다운됐나요? RAII 패턴으로 생성자·소멸자에 리소스 획득·해제를 묶어 예외·early return에도 안전하게. lock_guard·unique_ptr·파일 핸들·프로덕션 패턴까지.

들어가며: “파일을 열 수 없습니다” 장애

Too many open files — 리소스 누수의 공포

서비스 런칭 후 일주일, 갑자기 “Too many open files” 에러가 발생하며 서버가 멈췄습니다.

"로그 처리 중에 서버가 죽어요."
"lsof로 확인하니 열린 파일이 1024개예요. 시스템 한계에 도달했어요."

확인한 것:

$ lsof -p $(pgrep myserver) | wc -l
1024  # 열린 파일 개수 (시스템 한계!)

원인: 파일을 열기만 하고 닫지 않음. early return·예외 경로에서 fclose 누락.

파일 핸들, 소켓, 뮤텍스처럼 “획득 → 사용 → 해제”가 쌍을 이루는 리소스는, 예외나 early return 시에도 반드시 해제되어야 합니다. RAII(Resource Acquisition Is Initialization—리소스 획득은 초기화다)는 “리소스를 획득하는 순간(생성자)해제하는 순간(소멸자)을 묶어서, 스코프를 벗어나면 자동으로 해제되게 하는 패턴”입니다. std::lock_guard, std::unique_ptr, std::ifstream이 모두 이 패턴을 따릅니다.

비유하면: 자동문이 “열림 = 들어갈 때, 닫힘 = 나올 때”로 정해져 있어서 손으로 닫을 필요가 없듯이, RAII는 “열기 = 객체 생성, 닫기 = 객체 소멸”로 한 쌍을 맞춥니다.

이 글을 읽으면:

  • RAII 패턴의 원리와 장점을 이해할 수 있습니다.
  • 파일, 뮤텍스, 소켓 등 완전한 RAII 예제를 구현할 수 있습니다.
  • 자주 하는 실수와 해결법을 익힐 수 있습니다.
  • 프로덕션에서 바로 적용할 수 있는 패턴을 배울 수 있습니다.

목차

  1. 문제 시나리오
  2. RAII란 무엇인가?
  3. RAII 핵심 원칙
  4. 완전한 RAII 예제: 리소스 관리
  5. 완전한 RAII 예제: lock_guard
  6. 완전한 RAII 예제: 파일 핸들
  7. 자주 발생하는 에러와 해결법
  8. 모범 사례와 선택 가이드
  9. 프로덕션 패턴
  10. 성능·체크리스트

1. 문제 시나리오

시나리오 1: “파일을 닫지 않아서 서버가 죽어요”

"로그 파일 1만 개 처리하는 배치가 있는데, 10% 정도가 early return해요."
"fclose를 깜빡해서 파일 핸들이 쌓여 시스템 한계에 도달했어요."

상황: fopen으로 연 파일은 fclose로 닫아야 합니다. shouldStop()이 true일 때나 fopen 실패 시 return으로 나가면서 fclose를 호출하지 않으면 핸들이 누수됩니다.

해결 포인트: RAII 클래스로 “열기 = 생성자, 닫기 = 소멸자”를 묶으면, return이 어디서 나오든 스코프를 벗어날 때 소멸자가 fclose를 호출합니다.

시나리오 2: “뮤텍스 unlock을 깜빡해서 데드락이 나요”

"mtx.lock() 후 예외가 나면 unlock()이 호출되지 않아요."
"다른 스레드가 영원히 대기하게 됩니다."

상황: mtx.lock()logFile << msg에서 예외가 나면 mtx.unlock()까지 실행되지 않습니다.

해결 포인트: std::lock_guard<std::mutex> lock(mtx)를 사용하면 생성 시 lock, 소멸 시 자동 unlock이 보장됩니다.

시나리오 3: “소켓 close를 깜빡해서 EMFILE이 나요”

"connect() 실패나 타임아웃 시 close(sock)를 호출하지 않았어요."
"연결이 쌓여 'Too many open files'에 도달해요."

상황: socket()으로 fd를 얻은 뒤 connect() 실패 시 return하면 close(sock)가 호출되지 않습니다.

해결 포인트: Socket RAII 클래스로 소멸자에서 close(sockfd)를 호출하면 early return 시에도 안전합니다.

시나리오 4: “DB 트랜잭션 rollback을 깜빡해요”

"transfer() 중 예외가 나면 commit()이 호출되지 않아요."
"rollback()도 호출하지 않아서 커넥션이 풀에 반환되지 않아요."

상황: db.begin()commit() 전에 예외가 나면 rollback()이 호출되지 않아 커넥션 풀이 고갈됩니다.

해결 포인트: Transaction RAII 클래스로 소멸자에서 !committedrollback()을 호출하면 예외 안전합니다.

시나리오 5: “new로 할당한 메모리를 delete 깜빡해요”

"예외가 나면 delete까지 실행되지 않아요."
"Valgrind로 확인하니 메모리 누수가 쌓여 있어요."

상황: new로 할당한 후, 그 다음 줄에서 예외가 나면 delete까지 실행되지 않습니다.

해결 포인트: std::unique_ptr을 사용하면 스코프를 벗어날 때(예외 포함) 소멸자가 자동으로 delete를 호출합니다.

RAII 생명 주기 시각화

flowchart LR
  subgraph acquire["획득"]
    A1[객체 생성] --> A2[생성자: 리소스 획득]
  end
  subgraph use["사용"]
    B1[리소스 사용]
  end
  subgraph release["해제"]
    C1[스코프 종료] --> C2[소멸자: 리소스 해제]
  end
  A2 --> B1 --> C1 --> C2

2. RAII란 무엇인가?

RAII (Resource Acquisition Is Initialization)

정의: 리소스의 획득은 초기화다.

핵심 아이디어:

  • 생성자: 리소스 획득 (메모리, 파일, 소켓, 락 등)
  • 소멸자: 리소스 해제 (자동 호출 보장)

RAII의 장점

// ❌ RAII 없이 (수동 관리)
void badFunction() {
    FILE* file = fopen("data.txt", "r");
    std::mutex mtx;

    if (!file) return;  // ❌ 에러 처리 복잡

    mtx.lock();

    // ... 복잡한 로직 ...

    if (error1) {
        fclose(file);   // 잊기 쉬움!
        mtx.unlock();  // 잊기 쉬움!
        return;
    }

    if (error2) {
        fclose(file);   // 중복 코드!
        mtx.unlock();  // 중복 코드!
        return;
    }

    fclose(file);
    mtx.unlock();
}

// ✅ RAII 사용 (자동 관리)
void goodFunction() {
    std::ifstream file("data.txt");
    std::lock_guard<std::mutex> lock(mtx);

    if (!file) return;  // ✅ 자동으로 모두 해제!

    // ... 복잡한 로직 ...

    if (error1) return;  // ✅ 자동으로 모두 해제!
    if (error2) return;  // ✅ 자동으로 모두 해제!

    // ✅ 소멸자에서 자동으로 모두 해제!
}

위 코드 설명: badFunction에서는 fopen·mtx.lock()을 수동으로 호출한 뒤, error1·error2마다 fclosemtx.unlock()을 따로 호출해야 해서 누락·중복이 생깁니다. goodFunction에서는 std::ifstreamstd::lock_guard가 생성 시 리소스를 잡고, 스코프를 벗어날 때 소멸자에서 자동으로 닫고 풀어 주므로, return이 어디서 나와도 해제가 보장됩니다.


3. RAII 핵심 원칙

원칙 1: 생성자에서 리소스 획득

class Resource {
    int* data;

public:
    Resource(size_t size) {
        data = new int[size];  // 생성자에서 획득
        std::cout << "Resource acquired\n";
    }

    ~Resource() {
        delete[] data;  // 소멸자에서 해제
        std::cout << "Resource released\n";
    }
};

원칙 2: 소멸자에서 리소스 해제

{
    Resource res(100);  // 생성자 호출 → "Resource acquired"

    // 리소스 사용...

}  // 스코프 종료 → 소멸자 자동 호출 → "Resource released"

원칙 3: 복사/이동 의미 정의

class Resource {
    int* data;

public:
    // 복사 금지 (리소스는 하나만 소유)
    Resource(const Resource&) = delete;
    Resource& operator=(const Resource&) = delete;

    // 이동 허용 (소유권 이전)
    Resource(Resource&& other) noexcept : data(other.data) {
        other.data = nullptr;
    }

    Resource& operator=(Resource&& other) noexcept {
        if (this != &other) {
            delete[] data;
            data = other.data;
            other.data = nullptr;
        }
        return *this;
    }

    ~Resource() { delete[] data; }
};

위 코드 설명: 리소스 클래스는 “소유권이 하나”일 때 복사 금지·이동 허용으로 정리하는 경우가 많습니다. other.data = nullptr로 이중 해제를 방지합니다.


4. 완전한 RAII 예제: 리소스 관리

예제 1: Rule of Five를 따르는 리소스 클래스

#include <cstdio>
#include <stdexcept>

class FileHandle {
    FILE* file_ = nullptr;

public:
    explicit FileHandle(const char* filename, const char* mode) {
        file_ = fopen(filename, mode);
        if (!file_) throw std::runtime_error("Cannot open file");
    }

    ~FileHandle() noexcept {
        if (file_) {
            fclose(file_);
            file_ = nullptr;
        }
    }

    FileHandle(const FileHandle&) = delete;
    FileHandle& operator=(const FileHandle&) = delete;

    FileHandle(FileHandle&& other) noexcept : file_(other.file_) {
        other.file_ = nullptr;
    }

    FileHandle& operator=(FileHandle&& other) noexcept {
        if (this != &other) {
            if (file_) fclose(file_);
            file_ = other.file_;
            other.file_ = nullptr;
        }
        return *this;
    }

    FILE* get() const { return file_; }
};

int main() {
    FileHandle f("test.txt", "r");
    char buf[256];
    while (fgets(buf, sizeof(buf), f.get())) {
        printf("%s", buf);
    }
    return 0;  // ✅ 소멸자에서 자동 fclose
}

핵심: 소멸자 noexcept, if (this != &other) 자기 대입 방지, other.file_ = nullptr로 이중 해제 방지.

예제 2: unique_ptr로 RAII 메모리 관리

#include <memory>
#include <vector>

void processData() {
    // ✅ RAII: 스코프를 벗어나면 자동으로 delete
    auto buffer = std::make_unique<char[]>(4096);

    if (readFile("data.bin", buffer.get(), 4096) < 0) {
        return;  // ✅ 예외 없이 return해도 자동 해제
    }

    // ... 처리 ...
}

예제 3: 스코프 가드 (Scope Guard)

#include <functional>
#include <iostream>

class ScopeGuard {
    std::function<void()> on_exit_;
    bool active_ = true;

public:
    explicit ScopeGuard(std::function<void()> f) : on_exit_(std::move(f)) {}

    ~ScopeGuard() noexcept {
        if (active_ && on_exit_) on_exit_();
    }

    void release() { active_ = false; }
};

void example() {
    std::cout << "Enter\n";
    ScopeGuard guard([] { std::cout << "Exit\n"; });

    if (error) return;  // ✅ "Exit" 자동 출력

    guard.release();  // 명시적 해제 시 Exit 생략
}

5. 완전한 RAII 예제: lock_guard

lock_guard: 기본 뮤텍스 관리

#include <mutex>
#include <iostream>

std::mutex mtx;
int sharedCounter = 0;

// ❌ 수동 잠금/해제 (위험)
void badIncrement() {
    mtx.lock();

    sharedCounter++;

    if (error) {
        mtx.unlock();  // 잊기 쉬움!
        return;
    }

    mtx.unlock();
}

// ✅ RAII (안전)
void goodIncrement() {
    std::lock_guard<std::mutex> lock(mtx);

    sharedCounter++;

    if (error) {
        return;  // ✅ 자동으로 unlock!
    }
}

unique_lock: 유연한 뮤텍스 관리

#include <mutex>

void advancedLocking() {
    std::unique_lock<std::mutex> lock(mtx);

    // 작업 1
    doWork1();

    // 일시적으로 unlock (다른 스레드가 접근 가능)
    lock.unlock();
    doExpensiveWork();  // 락 없이 실행

    // 다시 lock
    lock.lock();

    // 작업 2
    doWork2();

    // ✅ 소멸자에서 자동으로 unlock
}

위 코드 설명: std::unique_locklock_guard처럼 RAII로 잠금/해제를 하지만, 중간에 lock.unlock()으로 잠시 풀었다가 lock.lock()으로 다시 잡을 수 있습니다.

scoped_lock: 다중 뮤텍스 데드락 방지

#include <mutex>

std::mutex mtx1, mtx2;

// ❌ 수동 잠금 → 데드락 위험
void thread1_bad() {
    mtx1.lock();
    mtx2.lock();  // thread2가 mtx2를 잡고 있으면 데드락!
    // ...
    mtx2.unlock();
    mtx1.unlock();
}

// ✅ scoped_lock: RAII + 데드락 방지
void thread1_good() {
    std::scoped_lock lock(mtx1, mtx2);  // 항상 같은 순서로 잠금
    // ...
}

위 코드 설명: std::scoped_lock(mtx1, mtx2)는 두 뮤텍스를 한 번에 잠그며, 내부적으로 항상 같은 순서(예: 주소 순)로 잠금을 걸어 데드락을 피합니다.

lock_guard vs unique_lock vs scoped_lock

타입특징사용 시점
lock_guard생성 시 lock, 소멸 시 unlock단순한 락
unique_lockunlock/lock 재호출 가능조건부 락, condition_variable
scoped_lock여러 뮤텍스 동시 잠금데드락 방지

6. 완전한 RAII 예제: 파일 핸들

C++ 표준 라이브러리 (권장)

#include <fstream>
#include <string>

void readFile(const std::string& filename) {
    std::ifstream file(filename);  // ✅ 생성자에서 파일 열기

    if (!file) {
        throw std::runtime_error("File not found");
        // ✅ 예외 발생해도 소멸자에서 자동으로 파일 닫힘
    }

    std::string line;
    while (std::getline(file, line)) {
        processLine(line);

        if (shouldStop()) {
            return;  // ✅ 소멸자에서 자동으로 파일 닫힘
        }
    }
}

C 스타일 FILE* RAII 래퍼

#include <cstdio>
#include <stdexcept>
#include <string>
#include <iostream>

class CFile {
    FILE* file = nullptr;
    std::string filename;

public:
    CFile(const std::string& fname, const char* mode) : filename(fname) {
        file = fopen(fname.c_str(), mode);
        if (!file) {
            throw std::runtime_error("Cannot open file: " + fname);
        }
        std::cout << "File opened: " << filename << "\n";
    }

    ~CFile() {
        if (file) {
            fclose(file);
            std::cout << "File closed: " << filename << "\n";
        }
    }

    CFile(const CFile&) = delete;
    CFile& operator=(const CFile&) = delete;

    FILE* get() { return file; }
};

void processFile() {
    CFile file("data.txt", "r");

    char buffer[1024];
    while (fgets(buffer, sizeof(buffer), file.get())) {
        if (error) return;  // ✅ 자동으로 파일 닫힘
    }
}

POSIX 소켓 RAII 클래스

#include <sys/socket.h>
#include <unistd.h>
#include <stdexcept>
#include <iostream>

class Socket {
    int sockfd = -1;

public:
    Socket(int domain, int type, int protocol) {
        sockfd = socket(domain, type, protocol);
        if (sockfd < 0) {
            throw std::runtime_error("Socket creation failed");
        }
        std::cout << "Socket created: " << sockfd << "\n";
    }

    ~Socket() {
        if (sockfd >= 0) {
            close(sockfd);
            std::cout << "Socket closed: " << sockfd << "\n";
        }
    }

    Socket(const Socket&) = delete;
    Socket& operator=(const Socket&) = delete;

    Socket(Socket&& other) noexcept : sockfd(other.sockfd) {
        other.sockfd = -1;
    }

    int get() const { return sockfd; }
};

void handleConnection(struct sockaddr* addr, socklen_t len) {
    Socket sock(AF_INET, SOCK_STREAM, 0);

    if (connect(sock.get(), addr, len) < 0) {
        return;  // ✅ 소멸자에서 자동으로 소켓 닫힘
    }

    // 데이터 송수신...
}

DB 트랜잭션 RAII 클래스

class Transaction {
    Database& db;
    bool committed = false;

public:
    Transaction(Database& database) : db(database) {
        db.begin();
        std::cout << "Transaction started\n";
    }

    ~Transaction() {
        if (!committed) {
            db.rollback();
            std::cout << "Transaction rolled back\n";
        }
    }

    void commit() {
        db.commit();
        committed = true;
        std::cout << "Transaction committed\n";
    }
};

void transferMoney(int from, int to, int amount) {
    Transaction txn(db);  // ✅ 트랜잭션 시작

    db.withdraw(from, amount);

    if (db.getBalance(from) < 0) {
        throw std::runtime_error("Insufficient funds");
        // ✅ 예외 발생 → 소멸자에서 자동 롤백!
    }

    db.deposit(to, amount);

    txn.commit();  // ✅ 명시적 커밋
}

7. 자주 발생하는 에러와 해결법

에러 1: 소멸자에서 예외 던지기

증상: std::terminate() 호출로 프로그램 비정상 종료.

원인: 소멸자에서 예외를 던지면 스택 언와인딩 중 추가 예외가 발생합니다.

// ❌ 위험
~BadFile() {
    if (fclose(f) != 0) throw std::runtime_error("Close failed");
}

// ✅ 소멸자에서는 예외를 던지지 않음
~GoodFile() noexcept {
    if (f && fclose(f) != 0) std::cerr << "fclose failed\n";
}

에러 2: 복사/이동 의미 정의 누락

증상: 리소스 이중 해제, undefined behavior.

원인: 복사를 허용하면 두 객체가 같은 리소스를 가리켜 소멸 시 두 번 해제됩니다.

// ❌ 기본 복사 → 얕은 복사 → 이중 해제
class Resource {
    int* data;

public:
    Resource(size_t n) : data(new int[n]) {}
    ~Resource() { delete[] data; }
};

// ✅ 복사 금지, 이동 허용
class Resource {
    int* data;

public:
    Resource(const Resource&) = delete;
    Resource& operator=(const Resource&) = delete;
    Resource(Resource&& other) noexcept : data(other.data) {
        other.data = nullptr;
    }
    ~Resource() { delete[] data; }
};

에러 3: 리소스 획득 실패 시 부분 해제 누락

증상: 생성자에서 여러 리소스 획득 중 예외 시 이미 획득한 리소스 누수.

해결법: std::unique_ptr 등으로 각 리소스를 RAII 객체로 감싸면 부분 실패 시에도 자동 해제됩니다.

// ❌ 위험: 두 번째 new에서 예외 시 첫 번째 delete 누락
class MultiResource {
    int* a;
    int* b;

public:
    MultiResource() {
        a = new int[100];
        b = new int[100];  // 예외 시 a 누수!
    }
};

// ✅ RAII: 각 리소스가 자동 해제
class MultiResource {
    std::unique_ptr<int[]> a{std::make_unique<int[]>(100)};
    std::unique_ptr<int[]> b{std::make_unique<int[]>(100)};
};

에러 4: lock_guard 범위 과대

증상: 불필요하게 긴 구간을 잠가 성능 저하, 데드락 위험 증가.

해결법: 락이 필요한 구간만 {} 블록으로 감싸 최소화합니다.

// ❌ 락 범위 과대
void process() {
    std::lock_guard<std::mutex> lock(mtx);
    auto data = loadData();   // 락 필요 없음
    transformData();           // 락 필요
    saveData();               // 락 필요 없음
}

// ✅ 락 범위 최소화
void process() {
    auto data = loadData();
    {
        std::lock_guard<std::mutex> lock(mtx);
        transformData();
    }
    saveData();
}

에러 5: RAII 객체를 힙에 동적 할당

증상: new로 만든 RAII 객체를 delete하지 않으면 리소스 누수.

해결법: RAII 객체는 스택에 두거나 std::unique_ptr로 관리합니다.

// ❌ 위험
void bad() {
    auto* file = new std::ifstream("data.txt");
    if (error) return;  // delete 누락!
    delete file;
}

// ✅ 스택에 두기
void good() {
    std::ifstream file("data.txt");
    if (error) return;
}

에러 6: lock 순서 불일치로 데드락

증상: 두 스레드가 서로 다른 순서로 뮤텍스를 잡아 데드락.

해결법: std::scoped_lock으로 한 번에 잠그거나, 모든 스레드에서 동일한 순서로 잠금합니다.

// ❌ 데드락: thread1은 mtx1→mtx2, thread2는 mtx2→mtx1
void thread1() {
    mtx1.lock();
    mtx2.lock();
    // ...
}

void thread2() {
    mtx2.lock();
    mtx1.lock();
    // ...
}

// ✅ scoped_lock: 항상 같은 순서
void thread1() {
    std::scoped_lock lock(mtx1, mtx2);
}

8. 모범 사례와 선택 가이드

원칙 1: 표준 라이브러리 우선

std::ifstream, std::lock_guard, std::unique_ptr 등 표준 RAII 타입을 우선 사용합니다.

원칙 2: 리소스 획득은 생성자, 해제는 소멸자

한 곳에서 획득하고 한 곳에서 해제하여 “획득-해제” 쌍을 명확히 합니다.

원칙 3: 소멸자는 noexcept

소멸자에서 예외를 던지지 않도록 하고, noexcept를 명시합니다.

원칙 4: 복사/이동 의미 명시

단일 소유권이면 복사 금지·이동 허용, 공유 리소스면 shared_ptr 등을 고려합니다.

원칙 5: 리소스 획득 실패 시 예외

생성자에서 실패 시 예외를 던져 유효하지 않은 객체가 생성되지 않게 합니다.

RAII 체크리스트

  • 생성자에서 리소스 획득
  • 소멸자에서 리소스 해제 (noexcept)
  • 복사/이동 의미 정의 (delete 또는 구현)
  • 표준 라이브러리 타입 우선 사용
  • 리소스 획득 실패 시 예외
  • 소멸자에서 예외 던지지 않음

리소스 타입별 선택 가이드

리소스권장 RAII비고
메모리unique_ptr, shared_ptrraw 포인터·delete 금지
파일std::ifstream, std::ofstreamC API는 FILE* 래퍼
뮤텍스lock_guard, unique_lock, scoped_lock수동 lock/unlock 금지
소켓커스텀 Socket 클래스close() 소멸자에서
DB 연결커스텀 ConnectionGuard풀 반환 소멸자에서

9. 프로덕션 패턴

패턴 1: 연결 풀 + RAII

class ConnectionGuard {
    ConnectionPool& pool_;
    Connection* conn_ = nullptr;

public:
    explicit ConnectionGuard(ConnectionPool& pool) : pool_(pool) {
        conn_ = pool_.acquire();
    }

    ~ConnectionGuard() noexcept {
        if (conn_) pool_.release(conn_);
    }

    ConnectionGuard(const ConnectionGuard&) = delete;
    ConnectionGuard& operator=(const ConnectionGuard&) = delete;

    Connection* get() const { return conn_; }
};

void processQuery() {
    ConnectionGuard conn(pool);
    conn.get()->execute("SELECT ...");
    // ✅ 소멸자에서 풀에 반환
}

패턴 2: 로그 스코프 (진입/퇴출 자동 로깅)

#include <chrono>
#include <iostream>
#include <string>

class LogScope {
    std::string name_;
    std::chrono::steady_clock::time_point start_;

public:
    explicit LogScope(const std::string& name)
        : name_(name), start_(std::chrono::steady_clock::now()) {
        std::cout << "ENTER " << name_ << "\n";
    }

    ~LogScope() {
        auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
            std::chrono::steady_clock::now() - start_)
            .count();
        std::cout << "EXIT " << name_ << " " << ms << "ms\n";
    }
};

void processRequest() {
    LogScope scope("processRequest");
    // ... 처리 ...
    // ✅ 소멸자에서 "EXIT processRequest 123ms" 출력
}

패턴 3: 스코프 트랜잭션 (예외 시 자동 롤백)

class ScopedTransaction {
    Database& db_;
    bool committed_ = false;

public:
    explicit ScopedTransaction(Database& db) : db_(db) { db_.begin(); }

    ~ScopedTransaction() noexcept {
        if (!committed_) db_.rollback();
    }

    void commit() {
        db_.commit();
        committed_ = true;
    }
};

패턴 4: Pimpl + unique_ptr

class Widget {
    struct Impl;
    std::unique_ptr<Impl> pimpl_;

public:
    Widget();
    ~Widget();  // unique_ptr이 Impl 자동 삭제
    Widget(Widget&&) = default;
    Widget& operator=(Widget&&) = default;
};

패턴 5: 타이머 RAII (구간 시간 측정)

#include <chrono>
#include <iostream>

class Timer {
    std::string name_;
    std::chrono::high_resolution_clock::time_point start_;

public:
    explicit Timer(const std::string& name) : name_(name) {
        start_ = std::chrono::high_resolution_clock::now();
    }

    ~Timer() {
        auto end = std::chrono::high_resolution_clock::now();
        auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
            end - start_)
            .count();
        std::cout << name_ << " took " << ms << "ms\n";
    }
};

void processData() {
    Timer timer("processData");
    // ... 복잡한 작업 ...
    // ✅ 소멸자에서 "processData took 1234ms" 출력
}

프로덕션 체크리스트

  • 모든 리소스를 RAII로 관리
  • 수동 close(), unlock() 호출 제거
  • 예외 발생 시에도 안전한지 확인
  • 소멸자는 noexcept (예외 던지지 않음)
  • 복사/이동 의미 명확히 정의
  • 표준 라이브러리 타입 우선 사용

10. 성능·체크리스트

lock_guard 범위 최소화

// ✅ 락이 필요한 구간만 잠금
void process() {
    auto data = loadData();  // 락 없음
    {
        std::lock_guard<std::mutex> lock(mtx);
        updateSharedState(data);
    }
    saveData();  // 락 없음
}

예외 안전성 수준

수준설명예제
No-throw예외 절대 발생 안 함소멸자, swap
Strong예외 발생 시 상태 변경 안 됨트랜잭션
Basic예외 발생 시 리소스 누수 없음RAII
No guarantee예외 발생 시 정의되지 않음레거시 코드

RAII로 Strong 보장 구현 (Copy-and-Swap)

class Data {
    std::unique_ptr<int[]> buffer;
    size_t size;

public:
    Data(size_t s) : buffer(std::make_unique<int[]>(s)), size(s) {}

    void swap(Data& other) noexcept {
        std::swap(buffer, other.buffer);
        std::swap(size, other.size);
    }

    Data& operator=(const Data& other) {
        Data temp(other);  // 복사 (예외 발생 가능)
        swap(temp);        // swap (예외 없음)
        return *this;
    }
};

같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

  • C++ RAII | “파일을 열 수 없습니다” 장애의 원인과 자동 리소스 관리
  • C++ 스마트 포인터 기초 완벽 가이드 | unique_ptr·shared_ptr
  • C++ 예외 안전성 | “예외 발생 시 리소스 누수” Basic·Strong·Nothrow 보장
  • C++ 메모리 누수 | 서버 다운시킨 실제 사례와 Valgrind로 찾는 5가지 패턴

이 글에서 다루는 키워드 (관련 검색어)

C++ RAII, 리소스 관리, 생성자 소멸자, 스마트 포인터 원리, 예외 안전, lock_guard, 자동 해제, 파일 핸들 관리, unique_ptr 등으로 검색하시면 이 글이 도움이 됩니다.

정리

항목설명
RAII생성자에서 획득, 소멸자에서 해제
lock_guard생성 시 lock, 소멸 시 unlock
scoped_lock다중 뮤텍스 데드락 방지
unique_ptr메모리 RAII
ifstream/ofstream파일 RAII

핵심 원칙:

  1. 리소스는 항상 RAII로 관리
  2. 수동 close(), unlock() 호출 제거
  3. 소멸자는 noexcept
  4. 복사/이동 의미 명시
  5. 표준 라이브러리 우선 사용

자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. 파일, 소켓, 뮤텍스, DB 연결, 메모리 등 모든 리소스를 다룰 때 RAII를 적용하면 예외·early return 시에도 누수를 막을 수 있습니다. lock_guard, unique_ptr, ifstream이 대표 예입니다.

Q. 선행으로 읽으면 좋은 글은?

A. 메모리 누수, 스마트 포인터, 예외 안전성을 먼저 읽으면 RAII의 배경을 이해하기 쉽습니다.

Q. 더 깊이 공부하려면?

A. cppreference RAII, C++ Core Guidelines R 시리즈, Effective Modern C++을 참고하세요.

한 줄 요약: 생성자에서 획득·소멸자에서 해제하는 RAII로 리소스 누수를 막을 수 있습니다. 다음으로 스레드 기초(#7-1)를 읽어보면 좋습니다.

이전 글: [C++ 실전 가이드 #19-3] PIMPL·Bridge: 구현 숨기기와 ABI 안정성

다음 글: [C++ 실전 가이드 #21-1] HTTP 클라이언트: 요청·응답 처리

참고 자료


관련 글

  • C++ 디자인 패턴 | Observer·Strategy
  • C++ 디자인 패턴 종합 가이드 | Singleton·Factory
  • C++ RAII |
  • C++ 디자인 패턴 | Singleton·Factory·Builder·Prototype 생성 패턴 가이드
  • C++ 이동 의미론 완벽 가이드 | rvalue 참조·std::move