RAII
자원 획득은 초기화(Resource acquisition is initialization, RAII)는 특정 언어 동작을 설명하기 위해 여러 객체 지향, 정적 자료형 프로그래밍 언어에서 사용되는 프로그래밍 관용구이다. RAII에서 자원을 보유하는 것은 클래스 불변성이며, 객체 수명에 연결된다. 자원 할당 (또는 획득)은 생성자에 의해 객체 생성 (특히 초기화) 중에 이루어지며, 자원 할당 해제 (반환)는 소멸자에 의해 객체 파괴 (특히 마무리) 중에 이루어진다. 다시 말해, 초기화가 성공하려면 자원 획득이 성공해야 한다. 따라서 자원은 초기화가 완료되고 마무리 시작 사이 (자원을 보유하는 것이 클래스 불변성)에 보유되며, 객체가 살아있는 동안에만 보유된다는 것이 보장된다. 그러므로 객체 누수가 없으면 자원 누수도 없다. RAII는 기원인 C++와 가장 밀접하게 연관되어 있지만, 에이다, 발라, 러스트와도 관련이 있다. 이 기법은 1984년에서 1989년 사이에 주로 비야네 스트롭스트룹과 앤드루 쾨닉에 의해 C++에서 예외 안전 자원 관리를 위해 개발되었으며, 이 용어 자체는 스트롭스트룹이 만들었다. 이 관용구의 다른 이름으로는 생성자 획득, 소멸자 해제(Constructor Acquires, Destructor Releases, CADRe)가 있으며, 특정 사용 방식은 스코프 기반 자원 관리(Scope-based Resource Management, SBRM)라고 불린다. 이 후자의 용어는 자동 변수의 특수한 경우를 지칭한다. RAII는 자원을 객체 수명에 연결하는데, 이는 스코프 진입 및 종료와 일치하지 않을 수 있다. (특히 자유 저장소에 할당된 변수는 특정 스코프와 무관한 수명을 갖는다.) 그러나 자동 변수에 RAII(SBRM)를 사용하는 것이 가장 일반적인 사용 사례이다.
자원 획득은 초기화(Resource acquisition is initialization, RAII)[1]는 특정 언어 동작을 설명하기 위해 여러 객체 지향, 정적 자료형 프로그래밍 언어에서 사용되는 프로그래밍 관용구이다.[2] RAII에서 자원을 보유하는 것은 클래스 불변성이며, 객체 수명에 연결된다. 자원 할당 (또는 획득)은 생성자에 의해 객체 생성 (특히 초기화) 중에 이루어지며, 자원 할당 해제 (반환)는 소멸자에 의해 객체 파괴 (특히 마무리) 중에 이루어진다. 다시 말해, 초기화가 성공하려면 자원 획득이 성공해야 한다. 따라서 자원은 초기화가 완료되고 마무리 시작 사이 (자원을 보유하는 것이 클래스 불변성)에 보유되며, 객체가 살아있는 동안에만 보유된다는 것이 보장된다. 그러므로 객체 누수가 없으면 자원 누수도 없다.
RAII는 기원인 C++와 가장 밀접하게 연관되어 있지만, 에이다[3], 발라[4], 러스트[5]와도 관련이 있다. 이 기법은 1984년에서 1989년 사이에 주로 비야네 스트롭스트룹과 앤드루 쾨닉에 의해 C++에서 예외 안전 자원 관리를 위해 개발되었으며[6][7], 이 용어 자체는 스트롭스트룹이 만들었다.[8]
이 관용구의 다른 이름으로는 생성자 획득, 소멸자 해제(Constructor Acquires, Destructor Releases, CADRe)[9]가 있으며, 특정 사용 방식은 스코프 기반 자원 관리(Scope-based Resource Management, SBRM)라고 불린다.[10] 이 후자의 용어는 자동 변수의 특수한 경우를 지칭한다. RAII는 자원을 객체 수명에 연결하는데, 이는 스코프 진입 및 종료와 일치하지 않을 수 있다. (특히 자유 저장소에 할당된 변수는 특정 스코프와 무관한 수명을 갖는다.) 그러나 자동 변수에 RAII(SBRM)를 사용하는 것이 가장 일반적인 사용 사례이다.
C++ 예시
[편집]다음 C++23 예시는 파일 접근과 뮤텍스 잠금에 대한 RAII 사용법을 보여준다.
import std;
using std::lock_guard;
using std::mutex;
using std::ofstream;
using std::runtime_error;
using std::string;
void writeToFile(const string& message) {
// mutex is to protect access to file (which is shared across threads).
static mutex m;
// Lock mutex before accessing file.
lock_guard<mutex> lock(m);
// Try to open file.
ofstream f{"example.txt"};
if (!f.is_open()) {
throw runtime_error("unable to open file");
}
// Write message to file.
std::println(f, message);
// file will be closed first when leaving scope (regardless of exception)
// mutex will be unlocked second (from lock destructor) when leaving scope
// (regardless of exception).
}
이 코드는 예외 안전이다. C++는 자동 저장 기간(지역 변수)을 가진 모든 객체가 해당 스코프를 벗어날 때 생성의 역순으로 소멸됨을 보장하기 때문이다.[11] 따라서 lock과 file 객체의 소멸자는 예외가 발생했는지 여부와 관계없이 함수에서 반환될 때 호출되는 것이 보장된다.[12]
지역 변수는 단일 함수 내에서 여러 자원을 쉽게 관리할 수 있게 한다. 이들은 생성의 역순으로 소멸되며, 객체는 완전히 생성된 경우에만 소멸된다. 즉, 생성자에서 예외가 전파되지 않은 경우에만 소멸된다.[13]
RAII를 사용하면 자원 관리가 크게 단순화되고, 전체 코드 크기가 줄어들며, 프로그램의 정확성을 보장하는 데 도움이 된다. 따라서 RAII는 업계 표준 가이드라인에서 권장하며,[14] 대부분의 C++ 표준 라이브러리는 이 관용구를 따른다.[15]
장점
[편집]RAII가 자원 관리 기법으로서 갖는 장점은 캡슐화, 예외 안전(스택 자원의 경우), 그리고 지역성(획득 및 해제 로직을 서로 가까이 작성할 수 있게 함)을 제공한다는 것이다.
캡슐화는 자원 관리 로직이 각 호출 사이트가 아닌 클래스에서 한 번 정의되기 때문에 제공된다. 예외 안전은 스택 자원(획득된 스코프와 동일한 스코프에서 해제되는 자원)에 대해 자원을 스택 변수(주어진 스코프에 선언된 지역 변수)의 수명에 연결함으로써 제공된다. 예외가 발생하고 적절한 예외 처리가 되어 있는 경우, 현재 스코프를 벗어날 때 실행되는 유일한 코드는 해당 스코프에 선언된 객체의 소멸자이다. 마지막으로, 정의의 지역성은 클래스 정의에서 생성자와 소멸자 정의를 서로 가까이 작성함으로써 제공된다.
따라서 자동 할당 및 회수를 얻기 위해서는 자원 관리가 적절한 객체의 수명에 연결되어야 한다. 자원은 초기화 중에 획득되며, 이 때는 사용 가능하기 전에 사용될 가능성이 없으며, 동일한 객체의 파괴와 함께 해제된다. 이는 오류 발생 시에도 보장된다.
RAII를 자바에서 사용되는 finally 구문과 비교하면서 스트롭스트룹은 "실제 시스템에서는 자원 종류보다 자원 획득이 훨씬 많기 때문에 '자원 획득은 초기화' 기법이 'finally' 구문 사용보다 적은 코드를 유도한다"고 썼다.[1]
클래스 불변성으로서 RAII는 자원을 획득했다고 가정되는 객체 인스턴스가 실제로 그렇게 했음을 보장한다. 이는 새로 생성된 객체를 사용할 수 있는 상태로 만들기 위한 추가 "설정" 메서드의 필요성(모든 이러한 작업은 생성자에서 수행됨; 마찬가지로, 자원을 해제하기 위한 "종료" 작업은 객체의 소멸자에서 발생함)과 매번 사용하기 전에 인스턴스를 테스트하여 제대로 설정되었는지 확인할 필요성을 제거한다.[16]
일반적인 용도
[편집]RAII 디자인은 다중 스레드 애플리케이션에서 뮤텍스 잠금을 제어하는 데 자주 사용된다. 이 경우 객체는 파괴될 때 잠금을 해제한다. 이 시나리오에서 RAII가 없으면 교착 상태 발생 가능성이 높아지고, 뮤텍스를 잠그는 로직이 잠금을 해제하는 로직과 멀리 떨어져 있게 된다. RAII를 사용하면 뮤텍스를 잠그는 코드는 RAII 객체의 스코프를 벗어날 때 잠금이 해제될 것이라는 로직을 본질적으로 포함한다.
또 다른 일반적인 예는 파일과의 상호 작용이다. 쓰기를 위해 열려 있는 파일을 나타내는 객체를 가질 수 있으며, 이 객체는 생성자에서 파일이 열리고 객체의 스코프를 벗어날 때 닫힌다. 두 경우 모두 RAII는 해당 자원이 적절하게 해제되도록 보장할 뿐이다. 예외 안전을 유지하기 위해서는 여전히 주의를 기울여야 한다. 데이터 구조나 파일을 수정하는 코드가 예외 안전하지 않으면, 데이터 구조나 파일이 손상된 채로 뮤텍스가 잠금 해제되거나 파일이 닫힐 수 있다.
동적으로 할당된 객체(C++에서 new로 할당된 메모리)의 소유권도 RAII로 제어할 수 있으며, 이 경우 RAII(스택 기반) 객체가 파괴될 때 객체가 해제된다. 이를 위해 C++11 표준 라이브러리는 단일 소유 객체를 위한 스마트 포인터 클래스 std::unique_ptr와 공유 소유 객체를 위한 std::shared_ptr를 정의한다. 비슷한 클래스는 C++98의 std::auto_ptr와 Boost 라이브러리의 boost::shared_ptr를 통해서도 사용할 수 있다.
또한 RAII를 사용하여 네트워크 자원에 메시지를 보낼 수 있다. 이 경우 RAII 객체는 초기화가 완료되는 생성자 끝에서 소켓으로 메시지를 보낸다. 또한 객체가 파괴되기 시작하는 소멸자 시작 부분에서도 메시지를 보낸다. 이러한 구조는 클라이언트 객체에서 다른 프로세스에서 실행 중인 서버와 연결을 설정하는 데 사용될 수 있다.
컴파일러 "정리" 확장 기능
[편집]클랭과 GNU 컴파일러 모음은 모두 RAII를 지원하기 위해 C 언어에 대한 비표준 확장 기능인 "cleanup" 변수 속성을 구현한다.[17] 다음은 변수가 스코프를 벗어날 때 호출할 특정 소멸자 함수로 변수를 주석 처리한다.
void example_usage() {
__attribute__((cleanup(fclosep))) FILE* logfile = fopen("logfile.txt", "w+");
fprintf("Hello logfile!", logfile);
}
이 예시에서 컴파일러는 `example_usage` 함수가 반환되기 전에 `logfile`에 대해 `fclosep` 함수가 호출되도록 준비한다.
제한 사항
[편집]RAII는 스택 할당 객체에 의해 (직접 또는 간접적으로) 획득되고 해제되는 자원에만 작동하며, 여기에는 잘 정의된 정적 객체 수명이 존재한다. 힙에 할당된 객체가 그 자체로 자원을 획득하고 해제하는 것은 C++를 포함한 많은 언어에서 흔하다. RAII는 힙 기반 객체가 가능한 모든 실행 경로를 따라 암시적으로 또는 명시적으로 삭제되어 자원 해제 소멸자(또는 동등한 것)를 트리거하도록 의존한다.[18]: 8:27 이는 모든 힙 객체를 관리하기 위해 스마트 포인터를 사용하고, 순환 참조 객체에 대해서는 약한 포인터를 사용하여 달성할 수 있다.
C++에서는 예외가 어딘가에서 잡힐 때만 스택 언와인딩이 발생한다고 보장된다. 이는 "프로그램에서 일치하는 핸들러를 찾을 수 없는 경우, terminate() 함수가 호출된다. terminate() 호출 전에 스택이 언와인딩되는지 여부는 구현 정의이다(15.5.1)." (C++03 표준, §15.3/9)이기 때문이다.[19] 이러한 동작은 일반적으로 허용된다. 운영 체제가 프로그램 종료 시 메모리, 파일, 소켓 등과 같은 나머지 자원을 해제하기 때문이다.
2018년 게임랩 컨퍼런스에서 조너선 블로는 RAII 사용이 메모리 단편화를 일으킬 수 있으며, 이는 차례로 캐시 미스와 성능에 100배 또는 그 이상의 타격을 줄 수 있다고 주장했다.[20]
참조 횟수 계산 방식
[편집]펄, 파이썬 ((C파이썬 구현에서)),[21] 그리고 PHP[22]는 참조 횟수 계산을 통해 객체 수명을 관리하며, 이는 RAII를 사용할 수 있게 한다. 더 이상 참조되지 않는 객체는 즉시 파괴되거나 최종화되어 해제되므로, 소멸자 또는 파이널라이저는 해당 시점에 자원을 해제할 수 있다. 그러나 이러한 언어에서 항상 관용적인 것은 아니며, 파이썬에서는 특히 권장되지 않는다(대신 컨텍스트 관리자와 weakref 패키지의 파이널라이저가 선호됨).
하지만 객체의 수명은 반드시 어떤 스코프에 묶여 있는 것은 아니며, 객체는 비결정적으로 파괴되거나 전혀 파괴되지 않을 수도 있다. 이로 인해 어떤 스코프의 끝에서 해제되어야 할 자원이 실수로 누출될 수 있다. 정적 변수(특히 전역 변수)에 저장된 객체는 프로그램이 종료될 때 최종화되지 않을 수 있으므로 해당 자원이 해제되지 않는다. 예를 들어 C파이썬은 이러한 객체의 최종화를 보장하지 않는다. 또한 순환 참조가 있는 객체는 단순한 참조 카운터에 의해 수집되지 않으며, 무기한으로 살아남게 된다. (더 정교한 쓰레기 수집기에 의해 수집된다고 해도) 파괴 시간과 파괴 순서는 비결정적이다. C파이썬에는 순환을 감지하고 해당 순환 내의 객체를 최종화하는 순환 감지기가 있지만, C파이썬 3.4 이전에는 순환 내의 어떤 객체라도 파이널라이저를 가지고 있다면 순환이 수집되지 않는다.[23]
같이 보기
[편집]각주
[편집]- ↑ 가 나 Stroustrup, Bjarne (2017년 9월 30일). “Why doesn't C++ provide a "finally" construct?”. 2019년 3월 9일에 확인함.
- ↑ Sutter, Herb; Alexandrescu, Andrei (2005). 《C++ Coding Standards》. C++ In-Depth Series. Addison-Wesley. 24쪽. ISBN 978-0-321-11358-0.
- ↑ “Gem #70: The Scope Locks Idiom” (영어). 《AdaCore》. 2021년 5월 21일에 확인함.
- ↑ The Valadate Project. “Destruction”. 《The Vala Tutorial version 0.30》. 2021년 5월 21일에 확인함.
- ↑ “RAII - Rust By Example”. 《doc.rust-lang.org》. 2020년 11월 22일에 확인함.
- ↑ Stroustrup 1994, 16.5 Resource Management, pp. 388–89.
- ↑ Stroustrup 1994, 16.1 Exception Handling: Introduction, pp. 383–84.
- ↑ Stroustrup 1994, 389쪽. I called this technique "resource acquisition is initialization."
- ↑ Arthur Tchaikovsky (2012년 11월 6일). “Change official RAII to CADRe”. 《ISO C++ Standard - Future Proposals》. Google Groups. 2019년 3월 9일에 확인함.
- ↑ Chou, Allen (2014년 10월 1일). “Scope-Based Resource Management (RAII)”. 2019년 3월 9일에 확인함.
- ↑ Richard Smith (2017년 3월 21일). “Working Draft, Standard for Programming Language C++” (PDF). 151, section §9.6쪽. 2023년 9월 7일에 확인함.
- ↑ “How can I handle a destructor that fails?”. Standard C++ Foundation. 2019년 3월 9일에 확인함.
- ↑ Richard Smith (2017년 3월 21일). “Working Draft, Standard for Programming Language C++” (PDF). 2019년 3월 9일에 확인함.
- ↑ Stroustrup, Bjarne; Sutter, Herb (2020년 8월 3일). “C++ Core Guidelines”. 2020년 8월 15일에 확인함.
- ↑ “I have too many try blocks; what can I do about it?”. Standard C++ Foundation. 2019년 3월 9일에 확인함.
- ↑ RAII at cppreference.com
- ↑ “Specifying Attributes of Variables”. 《Using the GNU Compiler Collection (GCC)》. GNU 프로젝트. 2019년 3월 9일에 확인함.
- ↑ Weimer, Westley; Necula, George C. (2008). “Exceptional Situations and Program Reliability” (PDF). 《ACM Transactions on Programming Languages and Systems》 30 (2).
- ↑ ildjarn (2011년 4월 5일). “RAII and Stack unwinding”. Stack Overflow. 2019년 3월 9일에 확인함.
- ↑ Gamelab2018 - Jon Blow's Design decisions on creating Jai a new language for game programmers - 유튜브
- ↑ “Extending Python with C or C++: Reference Counts”. 《Extending and Embedding the Python Interpreter》. 파이썬 소프트웨어 재단. 2019년 3월 9일에 확인함.
- ↑ hobbs (2011년 2월 8일). “Does PHP support the RAII pattern? How?”. 2019년 3월 9일에 확인함.
- ↑ “gc — Garbage Collector interface”. 《The Python Standard Library》. Python Software Foundation. 2019년 3월 9일에 확인함.
추가 자료
[편집]- Stroustrup, Bjarne (1994). 《The Design and Evolution of C++》. Addison-Wesley. Bibcode:1994dec..book.....S. ISBN 978-0-201-54330-8.
외부 링크
[편집]- 샘플 장: "Gotcha #67: Failure to Employ Resource Acquisition Is Initialization" by Stephen C. Dewhurst
- 인터뷰: "A Conversation with Bjarne Stroustrup" by Bill Venners
- 기사: "The Law of The Big Two" by Bjorn Karlsson and Matthew Wilson
- 기사: "Implementing the 'Resource Acquisition is Initialization' Idiom" by Danny Kalev
- 기사: "RAII, Dynamic Objects, and Factories in C++" by Roland Pibinger
- Delphi의 RAII: "One-liner RAII in Delphi" by Barry Kelly
- 가이드: C++의 RAII by W3computing