패턴으로서의 RAII
Coddy C++ 여정의 객체 지향 프로그래밍 섹션에 포함된 레슨 — 104개 중 102번째.
RAII (Resource Acquisition Is Initialization)는 단순한 C++ 관용구 그 이상입니다. 이는 자원 관리를 객체의 수명(lifetime)과 연결하는 강력한 디자인 패턴입니다. 이미 스마트 포인터를 통해 RAII를 접해보셨겠지만, 이 패턴은 파일 핸들, 네트워크 연결, 뮤텍스(mutexes), 또는 데이터베이스 트랜잭션과 같은 모든 종류의 자원에 적용됩니다.
핵심 아이디어는 간단합니다: 생성자에서 리소스를 획득하고, 소멸자에서 리소스를 해제하는 것입니다. C++는 객체가 범위를 벗어날 때 소멸자가 실행되는 것을 보장하므로, 예외가 발생하더라도 정리가 자동으로 이루어집니다:
#include <iostream>
#include <fstream>
class FileGuard {
std::ofstream file;
public:
FileGuard(const std::string& filename) : file(filename) {
if (!file.is_open()) {
std::cout << "Failed to open file\n";
}
}
void write(const std::string& text) {
if (file.is_open()) file << text;
}
~FileGuard() {
if (file.is_open()) {
file.close();
std::cout << "File closed automatically\n";
}
}
};
int main() {
{
FileGuard guard("output.txt");
guard.write("Hello RAII");
} // 여기서 소멸자 호출 - 파일이 닫힘
std::cout << "After scope\n";
}RAII는 멀티스레드 코드에서 락(lock)을 관리할 때 빛을 발합니다. 표준 라이브러리의 std::lock_guard는 이 패턴을 따릅니다. 생성 시 뮤텍스(mutex)를 획득하고 소멸 시 이를 해제하여, 언락(unlock)을 잊어버려 발생하는 데드락을 방지합니다.
RAII 클래스를 구현할 때, 리소스 중복 또는 이중 해제 문제를 방지하기 위해 복사/이동 연산(Rule of Five)을 삭제하거나 적절하게 구현해야 함을 기억하십시오. RAII는 오류가 발생하기 쉬운 수동 리소스 관리를 안전하고 자동화된 정리 작업으로 변환합니다.
챌린지
쉬움RAII를 사용하여 데이터베이스 연결을 안전하게 관리하는 커넥션 풀 매니저(Connection Pool Manager)를 구축해 보겠습니다. 실제 애플리케이션에서 데이터베이스 연결은 적절하게 획득하고 해제해야 하는 비용이 많이 드는 리소스입니다. 예외가 발생하거나 코드 경로가 복잡해지더라도 연결이 항상 풀로 반환되도록 보장하는 RAII 래퍼를 만들 것입니다.
코드는 세 개의 파일로 구성됩니다:
ConnectionPool.h: 제한된 수의 연결을 관리하는 간단한 커넥션 풀을 만듭니다.ConnectionPool클래스는 사용 가능한 연결 수를 추적해야 합니다(생성자에 전달된 용량으로 시작). 다음을 구현하세요:acquire()— 연결이 가능한 경우, 개수를 줄이고"Connection acquired (X available)"를 출력합니다(여기서 X는 남은 개수). 성공하면true를, 사용 가능한 연결이 없으면false를 반환합니다.release()— 사용 가능한 개수를 늘리고"Connection released (X available)"를 출력합니다.available()— 현재 사용 가능한 연결 수를 반환합니다.
ConnectionGuard.h: 단일 연결을 안전하게 관리하는 RAII 래퍼를 구축합니다.ConnectionGuard클래스는 RAII 패턴을 구현합니다. 다음을 수행해야 합니다:- 생성자에서
ConnectionPool에 대한 참조를 받고 연결 획득을 시도합니다. - 획득 성공 여부를 저장합니다.
- 가드가 유효한 연결을 보유하고 있는지 확인하는
isConnected()메서드를 제공합니다. - 소멸자에서 연결을 풀로 자동으로 반환합니다(연결을 획득한 경우에만).
- 리소스 중복을 방지하기 위해 복사 생성자와 복사 할당 연산자를 삭제합니다(Rule of Five 고려).
소멸자가 실행될 때, 연결을 보유하고 있었다면 풀의 release를 호출하기 전에
"Guard releasing connection"을 출력합니다.- 생성자에서
main.cpp: 스코프를 통한 RAII의 자동 정리 기능을 시연합니다.두 개의 입력을 읽습니다:
- 풀 용량 (정수)
- 요청할 연결 수 (정수)
주어진 용량으로
ConnectionPool을 생성합니다. 그런 다음, 중첩된 스코프(중괄호 사용) 내에서 벡터에 저장된 요청된 수만큼의ConnectionGuard객체를 생성합니다. 각 가드에 대해 연결 성공 여부를 출력합니다:- 연결된 경우:
"Guard N: Connected" - 연결되지 않은 경우:
"Guard N: Failed to connect"
(여기서 N은 1부터 시작합니다)
스코프가 종료된 후(가드들이 소멸된 후), 풀의 최종 상태를 보여주는
"After scope: X connections available"를 출력합니다.
예를 들어, 입력이 2와 3인 경우:
Connection acquired (1 available)
Guard 1: Connected
Connection acquired (0 available)
Guard 2: Connected
Guard 3: Failed to connect
Guard releasing connection
Connection released (1 available)
Guard releasing connection
Connection released (2 available)
After scope: 2 connections available입력이 3과 2인 경우:
Connection acquired (2 available)
Guard 1: Connected
Connection acquired (1 available)
Guard 2: Connected
Guard releasing connection
Connection released (2 available)
Guard releasing connection
Connection released (3 available)
After scope: 3 connections available가드가 스코프를 벗어날 때 연결이 어떻게 자동으로 해제되는지 확인하세요. 메인 코드에서 release를 명시적으로 호출할 필요가 없습니다. 소멸자는 생성의 역순으로 실행되며(마지막 가드가 먼저 소멸됨), 획득된 모든 연결은 반드시 반환되도록 보장됩니다. 이것이 RAII의 힘입니다. 스코프가 어떻게 종료되든 리소스 정리가 자동적이고 안정적으로 일어납니다.
치트 시트
RAII (Resource Acquisition Is Initialization)는 리소스 관리를 객체의 수명 주기와 연결하는 디자인 패턴입니다. 리소스는 생성자에서 획득되고 소멸자에서 해제됩니다.
C++는 객체가 범위를 벗어날 때 소멸자가 실행되는 것을 보장하므로, 예외가 발생하더라도 정리가 자동으로 이루어집니다:
class FileGuard {
std::ofstream file;
public:
FileGuard(const std::string& filename) : file(filename) {
if (!file.is_open()) {
std::cout << "Failed to open file\n";
}
}
void write(const std::string& text) {
if (file.is_open()) file << text;
}
~FileGuard() {
if (file.is_open()) {
file.close();
std::cout << "File closed automatically\n";
}
}
};
int main() {
{
FileGuard guard("output.txt");
guard.write("Hello RAII");
} // 여기서 소멸자 호출 - 파일이 자동으로 닫힘
std::cout << "After scope\n";
}RAII는 특히 멀티스레드 코드에서 락(lock)을 관리하는 데 유용합니다. 표준 라이브러리의 std::lock_guard는 생성 시 뮤텍스를 획득하고 소멸 시 해제하여, 언락(unlock)을 잊어버려 발생하는 데드락을 방지합니다.
RAII 클래스를 구현할 때는 리소스 중복 또는 이중 해제 문제를 방지하기 위해 복사/이동 연산(Rule of Five)을 삭제하거나 적절하게 구현해야 합니다.
직접 해보기
#include <iostream>
#include <vector>
#include "ConnectionPool.h"
#include "ConnectionGuard.h"
using namespace std;
int main() {
// 입력 읽기
int capacity;
int numConnections;
cin >> capacity;
cin >> numConnections;
// TODO: 주어진 capacity로 ConnectionPool 생성
// TODO: 중괄호를 사용하여 중첩된 스코프 생성
{
// TODO: ConnectionGuard 객체들을 저장할 vector 생성
// Hint: ConnectionGuard의
// 복사 생성자가 삭제되었으므로 포인터나 스마트 포인터를 사용해야 합니다
// TODO: numConnections 가드를 생성하기 위한 루프
// 각 가드에 대해 다음 중 하나를 출력:
// "Guard N: Connected" 또는 "Guard N: Failed to connect"
// 여기서 N은 1부터 시작
}
// 스코프가 종료될 때 여기서 가드들이 소멸됨
// TODO: "After scope: X connections available" 출력
return 0;
}
이 레슨에는 짧은 퀴즈가 포함되어 있습니다. 레슨을 시작해 문제를 풀고 진행 상황을 기록하세요.