C++ RAII 패턴 | "리소스 관리" 완벽 가이드

C++ RAII 패턴 | "리소스 관리" 완벽 가이드

이 글의 핵심

C++ RAII 패턴에 대한 실전 가이드입니다.

RAII란?

C++에서 객체 수명과 리소스를 묶는 흐름은 종합 패턴 가이드·시리즈 RAII 심화와 연결해 보면 실무 맥락이 잡힙니다.

Resource Acquisition Is Initialization

  • 리소스 획득은 초기화다
  • 생성자에서 리소스 획득
  • 소멸자에서 리소스 해제
// ❌ 수동 관리 (위험)
void badExample() {
    int* ptr = new int(10);
    // ... 복잡한 로직 ...
    if (error) return;  // 메모리 누수!
    delete ptr;
}

// ✅ RAII (안전)
void goodExample() {
    unique_ptr<int> ptr = make_unique<int>(10);
    // ... 복잡한 로직 ...
    if (error) return;  // 자동으로 delete됨!
}  // 자동으로 delete됨!

기본 RAII 클래스

class FileHandler {
private:
    FILE* file;
    
public:
    // 생성자: 리소스 획득
    FileHandler(const char* filename) {
        file = fopen(filename, "w");
        if (!file) {
            throw runtime_error("파일 열기 실패");
        }
        cout << "파일 열림" << endl;
    }
    
    // 소멸자: 리소스 해제
    ~FileHandler() {
        if (file) {
            fclose(file);
            cout << "파일 닫힘" << endl;
        }
    }
    
    // 복사 방지
    FileHandler(const FileHandler&) = delete;
    FileHandler& operator=(const FileHandler&) = delete;
    
    void write(const char* data) {
        fprintf(file, "%s\n", data);
    }
};

int main() {
    try {
        FileHandler fh("output.txt");
        fh.write("Hello");
        fh.write("World");
    } catch (exception& e) {
        cerr << e.what() << endl;
    }
    // 자동으로 파일 닫힘
}

스마트 포인터 (RAII의 대표 예)

unique_ptr

#include <memory>

class Resource {
public:
    Resource() { cout << "리소스 생성" << endl; }
    ~Resource() { cout << "리소스 해제" << endl; }
    void use() { cout << "리소스 사용" << endl; }
};

void process() {
    auto res = make_unique<Resource>();
    res->use();
    
    if (someCondition) {
        return;  // 자동 해제
    }
    
    // 예외 발생해도 자동 해제
    throw runtime_error("에러");
}  // 자동 해제

shared_ptr

class Data {
public:
    Data() { cout << "Data 생성" << endl; }
    ~Data() { cout << "Data 해제" << endl; }
};

void shareData() {
    auto data = make_shared<Data>();
    
    {
        auto data2 = data;  // 참조 카운트 증가
        cout << "참조 카운트: " << data.use_count() << endl;  // 2
    }  // data2 소멸, 참조 카운트 감소
    
    cout << "참조 카운트: " << data.use_count() << endl;  // 1
}  // data 소멸, Data 해제

lock_guard (뮤텍스 RAII)

#include <mutex>
#include <thread>

mutex mtx;
int counter = 0;

void increment() {
    // ❌ 수동 lock/unlock
    mtx.lock();
    counter++;
    if (error) {
        // mtx.unlock();  // 깜빡하면 데드락!
        return;
    }
    mtx.unlock();
}

void incrementSafe() {
    // ✅ RAII
    lock_guard<mutex> lock(mtx);  // 자동 lock
    counter++;
    if (error) {
        return;  // 자동 unlock
    }
}  // 자동 unlock

실전 예시

예시 1: 데이터베이스 연결

class DatabaseConnection {
private:
    void* connection;
    
public:
    DatabaseConnection(const string& connectionString) {
        connection = openConnection(connectionString);
        if (!connection) {
            throw runtime_error("DB 연결 실패");
        }
        cout << "DB 연결됨" << endl;
    }
    
    ~DatabaseConnection() {
        if (connection) {
            closeConnection(connection);
            cout << "DB 연결 종료" << endl;
        }
    }
    
    void execute(const string& query) {
        // 쿼리 실행
    }
};

void processData() {
    DatabaseConnection db("localhost:5432");
    db.execute("SELECT * FROM users");
    // 예외 발생해도 자동으로 연결 종료
}

예시 2: 타이머

#include <chrono>

class Timer {
private:
    chrono::time_point<chrono::high_resolution_clock> start;
    string name;
    
public:
    Timer(const string& n) : name(n) {
        start = chrono::high_resolution_clock::now();
        cout << name << " 시작" << endl;
    }
    
    ~Timer() {
        auto end = chrono::high_resolution_clock::now();
        auto duration = chrono::duration_cast<chrono::milliseconds>(end - start);
        cout << name << " 완료: " << duration.count() << "ms" << endl;
    }
};

void expensiveOperation() {
    Timer timer("expensiveOperation");
    
    // 복잡한 작업...
    this_thread::sleep_for(chrono::seconds(1));
    
}  // 자동으로 시간 측정

예시 3: 상태 복원

class StateGuard {
private:
    int& state;
    int oldState;
    
public:
    StateGuard(int& s, int newState) : state(s), oldState(s) {
        state = newState;
        cout << "상태 변경: " << oldState << " -> " << newState << endl;
    }
    
    ~StateGuard() {
        state = oldState;
        cout << "상태 복원: " << oldState << endl;
    }
};

void temporaryStateChange() {
    int globalState = 0;
    
    {
        StateGuard guard(globalState, 1);
        // globalState는 1
        cout << "현재 상태: " << globalState << endl;
    }  // 자동으로 0으로 복원
    
    cout << "복원된 상태: " << globalState << endl;
}

예시 4: 스코프 가드

template<typename F>
class ScopeGuard {
private:
    F func;
    bool active;
    
public:
    ScopeGuard(F f) : func(f), active(true) {}
    
    ~ScopeGuard() {
        if (active) {
            func();
        }
    }
    
    void dismiss() {
        active = false;
    }
};

template<typename F>
ScopeGuard<F> makeScopeGuard(F f) {
    return ScopeGuard<F>(f);
}

void processFile() {
    FILE* file = fopen("data.txt", "r");
    auto guard = makeScopeGuard([file]() {
        if (file) {
            fclose(file);
            cout << "파일 닫힘" << endl;
        }
    });
    
    // 파일 처리...
    if (error) {
        return;  // 자동으로 파일 닫힘
    }
    
    guard.dismiss();  // 성공 시 자동 닫기 취소
    // 수동으로 처리...
}

RAII 규칙

1. 생성자에서 리소스 획득

class Resource {
public:
    Resource() {
        // 리소스 획득
        data = new int[100];
    }
};

2. 소멸자에서 리소스 해제

class Resource {
public:
    ~Resource() {
        // 리소스 해제
        delete[] data;
    }
};

3. 복사 방지 또는 구현

class Resource {
public:
    // 복사 방지
    Resource(const Resource&) = delete;
    Resource& operator=(const Resource&) = delete;
    
    // 또는 이동만 허용
    Resource(Resource&& other) noexcept {
        data = other.data;
        other.data = nullptr;
    }
};

자주 발생하는 문제

문제 1: 생성자에서 예외

// ❌ 위험
class Bad {
private:
    int* data1;
    int* data2;
    
public:
    Bad() {
        data1 = new int[100];
        // 여기서 예외 발생하면?
        data2 = new int[100];  // data1 누수!
    }
};

// ✅ 안전
class Good {
private:
    unique_ptr<int[]> data1;
    unique_ptr<int[]> data2;
    
public:
    Good() {
        data1 = make_unique<int[]>(100);
        data2 = make_unique<int[]>(100);  // 예외 발생해도 data1 자동 해제
    }
};

문제 2: 소멸자에서 예외

// ❌ 위험
class Bad {
public:
    ~Bad() {
        throw runtime_error("에러");  // 절대 안됨!
    }
};

// ✅ 안전
class Good {
public:
    ~Good() noexcept {
        try {
            // 위험한 작업
        } catch (...) {
            // 예외 삼킴
        }
    }
};

문제 3: 복사 시 리소스 중복

// ❌ 얕은 복사
class Bad {
private:
    int* data;
    
public:
    Bad() : data(new int(10)) {}
    ~Bad() { delete data; }
    // 복사 생성자 없음 → double delete!
};

// ✅ 복사 방지
class Good {
private:
    int* data;
    
public:
    Good() : data(new int(10)) {}
    ~Good() { delete data; }
    
    Good(const Good&) = delete;
    Good& operator=(const Good&) = delete;
};

FAQ

Q1: RAII는 왜 중요한가요?

A:

  • 예외 안전성
  • 메모리 누수 방지
  • 코드 간결화
  • 자동 리소스 관리

Q2: 모든 리소스에 RAII를 사용해야 하나요?

A: 네, 가능하면 모든 리소스(메모리, 파일, 소켓, 뮤텍스 등)에 RAII를 사용하세요.

Q3: RAII vs try-finally?

A: C++에는 finally가 없습니다. RAII가 더 안전하고 간결합니다.

Q4: 성능 오버헤드는?

A: 거의 없습니다. 컴파일러가 최적화합니다.

Q5: 언제 RAII를 만들어야 하나요?

A:

  • 수동 해제가 필요한 리소스
  • 예외 안전성이 중요한 경우
  • 상태 복원이 필요한 경우

Q6: RAII 학습 리소스는?

A:

  • “Effective C++” (Scott Meyers)
  • “C++ Coding Standards” (Sutter & Alexandrescu)
  • STL 스마트 포인터 소스 코드

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

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

  • C++ RAII & Smart Pointers | “스마트 포인터” 가이드
  • C++ RAII 완벽 가이드 | “Too many open files” 장애 원인과 리소스 자동 관리
  • C++ RAII | “파일을 열 수 없습니다” 장애의 원인과 자동 리소스 관리

관련 글

  • C++ RAII |
  • C++ RAII 완벽 가이드 |
  • C++ 메모리 정렬 |
  • C++ Allocator |
  • C++ shared_ptr vs unique_ptr |