C++ RAII & 리소스 관리
개요 — RAII란
Section titled “개요 — RAII란”**RAII(Resource Acquisition Is Initialization)**는 C++의 핵심 자원 관리 이디엄입니다. 자원 획득을 객체 **초기화(생성자)**에, 자원 해제를 객체 **소멸(소멸자)**에 묶어 스코프가 끝나면 자동으로 자원이 반환되도록 합니다.
// RAII 없이 — 예외 발생 시 리소스 누수void BadFunction(){ FILE* File = fopen("data.txt", "r"); // 획득 int* Buffer = new int[1024]; // 획득
DoRiskyWork(); // 예외 발생하면?
delete[] Buffer; // 도달 안 함 → 누수 fclose(File); // 도달 안 함 → 누수}
// RAII 적용 — 예외가 발생해도 소멸자가 반드시 실행void GoodFunction(){ std::ifstream File("data.txt"); // 소멸 시 자동 close std::unique_ptr<int[]> Buffer(new int[1024]); // 소멸 시 자동 delete[]
DoRiskyWork(); // 예외 발생해도 OK
// 스코프 종료 → Buffer, File 소멸자 자동 호출}RAII가 자원 관리를 보장하는 전제: 소멸자는 예외 없이 반드시 실행된다 (스택 언와인딩)
1. 예외 안전성 보장 수준
Section titled “1. 예외 안전성 보장 수준”| 수준 | 설명 | 달성 방법 |
|---|---|---|
| No-throw | 절대 예외를 던지지 않음 | noexcept 소멸자, 스왑 |
| Strong | 예외 발생 시 원래 상태 완전 복원 | Copy-and-Swap 패턴 |
| Basic | 예외 발생 시 유효한 상태 유지 (값은 변할 수 있음) | RAII 기본 적용 |
| None | 예외 발생 시 상태 보장 없음 | 피해야 함 |
2. 표준 RAII 래퍼
Section titled “2. 표준 RAII 래퍼”2.1 std::unique_ptr — 단독 소유
Section titled “2.1 std::unique_ptr — 단독 소유”#include <memory>
// new/delete 대체std::unique_ptr<int> Ptr = std::make_unique<int>(42);std::cout << *Ptr; // 42// 스코프 종료 시 자동 delete
// 배열std::unique_ptr<int[]> Arr = std::make_unique<int[]>(100);Arr[0] = 1;// 스코프 종료 시 자동 delete[]
// 소유권 이전 (복사 불가, 이동만 가능)std::unique_ptr<int> Moved = std::move(Ptr);// Ptr는 nullptr, Moved가 소유
// 커스텀 삭제자auto FileDeleter = [](FILE* F) { if (F) fclose(F); };std::unique_ptr<FILE, decltype(FileDeleter)> FilePtr(fopen("data.txt", "r"), FileDeleter);2.2 std::shared_ptr — 공유 소유
Section titled “2.2 std::shared_ptr — 공유 소유”// 참조 카운팅 — 마지막 shared_ptr 소멸 시 deletestd::shared_ptr<int> SP1 = std::make_shared<int>(42);std::shared_ptr<int> SP2 = SP1; // 참조 카운트 2
std::cout << SP1.use_count(); // 2
SP1.reset(); // 참조 카운트 1 (SP2만 남음)std::cout << SP2.use_count(); // 1// SP2 소멸 시 delete2.3 std::weak_ptr — 약한 참조
Section titled “2.3 std::weak_ptr — 약한 참조”순환 참조를 깨거나, 소유 없이 관찰할 때 사용합니다.
struct Node{ int Value; std::shared_ptr<Node> Next; std::weak_ptr<Node> Prev; // 순환 참조 방지};
// weak_ptr 사용std::shared_ptr<int> SP = std::make_shared<int>(42);std::weak_ptr<int> WP = SP;
// 실제 접근 시 lock()으로 shared_ptr 획득if (auto Locked = WP.lock()){ std::cout << *Locked; // 42}else{ std::cout << "객체 소멸됨";}3. 커스텀 RAII 래퍼 작성
Section titled “3. 커스텀 RAII 래퍼 작성”표준 라이브러리가 제공하지 않는 자원(OS 핸들, 네트워크 소켓, GPU 리소스 등)을 위한 RAII 클래스를 직접 작성합니다.
3.1 파일 핸들 래퍼
Section titled “3.1 파일 핸들 래퍼”class FileHandle{public: explicit FileHandle(const char* Path, const char* Mode) : Handle(fopen(Path, Mode)) { if (!Handle) { throw std::runtime_error(std::string("파일 열기 실패: ") + Path); } }
~FileHandle() noexcept { if (Handle) { fclose(Handle); // 소멸자에서 반드시 해제 } }
// 복사 금지 (파일 핸들을 복사하면 의미가 없음) FileHandle(const FileHandle&) = delete; FileHandle& operator=(const FileHandle&) = delete;
// 이동은 허용 FileHandle(FileHandle&& Other) noexcept : Handle(Other.Handle) { Other.Handle = nullptr; // 원본은 무효화 }
FileHandle& operator=(FileHandle&& Other) noexcept { if (this != &Other) { if (Handle) fclose(Handle); Handle = Other.Handle; Other.Handle = nullptr; } return *this; }
FILE* Get() const noexcept { return Handle; } bool IsValid() const noexcept { return Handle != nullptr; }
// POSIX-style read/write 래핑 size_t Read(void* Buffer, size_t Size) const { return fread(Buffer, 1, Size, Handle); }
private: FILE* Handle;};
// 사용void ReadFile(const char* Path){ FileHandle File(Path, "rb"); // 생성자에서 open
char Buffer[256]; File.Read(Buffer, sizeof(Buffer));
// 스코프 종료 시 자동 fclose}3.2 뮤텍스 RAII 래퍼 (직접 구현 예시)
Section titled “3.2 뮤텍스 RAII 래퍼 (직접 구현 예시)”// std::lock_guard와 동일한 원리template<typename Mutex>class LockGuard{public: explicit LockGuard(Mutex& M) : MutexRef(M) { MutexRef.lock(); } ~LockGuard() noexcept { MutexRef.unlock(); }
LockGuard(const LockGuard&) = delete; LockGuard& operator=(const LockGuard&) = delete;
private: Mutex& MutexRef;};3.3 범용 스코프 가드
Section titled “3.3 범용 스코프 가드”임의의 정리 작업을 스코프 종료에 묶는 범용 도구입니다.
class ScopeGuard{public: template<typename Func> explicit ScopeGuard(Func&& CleanupFn) : Cleanup(std::forward<Func>(CleanupFn)) , bActive(true) {}
~ScopeGuard() noexcept { if (bActive) { Cleanup(); } }
// 조건부 비활성화 — 성공 시 정리 취소 void Dismiss() noexcept { bActive = false; }
ScopeGuard(const ScopeGuard&) = delete; ScopeGuard& operator=(const ScopeGuard&) = delete;
private: std::function<void()> Cleanup; bool bActive;};
// 사용 예시void TransactionalOperation(){ StartTransaction(); ScopeGuard Guard([]() { RollbackTransaction(); }); // 실패 시 롤백
DoRiskyWork1(); DoRiskyWork2();
CommitTransaction(); Guard.Dismiss(); // 성공 — 롤백 취소}4. Copy-and-Swap 패턴 — 강한 예외 안전성
Section titled “4. Copy-and-Swap 패턴 — 강한 예외 안전성”class Buffer{public: explicit Buffer(size_t Size) : Data(new int[Size]) , Size(Size) {}
~Buffer() { delete[] Data; }
Buffer(const Buffer& Other) : Data(new int[Other.Size]) // 여기서 예외 → 원본 unchanged , Size(Other.Size) { std::copy(Other.Data, Other.Data + Size, Data); }
// Copy-and-Swap: 강한 예외 안전성 보장 Buffer& operator=(Buffer Other) // 값으로 받아 복사 생성 (여기서 예외 가능) { Swap(Other); // noexcept — 절대 실패 안 함 return *this; // Other(원래 this의 데이터)는 소멸자에서 정리 }
Buffer(Buffer&& Other) noexcept : Data(Other.Data) , Size(Other.Size) { Other.Data = nullptr; Other.Size = 0; }
void Swap(Buffer& Other) noexcept { std::swap(Data, Other.Data); std::swap(Size, Other.Size); }
private: int* Data; size_t Size;};5. 실전 패턴 — 트랜잭션 RAII
Section titled “5. 실전 패턴 — 트랜잭션 RAII”class DatabaseTransaction{public: explicit DatabaseTransaction(Database& DB) : DB(DB) , bCommitted(false) { DB.BeginTransaction(); }
~DatabaseTransaction() noexcept { if (!bCommitted) { DB.Rollback(); // 커밋 없이 소멸 → 자동 롤백 } }
void Commit() { DB.Commit(); bCommitted = true; }
DatabaseTransaction(const DatabaseTransaction&) = delete; DatabaseTransaction& operator=(const DatabaseTransaction&) = delete;
private: Database& DB; bool bCommitted;};
// 사용void UpdateUserScore(Database& DB, int UserId, int Score){ DatabaseTransaction Tx(DB);
DB.Execute("UPDATE users SET score = ? WHERE id = ?", Score, UserId); DB.Execute("INSERT INTO score_log VALUES (?, ?, NOW())", UserId, Score);
Tx.Commit(); // 여기에 도달하지 못하면 자동 롤백}6. RAII와 예외 처리 통합
Section titled “6. RAII와 예외 처리 통합”// 소멸자에서 예외 던지기 — 절대 금지class BadRAII{public: ~BadRAII() { CleanupResource(); // 예외를 던질 수 있다면? // 스택 언와인딩 중 두 번째 예외 → std::terminate 호출 }};
// 올바른 패턴: 소멸자에서 예외 삼키기class GoodRAII{public: ~GoodRAII() noexcept { try { CleanupResource(); } catch (const std::exception& E) { // 로그 기록만, 예외를 전파하지 않음 LogError(E.what()); } }};| 자원 유형 | RAII 도구 |
|---|---|
| 힙 메모리 (단독 소유) | std::unique_ptr<T> |
| 힙 메모리 (공유 소유) | std::shared_ptr<T> |
| 약한 참조 (순환 방지) | std::weak_ptr<T> |
| 뮤텍스 | std::lock_guard, std::scoped_lock |
| 파일·소켓·OS 핸들 | 커스텀 RAII 클래스 |
| 임의 정리 작업 | ScopeGuard |
| 트랜잭션 | 커스텀 Transaction 클래스 |
핵심 규칙:
- 소멸자는 반드시
noexcept— 예외가 발생하면 내부에서 삼키고 로그 - 자원을 소유하는 클래스는 5의 법칙 준수 (소멸자·복사·이동 4개 명시)
std::make_unique/std::make_shared사용 —new를 직접 쓰면 예외 안전하지 않을 수 있음- Copy-and-Swap 패턴으로 대입 연산자의 강한 예외 안전성 보장