1.
<날개셋> 한글 입력기의 개발 작업은 단순히 새로운 기능을 구현하거나 알려진 버그를 수정하는 것 말고도, 멀쩡히 동작하는 기능의 내부 구현 형태를 바꾸는 리팩터링도 무시 못 할 비중을 차지하고 있다.
이미 지금도 문제가 없긴 하지만, 열기-닫기 내지 할당-해제 같은 패턴은 어지간하면 클래스의 생성자와 소멸자가 알아서 하게 바꿔서 리소스 누수(leak)를 컴파일러 차원에서 원천적으로 차단하고 있으며,
최근에는 비주얼 C++ 2010으로 갈아탄 덕분에 지저분한 임시 #define 함수들을 지역 변수 형태의 람다 함수로 교체하는 재미가 쏠쏠하다. 예를 들어 이런 것 말이다.
BEFORE
#define PickNumber(e) ((e)[1] ? wcstol((e), &f, 16): *(e))
AFTER
auto PickNumber = [&f](PCWSTR e) -> int { return e[1] ? wcstol(e, &f, 16): *e; };
별도의 함수로 추가하기에는 너무 지엽적이고 한 함수 안에서만 잠깐 쓰고 마는 반복적인 루틴들은 람다로 싸 주는 게 딱이다. type-safety가 보장되고, scope도 엄격하게 정해지고, 이 루틴을 매번 인라인으로 expand할지 아니면 그냥 함수 호출로 처리해서 코드 크기를 줄일지를 컴파일러가 좀 더 유연하게 판단할 수 있기 때문에 아주 좋다.
예전에는 C++에 대해서 C with classes라고 배웠겠지만, 이제는 C++은 C with classes라고만 정의하기에는 설명에 누락된 요소가 너무 많아졌다.
람다 함수를 전역 변수로 선언하는 건 문법적으로 불가능하지는 않으나, 그럴 바에야 그냥 재래식 형태의 함수를 하나 선언하고 말지 아무런 특별한 의미가 없을 것이다.
2.
그런데, 이렇게 리소스 누수를 막기 위해서 노력하고 있지만 구조체에다 함께 넘어온 핸들이나 메모리 포인터는 그것만 따로 클래스의 소멸자가 자동으로 소멸하게 할 수 없으니 구조적으로 여전히 누수 위험이 존재한다.
예를 들어 CreateProcess 함수는 실행 후 해당 프로세스에 대한 핸들을 PROCESS_INFORMATION 구조체에다가 되돌려 준다. 이 값을 이용해서 프로그램은 자신이 새로 실행한 프로그램이 실행이 끝날 때까지 기다린다거나 할 수 있다. 하지만 실행된 프로세스가 종료되더라도 그 프로세스를 가리키던 핸들은 해제되지 않는다. 호스트 프로그램이 핸들을 닫아 줘야만 완전히 해제된다.
CreateProcess 함수를 자주 쓴다면 핸들 해제까지 모든 작업을 자동화해 주는 클래스를 만들어서 쓰는 게 효과적이다. 사실, 이 함수는 받아들이는 인자가 많고 기능 한번 쓰는 게 번거로운 편이기 때문에 클래스를 쓸 법도 하지만, 어쩌다 한 번 쓰고 마는 특수한 함수를 전부 클래스로 감싸는 건 좀 낭비처럼 보이는 게 사실이다.
<날개셋> 편집기에는 있으나마나한 잉여이긴 하지만 명색이 텍스트 에디터이다 보니 인쇄 기능이 있다.
그런데 한때는 인쇄를 한 뒤에 자신이 사용한 프린터 DC를 해제하지 않아서 GDI 개체 누수가 생기는 버그가 있었다.
물론 이건, 리소스 제한이 있는 윈도우 9x에서 이 프로그램을 한 번 실행한 후, 프린터 드라이버를 이용한 인쇄(화면 인쇄 말고) 명령을 연달아 몇백, 몇천 번쯤 내려야(한 번에 몇백, 몇천 페이지를 인쇄하는 것과는 무관) 여파가 나타날 버그이니, 현실적으로는 아무런 위험이 아닌 것이나 마찬가지이다.
이 문제의 원인은 PrintDlg 함수가 PRINTDLG 구조체에다가 넘겨준 hDC 멤버(프린터 DC)를 해제하지 않아서였다.
그런데 이런 실수가 들어갈 법도 했던 게, MSDN에서 해당 함수나 해당 구조체에 대한 설명 어디에도, 사용이 끝난 DC를 처분하는 것에 대해서는 언급이 없었다.
이거 혹시 해제해야 하는 게 아닌지 미심쩍어서 내가 우연히 잉여질 차원에서 다른 예제 코드를 뒤져 본 뒤에야 DeleteDC로 해제를 해야 한다는 걸 알게 되었고, 예전 코드에 리소스 누수 버그가 있음을 깨달았다.
하긴, 내 기억이 맞다면, COM 오브젝트도 프로그래머가 Release를 제대로 안 해서 개체 누수가 하도 많이 생기다 보니 MS에서도 골머리를 썩을 정도였다고 하더라. 현실은 이상대로 되질 않는가 보다.
3.
윈도우 운영체제의 device context에 대해서 보충 설명을 좀 할 필요를 느낀다.
DC라는 건 그림을 그리는 매체가 (1) 화면, (2) 메모리(대부분은 화면에다 내보낼 비트맵을 보관하는 용도), 아니면 (3) 프린터 이렇게 셋으로 나뉜다. 화면용 DC는 GetDC나 GetWindowDC를 통해 HWND 오브젝트로부터 얻어 오고 해제는 ReleaseDC로 한다.
그러나 나머지 두 DC는 화면 DC와는 달리, DeleteDC로 해제한다는 차이가 있다. 화면용 DC는 운영체제가 통합적으로 관리하는 성격이 강한 반면, 나머지는 전적으로 사용자 프로그램의 재량에 달린 비중이 커서 그런 것 같다.
메모리 DC는 화면 같은 다른 물리적인 매체 DC와 연계를 할 목적으로 만들어지는 가상의 DC이므로, 보통 CreateCompatibleDC를 통해 이미 만들어진 DC를 레퍼런스로 삼아서 생성된다. 레퍼런스 DC가 무엇이냐에 따라서 하다못해 pixel format 같은 거라도 차이가 날 수 있기 때문이다.
그 반면 프린터 DC는 대개 가장 수준이 낮은 CreateDC를 통해 만들어지는데, 응용 프로그램이 특정 디바이스를 지목해서 DC를 하드코딩으로 생성하는 경우는 거의 없고 보통은 사용자에게 인쇄 대화상자를 출력한 뒤에 운영체제의 GUI가 생성해 주는 DC를 그대로 사용하면 된다.
사실, 프린터야 인쇄 전과 인쇄 후에 프린터에다 초기화 명령을 내리고 종이를 빼내는 등 여러 전처리· 후처리 작업이 필요하고 그런 저수준 명령은 프린터 하드웨어의 종류에 따라 다 다르다.
메모리는 프린터만치 하드웨어를 많이 가리지는 않겠지만, 그래도 그래픽을 보관하기 위해 메모리를 할당하고 나중에 해제하는 작업이 필요하다.
그에 반해 단순히 화면에다가 그림을 찍는 건 각 context별로 좌표를 전환하고 클리핑 영역 설정을 바꾸는 것 외에는 별다른 오버헤드가 존재하지 않는다. 도스 시절의 그래픽 라이브러리는 그런 DC 같은 추상화 계층 자체가 아예 존재하지도 않았으니 말이다.
그런 오버헤드의 위상이 ReleaseDC와 DeleteDC의 차이를 만든 것 같다.
Posted by 사무엘