C++ RAII & 리소스 관리
개요 — RAII란
섹션 제목: “개요 — 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. 예외 안전성 보장 수준
섹션 제목: “1. 예외 안전성 보장 수준”| 수준 | 설명 | 달성 방법 |
|---|---|---|
| No-throw | 절대 예외를 던지지 않음 | noexcept 소멸자, 스왑 |
| Strong | 예외 발생 시 원래 상태 완전 복원 | Copy-and-Swap 패턴 |
| Basic | 예외 발생 시 유효한 상태 유지 (값은 변할 수 있음) | RAII 기본 적용 |
| None | 예외 발생 시 상태 보장 없음 | 피해야 함 |
2. 표준 RAII 래퍼
섹션 제목: “2. 표준 RAII 래퍼”2.1 std::unique_ptr — 단독 소유
섹션 제목: “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 — 공유 소유
섹션 제목: “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 — 약한 참조
섹션 제목: “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 래퍼 작성
섹션 제목: “3. 커스텀 RAII 래퍼 작성”표준 라이브러리가 제공하지 않는 자원(OS 핸들, 네트워크 소켓, GPU 리소스 등)을 위한 RAII 클래스를 직접 작성합니다.
3.1 파일 핸들 래퍼
섹션 제목: “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 래퍼 (직접 구현 예시)
섹션 제목: “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 범용 스코프 가드
섹션 제목: “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 패턴 — 강한 예외 안전성
섹션 제목: “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
섹션 제목: “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와 예외 처리 통합
섹션 제목: “6. RAII와 예외 처리 통합”// 소멸자에서 예외 던지기 — 절대 금지class BadRAII{public: ~BadRAII() { CleanupResource(); // 예외를 던질 수 있다면? // 스택 언와인딩 중 두 번째 예외 → std::terminate 호출 }};
// 올바른 패턴: 소멸자에서 예외 삼키기class GoodRAII{public: ~GoodRAII() noexcept { try { CleanupResource(); } catch (const std::exception& E) { // 로그 기록만, 예외를 전파하지 않음 LogError(E.what()); } }};7. 정리
섹션 제목: “7. 정리”| 자원 유형 | 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 패턴으로 대입 연산자의 강한 예외 안전성 보장
8. 네트워크 소켓 RAII 래퍼
섹션 제목: “8. 네트워크 소켓 RAII 래퍼”#ifdef _WIN32#include <winsock2.h>using SocketHandle = SOCKET;constexpr SocketHandle INVALID = INVALID_SOCKET;inline void close_socket(SocketHandle s) { closesocket(s); }#else#include <unistd.h>using SocketHandle = int;constexpr SocketHandle INVALID = -1;inline void close_socket(SocketHandle s) { close(s); }#endif
class Socket {public: explicit Socket(SocketHandle handle) : handle_(handle) { if (handle_ == INVALID) throw std::runtime_error("소켓 생성 실패"); }
~Socket() noexcept { if (handle_ != INVALID) close_socket(handle_); }
Socket(const Socket&) = delete; Socket& operator=(const Socket&) = delete;
Socket(Socket&& other) noexcept : handle_(other.handle_) { other.handle_ = INVALID; }
SocketHandle get() const noexcept { return handle_; } bool valid() const noexcept { return handle_ != INVALID; }
private: SocketHandle handle_;};
// 사용void connect_and_send(const char* host, int port){ Socket sock(socket(AF_INET, SOCK_STREAM, 0)); // RAII로 관리 // ... connect, send ... // 스코프 종료 시 자동 close (예외 발생해도 보장)}9. C++23 std::scope_exit / std::scope_fail
섹션 제목: “9. C++23 std::scope_exit / std::scope_fail”C++23에서는 표준 ScopeGuard가 <scope> 헤더에 추가되었습니다.
// C++23 표준 scope guard#include <scope>
void example(){ auto* conn = open_connection("db://...");
// 항상 실행 std::scope_exit cleanup([&] { close_connection(conn); });
// 예외 발생 시만 실행 (롤백) std::scope_fail rollback([&] { rollback_transaction(conn); });
// 성공 시만 실행 std::scope_success commit([&] { commit_transaction(conn); });
do_risky_work(conn); // 예외 발생 가능
// 정상 종료: cleanup + commit 실행 // 예외 발생: cleanup + rollback 실행}