Menu
Coddy logo textTech

패턴으로서의 RAII

Coddy C++ 여정의 객체 지향 프로그래밍 섹션에 포함된 레슨. 104개 중 102번째.

RAII (Resource Acquisition Is Initialization)는 단순한 C++ 관용구 그 이상으로, resource 관리를 객체 수명에 연결하는 강력한 설계 패턴입니다. 스마트 포인터를 사용한 RAII는 이미 살펴보았지만, 이 패턴은 파일 핸들, 네트워크 connections, 뮤텍스 또는 데이터베이스 트랜잭션과 같은 모든 resource에 적용됩니다.

핵심 아이디어는 간단합니다. constructor에서 리소스를 acquire하고 destructor에서 해제합니다. C++는 객체가 스코프를 벗어날 때 소멸자가 실행되도록 보장하므로, 예외가 발생하는 경우에도 정리가 automatically 수행됩니다:

#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는 멀티스레드 코드에서 잠금을 관리할 때 진가를 발휘합니다. 표준 라이브러리의 std::lock_guard는 이 패턴을 따릅니다. 생성 시 뮤텍스를 획득하고 소멸 시 해제하여, unlock을 잊어서 발생하는 교착 상태를 방지합니다.

RAII 클래스를 구현할 때는 리소스가 중복되거나 이중 해제되는 문제를 방지하기 위해 delete하거나 복사/이동 연산을 올바르게 구현해야 한다는 점(Rule of Five)을 기억하세요. RAII는 오류가 발생하기 쉬운 수동 리소스 관리를 안전하고 자동화된 정리 작업으로 바꿔 줍니다.

challenge icon

챌린지

쉬움

RAII를 사용하여 데이터베이스 연결을 안전하게 관리하는 Connection Pool Manager를 만들어 봅시다. 실제 애플리케이션에서 데이터베이스 연결은 적절하게 획득하고 해제해야 하는 값비싼 리소스입니다. 예외가 발생하거나 코드 경로가 복잡해지더라도 연결이 항상 풀로 반환되도록 보장하는 RAII 래퍼를 만들 것입니다.

코드를 세 개의 파일로 구성합니다.

  • ConnectionPool.h: 제한된 수의 연결을 관리하는 간단한 연결 풀을 Create합니다.

    ConnectionPool class는 사용할 수 있는 연결 수를 추적해야 합니다(생성자에 전달된 capacity로 시작). 다음을 구현하세요.

    • acquire() — 연결을 사용할 수 있으면 count를 줄이고, 남은 count를 나타내는 Connection acquired (X available)를 출력합니다. 성공하면 true를 반환하고, 사용할 수 있는 연결이 없으면 false를 반환합니다.
    • release() — available count를 늘리고 Connection released (X available)를 출력합니다.
    • available() — 현재 사용할 수 있는 연결 수를 반환합니다.
  • ConnectionGuard.h: 단일 연결을 안전하게 관리하는 RAII 래퍼를 만듭니다.

    ConnectionGuard class는 RAII 패턴을 구현합니다. 다음과 같이 동작해야 합니다.

    • Constructor에서 ConnectionPool에 대한 reference를 받고 연결 획득을 시도합니다.
    • acquisition이 성공했는지 여부를 저장합니다.
    • guard가 유효한 연결을 보유하고 있는지 확인할 수 있도록 isConnected() method를 제공합니다.
    • Destructor에서 연결을 pool로 automatically 반환합니다(연결이 acquired된 경우에만).
    • resource duplication을 방지하기 위해 copy constructor와 copy assignment를 Delete합니다(Rule of Five 고려).

    Destructor가 실행될 때 연결이 보유되어 있었다면, pool에서 release를 호출하기 전에 Guard releasing connection을 출력합니다.

  • main.cpp: scope를 통해 RAII의 automatic cleanup을 보여 줍니다.

    두 개의 입력을 읽습니다.

    1. Pool capacity (integer)
    2. 요청할 연결 수 (integer)

    주어진 capacity로 ConnectionPool을 Create합니다. 그런 다음 중첩된 scope(curly braces 사용) 안에서 요청된 수만큼 ConnectionGuard 객체를 Create하고 vector에 저장합니다. 각 guard에 대해 연결에 성공했는지 출력합니다.

    • 연결된 경우: Guard N: Connected
    • 연결되지 않은 경우: Guard N: Failed to connect

    (여기서 N은 1부터 시작합니다.)

    scope가 ends된 후(guards가 destroyed된 후), pool의 최종 상태를 보여 주는 After scope: X connections available을 출력합니다.

예를 들어 입력이 23인 경우:

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

입력이 32인 경우:

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

guards가 scope를 벗어날 때 connections가 automatically released된다는 점에 주목하세요. main 코드에서 release를 명시적으로 호출하지 않아도 됩니다. Destructors는 construction의 역순으로 실행되므로(마지막 guard가 먼저 destroyed됨), acquired된 모든 connection은 반드시 반환됩니다. 이것이 RAII의 강력한 점입니다. scope가 어떻게 종료되든 resource cleanup이 automatically 안정적으로 수행됩니다.

직접 해보기

#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" or "Guard N: Failed to connect"
        // 여기서 N은 1부터 시작
        
    }
    // 스코프가 끝날 때 가드들이 여기서 소멸됩니다
    
    // TODO: Print "After scope: X connections available"
    
    return 0;
}
quiz icon실력 점검

이 레슨에는 짧은 퀴즈가 포함되어 있습니다. 레슨을 시작해 문제를 풀고 진행 상황을 기록하세요.

객체 지향 프로그래밍의 모든 레슨

직접 연습해 보세요: 온라인 C++ 컴파일러