들어가는 말
- 프로젝트 단위: 말 그대로 한 개의 결과물을 생성하는 것을 목표로 하는 한 비주얼 C++ 프로젝트당 하나씩만 생성되는 파일이다. 리소스는 특수한 경우가 아니면 보통 프로젝트마다 하나만 있기 때문에, per-프로젝트인 것으로 간주된다.
- configuration 단위: 한 프로젝트 내에서 debug나 release 별로 따로 생성되고, x86이나 x64 같은 플랫폼별로 다 따로 생성되는 파일이다.
- 소스 단위: 번역 단위(translation unit)별로 다 제각각 생성되는 파일이다. configuration에도 물론 종속적이며, 다 따로 생성된다.
※ 프로젝트를 열면 생성되는 것
APS (프로젝트 단위)
전통적인 윈도우용 실행 파일(EXE/DLL)을 빌드하기 위해서는 잘 알다시피 컴파일된 코드뿐만 아니라 리소스도 같이 들어가는데, 그 리소스를 명시해 주는 '리소스의 소스', 일명 리소스 스크립트는 바로 *.rc 파일이다. 그리고 *.rc와 일반 소스 코드 *.cpp는 resource.h에 정의된 심벌들을 통해 동일 리소스를 식별하게 된다.
그런데 매번 일반 텍스트 형태로 된 rc 파일을 resource.h와 엮어서 파싱하자니 불편하다. 리소스 스크립트는 텍스트 에디터를 써서 사람이 손으로 편집한 뒤 컴파일하기에는 적합하지만, IDE 같은 소프트웨어가 자동으로 다뤄 주기에는 비효율적인 구조인 것이다.
그래서 비주얼 C++은 리소스 ID까지 포함하여 리소스 스크립트의 바이너리 representation을 따로 만들어 두고 지낸다. APS 파일이 존재하고 이게 RC나 H 같은 텍스트 소스에 비해 outdate되지 않았다면, 프로그램은 매번 텍스트를 파싱하는 게 아니라 APS 파일을 곧장 읽는다.
비주얼 C++에서 프로젝트를 처음으로 열어서 리소스 뷰로 리소스들을 처음 열람하면, 프로그램이 리소스 컴파일러를 가동해서 뭘 파싱하면서 시간이 오래 걸린다. 하지만 다음에 열 때부터는 리소스가 곧바로 빨리 열리는데, 이것이 바로 APS 파일 덕분이다.
CLW (프로젝트 단위) deprecated
이것은 비주얼 C++ 4~6 사이에, 그 이름도 유명한 MFC Class Wizard (클래스 마법사) 때문에 도입되었던 부가정보 파일이다.
MFC 클래스에서 파생된 윈도우 클래스 같은 데서 메시지 핸들러(마법사의 용도가 굳이 메시지 핸들러뿐인 건 아니지만)를 추가하려면 일단 헤더 파일에 afx_msg void OnXXXX가 추가되어야 하고, 메시지 맵 BEGIN_MESSAGE_MAP() 밑에 ON_MESSAGE_***가 추가되어야 하고, 끝으로 소스 파일에 해당 멤버 함수의 몸체가 추가되어야 한다.
그런데 이 일을 모든 소스 코드를 일일이 파싱하면서 추가 지점을 찾아서 하기란 여간 어려운 일이 아닐 수 없다.
C++은 선언 따로, 정의 따로이고(C#, 자바는 그렇지 않다) 정의부가 반드시 어느 번역 단위에 존재해야 한다는 제약이 전혀 없다. FM대로 하는 건 15년 전의 펜티엄 컴으로는 무리였다.
그래서 편의상 클래스의 선언부와 메시지 맵의 주변엔 클래스 마법사만이 식별하는 문자열이 들어간 주석이 있고, 클래스 마법사는 그 구간을 대상으로만 작업을 신속하게 했다. 그리고 그걸로도 부족해서 클래스 마법사의 파싱 결과가 CLW 파일에 들어갔다. 식별자 주석을 건드리면 클래스 마법사가 제대로 동작하지 못했다.
21세기에 나온 비주얼 C++ .NET과 그 이후 버전은 CLW 파일을 만들거나 사용하지 않으며, 클래스 마법사 주석 없이도 멤버 함수나 핸들러의 추가를 그럭저럭 정확하게 해낸다. 사실 클래스 마법사 자체가 비주얼 C++ 200x에서는 사라졌다가 2010에서부터 부활했다.
※ 빌드하면 생성되는 것
OBJ (소스 파일 단위)
비주얼뿐만이 아니라 전세계의 어느 C/C++ 컴파일러를 돌리더라도, 소스 코드를 컴파일하면 이것이 매 소스, 즉 번역 단위별로 생성된다. 소스 코드를 번역한 기계어 코드가 obj 파일 포맷에 맞게 들어있는데, 때로는 기계어 코드뿐만 아니라 각종 디버깅 정보와 링크 때 링커가 참고할 만한 메타데이터도 잔뜩 가미된다.
static library라고 불리는 LIB는 별개의 포맷이 아니라, 그냥 여러 번역 단위들을 컴파일한 obj들의 컬렉션일 뿐이다. obj를 단순히 lib로 합치기만 할 때는 링크 에러가 나지 않는다(즉, 선언된 심벌들이 반드시 정의되어야 할 필요가 없다.)
RES (configuration 단위)
리소스 스크립트를 컴파일하여 생성되는 결과물이다. 리소스 스크립트의 바이너리 최적화 형태인 APS와 무엇이 다르냐고 물으신다면, 차이가 적지 않다.
APS는 리소스 스크립트 파일의 표현 형태만 메모리 친화적으로 바꾼 것이기 때문에 ID_RADIO1 같은 상수 명칭의 문자열 원형과 심지어 조건부 컴파일을 위한 스펙까지 다 보존되어 있으며, 참조하는 비트맵 같은 데이터 파일도 파일명 형태로 존재한다. APS 파일로부터 RC 파일과 resource.h 파일을 복원해 낼 수 있다.
그러나 RES는 상수는 다 숫자로 박히고 참조하는 데이터 파일도 모두 내부에 embed되었으며, 이 상태 그대로 실행 파일에다 링크되어 들어가기만 하면 되는 상태인 것이다.
PCH (configuration 단위)
pre-compiled header인 stdafx.h와, 이에 대응하는 번역 단위인 stdafx.cpp를 컴파일하여 얻은 각종 컴파일 context들, 즉 함수와 클래스 선언, #define 명칭 등등을 바이너리 형태로 보관하고 있는 파일들이다. 이게 있으면 stdafx.h를 인클루드하라는 명령은 실제 헤더 파일을 파싱하는 게 아니라 그냥 pch 파일을 참조하는 것으로 대체된다.
컴파일러의 버전이 올라가고 각종 플랫폼 SDK의 크기가 커질수록 이 파일의 크기도 야금야금 커져 왔다. 이거 없이는 C++은 살인적인 인클루드질 때문에, 느린 빌드 속도를 도저히 감당할 수 없다.
PDB (configuration 단위)
빌드 결과 만들어진 EXE/DLL에서 기계어 코드의 어느 부분이 어느 소스의 몇째 줄에 대응하는지(소스 코드 자체는 없고 소스의 경로만), 이 함수에서 이 지역변수의 이름이 무엇인지 등을 담고 있는 디버그 정보 데이터베이스이다.
디버그 모드가 아니라 릴리스 모드로 빌드한 최적화된 실행 파일이라도, PDB 파일을 참조하게 하는 최소한의 정보만이라도 남겨 두면, 나중에 프로그램이 뻗는다거나 할 때 소스상으로 최소한 어느 지점에서 뻗었는지를 개발자의 컴에서 확인해 볼 수 있다. 개발자의 컴엔 직전에 이 바이너리를 빌드하면서 같이 생성된 PDB 파일이 존재하기 때문이다.
ILK (configuration 단위. 대개 디버그 빌드에서만)
증분 링크(incremental link)를 위한 context 정보가 들어있다.
이것은 프로그램의 빌드 속도를 올리기 위한 테크닉이다. 매번 링크를 처음부터 일일이 새로 하는 게 아니라, 처음에 빌드할 때 바이너리를 좀 여분을 둬서 듬성듬성 큼직하게 만들어 두고, 다음부터는 바뀐 obj 파일 내용만 기존 바이너리의 자기 지점에다 대체하는 방식으로 빌드를 신속하게 끝낸다. 혹은 뒷부분에다가 새로운 빌드 내용을 계속 추가해 넣기만 하고, 예전 빌드 내용을 무효화시키는 방법도 쓴다.
요즘 디버그 빌드가 단순히 최적화를 안 한 것 이상으로 릴리스 빌드보다 빌드된 바이너리의 크기가 유난히 큰 이유가 여기에 있다. 게다가 Edit and continue 기능을 위해서도 여분 공간이 필요하기 때문에 크기가 커질 수밖에 없다. 디버그 빌드 바이너리를 바이너리 에디터로 들여다보면, 온통 0xCC (no op)으로 도배가 되고 내부가 헐렁함을 알 수 있다.
MS 오피스도 2007 이전 버전을 보면 방대한 워드/엑셀 문서를 편집할 때 바뀐 내용만 짤막하게 저장하는 옵션이 있었다. 그게 일종의 증분 저장 기능이다. 지금은 그게 보안상으로 문제가 되기도 하고 문서 파일 포맷이 크게 바뀌었으며, 굳이 증분 저장을 안 써도 될 정도로 PC 성능이 좋아졌다고 여겨져서 그런 기능이 없어졌지만 말이다.
증분 링크는 보통은 디버그 모드 빌드에서만 쓰인다.
VC???.idb (configuration 단위. 대개 디버그 빌드에서만)
ILK 파일과 마찬가지로 빌드 시간의 단축을 위해 존재하는 파일이다.
디버그 모드로 빌드를 해 보면, 헤더 파일이 바뀌었더라도 해당 헤더를 인클루드하는 cpp 파일들이 전부 리빌드되는 게 아니라 가끔 'Skipping.. (no relevant changes detected)'이러면서 넘어가는 파일도 있다. 그리고 대체로 이런 컴파일러의 판단이 맞다. 헤더 파일을 고쳤더라도 클래스의 선언부 같은 크리티컬한 부분이 아니라 그냥 주석 같은 trivial한 부분만 바뀌었기 때문에 굳이 리빌드가 필요하지 않다는 걸 어떻게 판단할까?
컴파일러가 제공하는 Enable Minimal Rebuild (/Gm) 옵션 때문에 가능하다. 이게 지정되면 빌드 과정에서 프로젝트명이 아니라 고정된 이름의 의존성 판단용 부가정보 파일이 생긴다. ???는 해당 비주얼 C++의 버전이다. 2008의 경우 90, 2010의 경우 100.
정리하자면, 빌드와 함께 생성되는 파일들 중, 실제로 링커에 의해 EXE/DLL 따위를 만드는 데 동원되는 파일은 OBJ, RES이다.
빌드 시간을 단축시키는 데 쓰이는 파일은 PCH, IDB, ILK이다.
PDB는 프로그램의 문제 추적을 위해 추후에 쓰이는 파일이다.
※ 편의 기능 + 빌드
SBR (소스 파일 단위), BSC (configuration 단위)
자, 이 파일은 빌드를 하면 생성되지만, 프로그램의 빌드나 디버깅을 위해서 반드시 생성해야만 하는 파일은 아니다.
방대한 양의 소스 코드를 컴파일하고 나면 컴파일러는 그 소스 코드의 모든 내부 구조에 대해서 알게 된다. 그걸 알아야만 기계어 코드를 생성할 수 있을 테니까.
컴파일이 끝났다고 그 정보를 그냥 버리는 건 아깝기 때문에, 일정한 파일 포맷을 제정하여 이것을 소스 코드에 대한 browsing에 활용할 수 있다. 가령, 이 클래스 멤버 함수의 정의는 어디에 있고, 이 함수가 호출하는 함수와, 이 함수를 호출하는 함수와의 그래프 관계는 어떻고 하는 것 말이다. 소스 코드가 텍스트라면, browse 정보는 정교하게 짜여진 색인인 셈이다.
이 개념과 파일 포맷은 비주얼 C++의 아주 초창기 시절부터 존재했다.
그리고 비주얼 C++은 버전 6까지는, 프로젝트를 빌드할 때 browse 정보도 같이 이렇게 덤으로 빌드되게 해서 browse 정보를 조회하는 기능을 갖추고 있었다. SBR과 BSC의 관계는 C/C++ 소스 코드에서 OBJ와 EXE의 관계와 정확히 같다. 한 번역 단위를 컴파일하면 한 SBR이 생겼고, SBR들을 뭉쳐서 BSC 파일이 생성되었다.
물론 이렇게 하면 빌드 시간이 더욱 길어졌고, 굳이 browse 기능을 쓰지 않는 사람도 있었기 때문에 이 기능은 철저히 선택사항이었다. 그리고 닷넷부터는 이 정보를 만들지 않더라도, 뒤에서 설명할 인텔리센스 정보만으로 IDE 차원에서 browse 대체 기능을 갖추기 시작했다.
※ 인텔리센스
NCB (프로젝트 단위) deprecated
sbr/bsc보다는 나중에, 시기적으로는 clw와 비슷한 타이밍(비주얼 C++ 4)에 만들어진 파일 포맷이다.
바야흐로 비주얼 C++ 4에서는 최초로 Class View라는 게 생겨서 프로젝트에 존재하는 모든 클래스와 멤버, 전역 변수/함수들을 표시하는 기능이 추가되었다. ncb는 browse 정보를 만드는 것만치 소스를 심도 있게 일일이 다 까 보지는 않고, 그보다는 단순하게 코드를 파싱하여 해당 기능을 빠르게 구현하는 데 필요한 부가 정보를 저장했다.
Class View가 도입되었던 초창기에는 소스 코드를 매번 빌드는 아니어도 저장을 해야만 컨텐츠가 업데이트되었다. 그나마 저장하지 않고도 실시간으로 업데이트가 되기 시작한 건 VC 6부터이다.
그리고 VC 6에서는 잘 알다시피 초보적인 수준의 인텔리센스 및 멤버 표시/자동 완성 기능이 구현되었고, 그 정보 역시 ncb 파일에다 저장되었다. 당연히 같은 프로젝트를 만들어도 ncb 파일의 크기는 더욱 커지게 됐다.
비주얼 C++이 버전업되면서 인텔리센스는 성능이 더욱 강력해졌다. 바로 닷넷에서부터는 #define 심벌이 추가로 인텔리센스의 혜택을 입기 시작했으며 템플릿도 제대로 지원되기 시작했다. 오동작 빈도도 더욱 줄었다.
그러나 이 모든 것은 여전히 10년 전의 ncb 파일을 기반으로, 진품이 아닌 가짜 parser를 임기응변 식으로 확장하면서 구현된 것이기 때문에, 어느 수준 이상의 정확도를 낼 수는 없었으며 복잡한 C++ 문법의 모든 것을 수용하는 데에도 근본적인 한계가 있었다.
가령, 클래스 멤버 함수의 선언이 복잡한 #define 매크로 안에 숨어 있으면 Class View에 이것이 제대로 나타나지 않았다. 갑자기 빌드 configuration이나 플랫폼을 확 바꿔 버리면 인텔리센스가 멘붕을 일으켰으며, 복잡한 조건부 컴파일 구간에 숨어 있는 코드도 인텔리센스가 상황 파악을 제대로 못 하는 경우가 많았다. 멘붕의 정도가 심하면 인텔리센스가 아예 동작을 멎어 버리기도 했기 때문에, 수시로 ncb 파일을 지우고 다시 만들어 주는 건 필수 작업이었다.
SDF (프로젝트 단위), IPCH (configuration 단위)
위와 같은 기존 ncb 기반 인텔리센스의 문제를 극복하고자 비주얼 C++ 2010은 안 그래도 C++11 때문에 문법도 대폭 확장해야 하는데 이 기회에 인텔리센스 엔진을 완전히 갈아 엎었다. SQL server compact edition이라는 전문 DB 엔진을 쓰기 시작했다.
2010부터는 가짜 parser가 아니라 진짜 컴파일러와 똑같은 수준의 parser가 background에서 모든 소스와 헤더 파일들을 일일이 파싱하여 실시간으로 심벌 정보를 고친다. 정확한 문맥을 파악하고 있기 때문에 100% 정확한 인텔리센스가 제공되며, 예전처럼 좀 오동작한다 싶어도 잠시 기다려서 파싱 정보가 갱신되고 나면 곧장 똑바로 동작하기 시작한다.
다만, 이런 첨단 기술이 공짜로 된 건 아니기 때문에, 어지간한 C++ 프로젝트는 이제 인텔리센스 파일만 수십~100수십 MB씩 디스크를 쳐묵쳐묵 하는 대가를 감수해야 한다. 어느 프로젝트를 열든지 동일하게 공유되는 MFC나 플랫폼 SDK의 인텔리센스 정보는 여러 프로젝트들이 한데 공유만 할 수 있어도 인텔리센스의 용량이 크게 줄어들 텐데, 무척 아쉽다.
그래도 비주얼 C++ 제작진에서 일말의 배려를 했다 싶은 대목은, 인텔리센스 DB 파일이 생성되는 곳만 한 곳에 따로 대체 지정이 가능하다는 것이다. 프로젝트-옵션이 아니라 도구-옵션에서 “텍스트 편집기-C/C++/고급”으로 가면 fallback location을 지정하는 옵션이 있으며, 이것만 해 주면 비주얼 C++로 만드는 모든 프로젝트들의 인텔리센스 DB는 거기 아래로 한데 모이게 된다.
이렇듯, 비주얼 C++ IDE나 컴파일러가 생성하는 보조 파일들의 용도와 배경에 대해서 공부하면 C/C++ 언어의 특성을 알 수 있고, 프로그래밍 언어에 대한 비판적인 안목, 그리고 언어의 비효율을 극복하고 조금이라도 개발 도구의 생산성을 올리기 위해 해당 제작진이 어떤 꼼수를 동원했는지에 대해서도 알 수 있다.
Posted by 사무엘