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 예제를 구현할 수 있습니다.
- 자주 하는 실수와 해결법을 익힐 수 있습니다.
- 프로덕션에서 바로 적용할 수 있는 패턴을 배울 수 있습니다.
목차
- 문제 시나리오
- RAII란 무엇인가?
- RAII 핵심 원칙
- 완전한 RAII 예제: 리소스 관리
- 완전한 RAII 예제: lock_guard
- 완전한 RAII 예제: 파일 핸들
- 자주 발생하는 에러와 해결법
- 모범 사례와 선택 가이드
- 프로덕션 패턴
- 성능·체크리스트
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 클래스로 소멸자에서 !committed 시 rollback()을 호출하면 예외 안전합니다.
시나리오 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마다 fclose와 mtx.unlock()을 따로 호출해야 해서 누락·중복이 생깁니다. goodFunction에서는 std::ifstream과 std::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_lock은 lock_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_lock | unlock/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_ptr | raw 포인터·delete 금지 |
| 파일 | std::ifstream, std::ofstream | C 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 |
핵심 원칙:
- 리소스는 항상 RAII로 관리
- 수동
close(),unlock()호출 제거 - 소멸자는
noexcept - 복사/이동 의미 명시
- 표준 라이브러리 우선 사용
자주 묻는 질문 (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 클라이언트: 요청·응답 처리
참고 자료
- RAII on cppreference
- C++ Core Guidelines - Resource Management
- “Effective Modern C++” by Scott Meyers
- “C++ Concurrency in Action” by Anthony Williams
관련 글
- C++ 디자인 패턴 | Observer·Strategy
- C++ 디자인 패턴 종합 가이드 | Singleton·Factory
- C++ RAII |
- C++ 디자인 패턴 | Singleton·Factory·Builder·Prototype 생성 패턴 가이드
- C++ 이동 의미론 완벽 가이드 | rvalue 참조·std::move