내용 추가 RAII에 관련하여 위키북스 내용도 번역했습니다. 위키미디어 문서보다는 위키북스 내용이 더 좋으므로 위키북스 내용도 꼭 보시기 바랍니다.
들어가며
C++을 처음 접한 건 1998년… 본격적으로 사용한 것은 2008년… 본격적으로 사용했다고 해 봤자, 그냥 기능별로 Class로 만든다던가, 자료 구조를 STL로 쉽게 만드는 정도였다. 지금 개발 중인 C++ 프로그램은 수만 라인인데 C++에 이해 없이 바쁜 상태로 개발하다보니 너무 조잡하다. 더 이상 유지보수가 어렵다.
그래서 작년부터 C++ 공부를 다시 시작하고 있는데 최근 C++ 동향은(말이 최근이지 이미 1990년대 후반부터 나왔던 것들이지만) Modern C++이라고 해서 몇개의 programming idiom들로 구성된다.
개발로 계속 밥벌어 먹으려면 지금 나의 Skill로는 부족하다. MySQL은 잘 나가는 DBA보다는 부족할 수 있지만 일반적인 개발자 수준에서는 부족하지는 않다고 생각된다.
그래서 올해는 C++ 공부에 집중하려고 한다. 그 첫번째 공부가 RAII (Resource Acquisition is Initialization)이다. 내가 스스로 RAII에 대해서 설명하기는 어렵고, 영문 위키피디어에 있는 내용을 번역해 보려고 한다.
번역 내용
원문 URL : http://en.wikipedia.org/wiki/Resource_Acquisition_Is_Initialization
Resource Acquisition is Initialization (RAII)는 C++, D, Ada, Vala와 같은 다양한 객체지향 언어에서 사용되는 프로그래밍 이디엄 (Idiom, 한국말로 적당한 것이 없어서 그냥 이디엄이라고 하겠다.) RAII는 C++ 창시자인 Bjarne Stroustrup이 만들었으며 C++에서 자원의 할당과 해제를 담당한다. C++에서 Exception이 발생할 때 적절한 Exception 처리가 발생하도록 한다. 객체가 선언된 scope를 벗어날 때 자동으로 호출되는 유일한 함수는 소멸자뿐이다. 따라서 객체의 자원을 자동으로 할당하고 해제하기 위하여 자원 관리는 객체의 생명 주기와 엮는 것이 좋다. 초기화 단계에서 자원이 획득되고 소멸 단계에서 자원이 해제된다면 에러가 발생하더라도 자원이 항상 해제되는 것을 보장할 수 있다. (역주 : 소멸자에서 Exception이 발생할 수 있으므로 항상 보장되지는 않을 듯 한데.. 물론 생성자에서도 Exception이 발생할 수 있으므로 주의해야 하고)
RAII는 예외처리에 대한 안전한 C++ code를 작성하는데 필수적이다. Exception 발생 시 자원을 소유한 객체의 소멸자가 호출되므로 소멸자에서 자원 해제를 잘 한다면 Exception을 처리하는 중복된 코드를 없앨 수 있다.
일반적인 사용처
RAII 디자인은 다중 쓰레드 어플리케이션에서 mutex lock을 처리하는데 자주 사용된다. 그러한 경우 객체가 사라질 때 lock이 자동으로 해제된다. RAII가 아니었다면 dead lock이 발생할 가능성이 높으며 mutex lock을 처리하는 로직과 lock을 해제하는 로직이 멀리 떨어져있을 수 있다. RAII을 이용하는 경우 mutex lock을 하는 코드는 lock을 해제하는 코드를 자연스럽게 포함하게 된다.
다른 예로서는 파일을 처리하는 예를 들 수 있다. 파일 처리를 하는 객체가 존재한다고 가정하자. 해당 객체는 RAII를 이용하여 파일은 생성자에서 열리고, 소멸자에서 닫히게 되도록 설계되었다면 사용 중인 자원은 자동으로 해제될 것이다.
동적 할당된 객체의 소유권 또한 RAII를 이용하여 처리될 수 있다. 이러한 목적으로 C++11 표준 라이브러리에서는 스마트 포인터를 정의하고 있다. std::unique_ptr은 단일 소유객체에 사용되고, std::shared_ptr은 공유된 소유권에 관련된 객체에 사용된다. 비슷하게 C++98의 std::auto_ptr와 Boost 라이브러리의 boost:::shared_ptr이 존재한다.
C++ 예
다음의 C++ 코드는 파일 접근과 mutex lock에서 RAII의 사용을 보여주는 예이다.
#include <string>
#include <mutex>
#include <iostream>
#include <fstream>
#include <stdexcept>
void write_to_file (const std::string & message) {
// mutex to protect file access
static std::mutex mutex;
// lock mutex before accessing file
std::lock_guard<std::mutex> lock(mutex);
// try to open file
std::ofstream file("example.txt");
if (!file.is_open())
throw std::runtime_error("unable to open file");
// write message to file
file << message << std::endl;
// file will be closed 1st when leaving scope (regardless of exception)
// mutex will be unlocked 2nd (from lock destructor) when leaving
// scope (regardless of exception)
}
앞의 코드는 예외 안전하다. C++이 Scope가 종료될 때 Stack에 있는 모든 객체의 소멸자를 호출하기 때문이다. 이는 stack unwinding이라고 불린다. lock과 file 객체의 소멸자는 함수가 종료될 때 항상 호출되는데 Exception이 발생하든 발생하지 않든 소멸자는 항상 호출된다.
단일 함수에 존재하는 지역 변수는 다중 자원 관리를 쉽게할 수 있다. 지역 변수들은 생성된 순서의 반대로 소멸된다. 또한 객체는 완벽히 생성된 경우에만 소멸자가 호출된다. (역주:위키북스에 관련 내용이 설명되어 있습니다.)
RAII를 이용하면 자원 관리를 획기적으로 단순화시킬 수 있다. 전체 코드 크기를 줄일 수 있으며 프로그램의 정확성을 확실히하는데도 도움을 준다. 따라서 C++에서 RAII는 적극 추천된다. 대부분의 C++ 표준 라이브러리들은 RAII 이디엄을 따르고 있다.
RAII를 사용하지 않는 자원 관리
Dispose 패턴 가비지 컬랙션을 지원하는 Java, C#, Python 같은 언어들은 다양한 형태의 dispose 패턴을 지원하여 자원 해제를 단순화하고 있다.
이하 생략
Reference counting Perl, CPython, PHP들은 참조 카운트를 이용하여 객체의 생명 주기를 관리한다.
이하 생략
C를 위한 GCC 확장 GNU Compiler Collection은 RAII를 지원하기 위하여 C 언어의 비표준 확장인 “cleanup” 변수 속성을 구현했다.
번역 후기
RAII의 Naming에 대하여… RAII의 컨셉과 이름이 상이하기 때문에 RAII를 이해하는데 헷갈렸었다. RAII의 개념은 객체가 scope를 벗어나게 될 때 객체가 사라지게되면서 소멸자가 항상 호출되기 때문에 자원 해제 코드를 소멸자에 넣자는 것으로 볼 수 있다. 따라서 Resource Acquisition is Initialization라는 이름보다는 Resource De-allocation is Destruction이 더 어울릴 듯 하다. 구글에서 “RAII misleading name”으로 검색해 보면 비슷한 글이 있는데 이를 읽어보는 것도 RAII 개념을 익히는데 도움이 될 것이라 생각된다.
위키피디어 문서의 품질 일단 번역은 시작했지만, 별로 내용이 좋지를 못하다. 좀 더 좋은 내용을 찾아서 번역해 보려고 한다.