Computing

RAII 개념과 필요성 본문

Programming/C++

RAII 개념과 필요성

jhson989 2023. 8. 3. 22:34

RAII 개념

RAII (Resource Acuquisition Is Intialization) 는 C++에서 강조되는 디자인 테크닉 중 하나이다[1]. "자원 획득(Resource Acquisition)은 초기화(Initialization)이다" 라고 직역할 수 있는데, 한글로 직역하여도 바로 와닿지는 않을 것이다.

 

좀 더 풀어서 얘기하자면, 자원 획득을 객체 초기화 시에만 하라, 혹은, 자원 획득이 필요한 경우, 자원 획득을 담당하는 클래스를 만들어 그 클래스의 생성자에서만 자원 획득을 하라고 이해하면 좋을 것 같다. (프로그램을 짜다보면 동적 메모리 할당, 파일 열기, 락 등 자원 획득을 하는 일이 많은데, 이러한 자원 획득을 담당하는 클래스를 만들어 그 클래스의 생성자에서만 자원 획득을 하라고 강조하는 말)

 

cppreference에 따르면, RAII는 자원의 생애주기(life cycle, 자원 획득과 자원 해제)를 객체의 생애(lifetime)에 바인딩시키는 기법이다[1]. (원문은 밑에) 일단 이 문장을 깊게 이해하기 위해서는 자원의 생애 주기, 객체의 생애, 왜 두개를 바인딩(결합)시켜야 하는 지가 정리되어야 할 듯하다.

 

RAII is a C++ programming technique which binds the life cycle of a resource that must be acquired before use to the lifetime of an object.

 

 

 

자원의 생애 주기

C++ 프로그램에서 자원은 동적 메모리, 쓰레드, 소켓, 파일, 뮤텍스, 데이터베이스 커넥션 등등을 의미한다. 이러한 자원을 사용하기 위해서는, 자원을 사용하기 전에 자원을 획득해야 하고 자원을 다 사용하면 자원을 해제해야 한다. 이를 자원의 생애 주기라고 한다.

 

자원을 사용하기 위해서는 먼저 자원 획득부터 해야 한다. 길이 10인 동적 배열을 사용하는 경우를 생각해보자. 동적 배열을 사용하기 위해서는 먼저 new나 malloc을 이용하여 동적으로 메모리 공간을 할당해야 한다(자원 획득). 동적 메모리를 할당하지 않고 사용한다면 주소 접근 오류가 발생할 것이다. 동적 배열을 다 사용하고 난 다음에는 delete나 free를 이용해 할당된 동적 메모리 공간을 해제해 주어야 한다(자원 해제). 그렇지 않으면, 그 공간은 memory leak으로 남을 것이다.

 

이처럼 자원을 사용하기 위해서는 사용하기 이전에 자원 획득 과정을, 사용 완료 이후에는 자원 해제 과정을 반드시 실행해야 한다. 문제는 실제 프로그램 실행 시, 이러한 자원 획득 및 해제를 하지 못하는 경우가 많다고 한다.

 

자원 획득을 경우에는 획득하지 않고 사용할 시 즉각적으로 에러가 발생하기에 그래도 쉽게 디버깅이 가능하다. 그러나 자원 해제의 경우 자원 해제를 호출하지 않으면 메모리 누수만 발생할 뿐, 즉각적으로 문제를 일으키지는 않아서 디버깅하기가 쉽지 않다. 그렇다면 까먹지 않고 자원 획득 및 해제를 잘 해주면 되지 않나 생각하겠지만, 프로그램이 거대해지면 예외 발생 (throw exception) 등 프로그램의 실행 흐름이 매우 복잡해지기에 생각보다 쉽지 않은 문제라고 한다.

 

그렇기에 다른 객체지향 프로그램, 예를 들면 java나 python, C#에서는 런타임이 자원 관리를 돕는 경우도 있다. 특히 메모리 관련하여 garbage collection과 같은 자동 메모리 해제 기능을 제공하여 메모리 누수 문제를 자동으로 해결한다. 그에 비해 C++은 언어적으로 자동 자원 관리를 해주지 않기에 오로지 프로그래머의 실력에 의해 메모리 문제가 좌우되는 한계가 있다.

 

 

 

객체의 생애

C++에서 모든 객체(object)는 lifetime(생애)이라는 런타임 특성을 가지는데, 객체는 프로그램 실행 중에 무조건 생애가 시작되는 시점과 생애가 끝나는 시점을 가진다[2].

 

[2]에 따르면 lifetime은 조금 더 복잡하게 정의되지만, 클래스 객체에 대해서는 쉽게 클래스의 생성자(constructor)가 끝나는 시점이 객체의 생애 시작 시점이고 클래스의 소멸자(descructor)가 호출되는 시점이 객체의 생애 삭제 시점이라고 생각하면 될 것 같다. 즉 객체는 생성자가 호출됨으로서 생성되고, 소멸자가 호출됨으로서 삭제된다.

 

Stack 메모리 영역에 저장되는 로컬 객체 변수(정확히는 automatic storage duration를 가지는 객체를 의미[3])는 변수가 정의될때 생성자가 자동으로 호출된다. 그리고 객체가 정의된 함수 혹은 block이 끝나면 C++런타임에 의해 자동으로 해당 객체의 소멸자가 호출되며[4] 객체는 제거된다.

 

즉 Stack 메모리 영역에 저장되는 로컬 객체(= objects with automatic storage duration)의 생애는 컴파일러와 C++ 런타임에 의해 자동으로 관리된다. 사용자가 아무리 까먹어도 위와 같은 객체의 생성자와 소멸자는 항상, 무슨 일이 있어도 실행된다. 이러한 특성은 앞서 말한 자원 관리 시 할당 및 해제를 까먹는 실수를 해결할 실마리가 된다.

 

 

RAII의 필요성

RAII 디자인 패턴은 자원의 생애주기를 객체의 생애에 결합시킨다는 것이다. 좀 더 풀어서 말하자면, 자원을 사용하고자 하는 상황에서, 생성자에서 자원 획득을, 소멸자에서 자원 해제를 하는 자원 관리용 클래스를 만드는 프로그래밍 패턴이다. 즉 객체의 시작과 끝은 컴퓨터가 알아서 해주는 이점을 활용하여, 자원 관리용 클래스를 만들어 생성자에서 자원 획득을, 소멸자에서 자원 해제를 하게한다면 자원 관리의 문제를 해결할 수 있게 된다.

 

이렇게 되면 자원의 생애 주기는 객체의 생애와 결합된다. 객체의 생애는 런타임이 알아서 잘 관리해준다고 하였다. 즉 그렇기에 앞으로는 객체와 바인딩된 자원은 런타임이 자동으로 관리할 수 있다.

 

automatic storage duration을 가지는 로컬 객체 변수의 경우 객체가 생성될 때 생성자가 자동으로 호출된다. 생성자가 호출되는 시점에 자원 획득이 자동으로 실행된다. 마찬가지로 객체가 소멸될 때 (함수가 끝나든, 함수가 속한 block이 끝나든) 자동으로 소멸자가 호출된다. 소멸자가 호출되는 시점에 자원 해제가 자동으로 실행된다. 소멸자는 exception이 발생하는 경우에도 항상 실행되기에 자원 해제는 무슨 일이 발생해도 실행된다. 이것이 RAII을 쓰는 이유이다.

 

정리하자면 RAII는 다음을 보장한다.

- RAII는 객체가 생성된 한, 자원이 사용가능함을 보장한다.

- RAII는 객체가 종료된 한, 자원이 완전히 해제되었음을 보장한다.

- RAII는 automatic storage duration을 가지는 객체와 자원이 결합될 경우, 어떠한 상황(ex, exception)에서도 자원이 완전히 해제됨을 보장한다.

 

추가로 생성자, 소멸자 이외에 다른 맴버 함수(예를 들어, open()/close())에서 자원 관리 코드를 호출하도록 구현되었다면 그것은 RAII class가 아니라고 한다. 오로지 생성자, 소멸자에서 자원 관리 코드가 실행되어야 RAII class이다. RAII 디자인 패턴을 달성하기 위해 만들어진 대표적인 class로는 std::unique_ptr, std::shared_ptr, std::lock_guard가 있다.

 

 

+) RAII의 다른 표현으로는 SBRM(Scope-Bound Resource Management)가 있다. RAII는 자원 관리는 꼭 로컬 객체로 하라는 것을 강조하는 표현이고, SBRM은 사용목적을 잘 들어내는 표현이라고 생각이 든다.

 

 

 

Reference

[1] https://en.cppreference.com/w/cpp/language/raii

[2] https://en.cppreference.com/w/cpp/language/lifetime

[3] https://en.cppreference.com/w/cpp/language/storage_duration

[4] https://stackoverflow.com/questions/8311457/are-destructors-called-after-a-throw-in-c