C++을 실무에서 사용하다 보면 RAII라는 표현을 자주 만나게 됩니다.
처음 보면 용어가 조금 어렵게 느껴질 수 있습니다.
Resource Acquisition Is Initialization, 즉 “자원 획득은 초기화”라는 뜻입니다.
하지만 핵심은 그렇게 복잡하지 않습니다.
자원을 객체가 책임지게 만들고, 객체가 사라질 때 자원도 자동으로 정리되게 만드는 방식입니다.
이번 글에서는 RAII가 왜 중요한지, 스마트 포인터와 어떤 관계가 있는지, 그리고 실무 C++ 코드에서 어떤 기준으로 봐야 하는지 정리해보겠습니다.
1. RAII는 무엇인가?
RAII는 C++에서 자원을 안전하게 관리하기 위한 대표적인 패턴입니다.
자원이라고 하면 메모리만 떠올리기 쉽지만, 실무에서는 훨씬 더 많은 것들이 자원입니다.
| 자원 종류 | 예시 |
| 메모리 | 동적 할당 객체 |
| 파일 | 파일 핸들, 파일 스트림 |
| 네트워크 | 소켓, 연결 객체 |
| 동시성 | mutex lock |
| 외부 시스템 | DB 연결, 트랜잭션 |
| 운영체제 | OS handle, descriptor |
이런 자원들은 사용이 끝나면 반드시 정리해야 합니다.
문제는 사람이 직접 close, unlock, delete 같은 정리 함수를 호출하는 방식이 실수하기 쉽다는 점입니다.
RAII는 이 문제를 객체 생명주기로 해결합니다.
- 생성자에서 자원을 획득한다.
- 객체가 살아 있는 동안 자원을 사용한다.
- 소멸자에서 자원을 해제한다.
즉, 객체가 스코프 안에 있는 동안 자원도 살아 있고,
객체가 스코프를 벗어나면 자원도 함께 정리됩니다.
2. RAII가 필요한 이유
다음과 같은 파일 처리 코드를 생각해보겠습니다.
FILE* file = fopen("app.log", "w");
if (!file) {
return;
}
fprintf(file, "start\n");
if (/* 중간에 실패 */) {
return;
}
fprintf(file, "finish\n");
fclose(file);
이 코드에는 문제가 있습니다.
중간에 return이 발생하면 fclose가 호출되지 않을 수 있습니다.
예외가 발생하는 코드라면 문제는 더 커집니다.
처음에는 단순한 코드였더라도, 실무 코드에서는 조건문과 예외 처리, 조기 반환이 계속 추가됩니다.
그때마다 모든 경로에서 자원이 정리되는지 사람이 확인해야 합니다.
RAII는 이 부담을 줄여줍니다.
정리 코드를 모든 분기에 흩뿌리는 대신, 소멸자 한 곳에 모읍니다.
3. RAII 흐름을 그림으로 보기
RAII의 흐름은 다음처럼 볼 수 있습니다.
- RAII flow chart
객체 생성
|
v
생성자에서 자원 획득
|
v
스코프 안에서 자원 사용
|
v
스코프 종료, return, 예외 발생
|
v
소멸자 자동 호출
|
v
자원 해제
중요한 점은 스코프 종료가 정상 흐름만 의미하지 않는다는 것입니다.
함수가 중간에 return되거나 예외로 빠져나가도, 스택에 만들어진 객체의 소멸자는 호출됩니다.
그래서 RAII는 정상 흐름뿐 아니라 비정상 흐름에서도 자원 정리 누락을 줄여줍니다.
4. RAII 방식으로 파일 관리하기
앞의 파일 처리 코드를 RAII 방식으로 감싸면 다음처럼 만들 수 있습니다.
#include <cstdio>
#include <exception>
#include <iostream>
#include <stdexcept>
#include <string_view>
class FileWriter {
public:
explicit FileWriter(const char* path)
: file_(std::fopen(path, "w")) {
if (file_ == nullptr) {
throw std::runtime_error("failed to open file");
}
}
~FileWriter() {
if (file_ != nullptr) {
std::fclose(file_);
}
}
FileWriter(const FileWriter&) = delete;
FileWriter& operator=(const FileWriter&) = delete;
void write(std::string_view text) {
const auto written = std::fwrite(text.data(), 1, text.size(), file_);
if (written != text.size()) {
throw std::runtime_error("failed to write file");
}
}
private:
std::FILE* file_;
};
void writeLog(bool failEarly) {
FileWriter writer("app.log");
writer.write("start\n");
if (failEarly) {
return;
}
writer.write("finish\n");
}
int main() {
try {
writeLog(true);
std::cout << "done\n";
} catch (const std::exception& e) {
std::cerr << e.what() << '\n';
}
}
이 코드에서 writeLog(true)는 중간에 return됩니다.
하지만 FileWriter writer가 스코프를 벗어나면서 소멸자가 호출됩니다.
따라서 fclose를 직접 호출하지 않아도 파일은 정리됩니다.
이것이 RAII의 핵심입니다.
자원을 쓰는 쪽에서는 “언제 닫을지”를 매번 신경 쓰지 않고,
자원을 소유한 객체가 사라질 때 정리되도록 설계합니다.
5. 복사를 막아야 하는 이유
예제 코드에서 다음 부분도 중요합니다.
FileWriter(const FileWriter&) = delete;
FileWriter& operator=(const FileWriter&) = delete;
파일 핸들은 보통 하나의 객체가 책임져야 합니다.
만약 FileWriter가 복사 가능하다면 어떻게 될까요?
FileWriter a("app.log");
FileWriter b = a;
이런 코드가 가능해질 수 있습니다.
그러면 a와 b가 같은 FILE*를 가지고 있을 수 있고,
두 객체의 소멸자에서 같은 파일 핸들을 두 번 닫으려 할 수 있습니다.
이런 문제를 이중 해제라고 볼 수 있습니다.
그래서 자원을 단독으로 소유하는 RAII 타입은 복사를 금지하는 경우가 많습니다.
반대로 복사가 필요하다면 깊은 복사를 구현할지,
공유 소유권을 사용할지,
애초에 복사 비용을 감수할지 명확하게 정해야 합니다.
6. 스마트 포인터도 RAII다
RAII를 설명할 때 가장 많이 등장하는 예시는 스마트 포인터입니다.
std::unique_ptr은 동적 할당 객체를 단독으로 소유합니다.
unique_ptr 객체가 사라지면 내부 포인터에 대해 delete가 호출됩니다.
#include <iostream>
#include <memory>
class User {
public:
explicit User(int id) : id_(id) {
std::cout << "User created\n";
}
~User() {
std::cout << "User destroyed\n";
}
private:
int id_;
};
int main() {
auto user = std::make_unique<User>(1);
}
main 함수가 끝나면 user가 스코프를 벗어납니다.
그 순간 unique_ptr의 소멸자가 호출되고, User 객체도 자동으로 해제됩니다.
즉, unique_ptr은 다음 일을 대신해줍니다.
- new로 만든 객체를 소유한다.
- 복사를 막아 단독 소유권을 보장한다.
- 스코프가 끝나면 delete를 호출한다.
이것도 전형적인 RAII입니다.
7. lock_guard로 보는 RAII
RAII는 메모리뿐 아니라 락 관리에서도 중요합니다.
멀티스레드 코드에서 mutex를 직접 lock, unlock으로 다루면 실수하기 쉽습니다.
mutex.lock();
// 작업 수행
mutex.unlock();
중간에 예외가 발생하면 unlock이 호출되지 않을 수 있습니다.
그러면 다른 스레드가 계속 기다리게 되고, 데드락으로 이어질 수 있습니다.
이럴 때 std::lock_guard를 사용할 수 있습니다.
#include <iostream>
#include <mutex>
class Counter {
public:
void increase() {
std::lock_guard<std::mutex> lock(mutex_);
++value_;
}
int value() const {
std::lock_guard<std::mutex> lock(mutex_);
return value_;
}
private:
mutable std::mutex mutex_;
int value_ = 0;
};
int main() {
Counter counter;
counter.increase();
std::cout << counter.value() << '\n';
}
std::lock_guard는 생성될 때 mutex를 잠급니다.
그리고 스코프를 벗어날 때 자동으로 unlock합니다.
따라서 중간에 예외가 발생하더라도 락이 풀릴 수 있습니다.
이처럼 RAII는 메모리 관리뿐 아니라 동시성 코드에서도 매우 중요합니다.
8. RAII를 사용할 때 주의할 점
RAII는 강력하지만, 아무렇게나 사용하면 안 됩니다.
8-1. 소멸자에서 예외를 던지지 말자
소멸자는 객체가 정리되는 과정에서 호출됩니다.
특히 예외가 이미 발생해서 스택을 되감는 중에 소멸자에서 또 다른 예외가 발생하면 프로그램이 종료될 수 있습니다.
그래서 소멸자에서는 예외를 던지지 않도록 설계하는 것이 좋습니다.
실패 가능성이 있는 작업은 close 같은 명시적 함수에서 처리하고,
소멸자는 마지막 정리 역할만 하도록 두는 편이 안전합니다.
8-2. 소유권 정책을 명확히 하자
RAII 객체를 만들 때 가장 먼저 봐야 하는 것은 소유권입니다.
- 이 객체가 자원을 단독으로 소유하는가?
- 여러 객체가 공유해도 되는가?
- 복사되면 어떤 일이 일어나는가?
- 이동은 허용할 것인가?
단독 소유라면 복사를 막고 이동만 허용하는 설계가 자연스러울 수 있습니다.
공유 소유가 필요하다면 shared_ptr 같은 타입을 고려할 수 있지만,
공유 소유권은 생명주기를 추적하기 어렵게 만들 수도 있습니다.
8-3. RAII가 모든 설계를 대신하지는 않는다
RAII는 자원 정리 누락을 줄여주지만, 자원 사용 정책 자체를 대신 정해주지는 않습니다.
예를 들어 DB 트랜잭션이라면 소멸자에서 rollback할지,
명시적으로 commit을 호출해야만 성공 처리할지,
실패 로그를 어디에 남길지 같은 정책을 따로 정해야 합니다.
RAII는 이런 정책을 안전하게 구현하는 도구이지, 정책 자체는 아닙니다.
9. 실무에서는 어떻게 봐야 할까?
실무 코드에서 RAII는 “정리 코드를 어디에 둘 것인가”에 대한 기준입니다.
다음과 같은 호출이 보이면 RAII로 감쌀 수 있는지 먼저 생각해보는 것이 좋습니다.
new
delete
malloc
free
open
close
lock
unlock
connect
disconnect
직접 정리하는 코드가 무조건 나쁜 것은 아닙니다.
하지만 정리 책임이 여러 분기에 흩어져 있다면 버그가 숨어 있을 가능성이 높습니다.
실무에서는 다음 질문을 자주 해보면 좋습니다.
- 이 자원은 누가 소유하는가?
- 이 자원은 언제 해제되는가?
- 조기 반환이 있어도 정리되는가?
- 예외가 발생해도 정리되는가?
- 복사되면 이중 해제 문제가 생기지 않는가?
- 표준 라이브러리의 RAII 타입으로 대체할 수 있는가?
이 질문에 답할 수 있으면 C++ 코드의 객체 생명주기와 소유권이 훨씬 명확해집니다.
10. 정리
RAII는 C++에서 자원을 안전하게 관리하기 위한 핵심 패턴입니다.
핵심만 정리하면 다음과 같습니다.
- RAII는 생성자에서 자원을 획득하고 소멸자에서 자원을 해제한다.
- 객체가 스코프를 벗어나면 소멸자가 자동으로 호출된다.
- 조기 반환이나 예외 상황에서도 자원 정리 누락을 줄일 수 있다.
- unique_ptr, lock_guard, fstream은 모두 RAII의 대표적인 예시다.
- 자원을 단독 소유하는 타입은 복사 가능 여부를 신중하게 정해야 한다.
- 소멸자에서는 예외를 던지지 않도록 설계하는 것이 좋다.
결국 RAII는 단순한 C++ 문법이 아니라, 객체 생명주기와 자원 소유권을 연결하는 설계 방식입니다.
C++을 실무에서 다루려면 “어디서 자원을 얻고, 누가 책임지고, 언제 해제되는가”를 항상 같이 생각하는 습관이 중요합니다.
'C++ > Concepts' 카테고리의 다른 글
| C++ unordered_map bucket과 rehash 이해하기 (0) | 2026.07.02 |
|---|---|
| C++ optional, 언제 쓰고 언제 피할까 (0) | 2026.06.29 |
| std::shared_ptr과 std::weak_ptr 제대로 이해하기 (0) | 2026.06.22 |
| std::unique_ptr 제대로 이해하기 (0) | 2026.06.18 |
| C++ std::move는 진짜 객체를 이동시킬까? (0) | 2026.06.12 |