[build] 빌드 시간에 관한 고찰
제가 사용하고 있는 tip 을 간략하게 정리해 보도록 하겠습니다.


1. 코딩 습관




왜 헤더 파일에 많은 링크를 걸면 안되는가


헤더 파일에 다른 헤더를 주렁주렁 다는 것은 좋지 않은 습관입니다. 물론, cpp 에도 불필요한 헤더를 추가하면 할 수록 빌드 시간도 길어지고 코드도 지저분해 집니다만, 헤더에서는 보다 큰 문제가 될 수 있습니다. 

cpp 는 그 자체로 .obj 파일로 묶입니다. 따라서 cpp 에서 구현한 내용은 다른 모듈에게는 보이지 않습니다. class 를 구현할 때 public, private 를 사용하는 것처럼 cpp 는 private 인 셈이죠.

하지만 헤더는 다릅니다. 여기 저기서 가져다 쓸 수가 있는 것은 헤더의 특징입니다. 가벼운 헤더를 만들었다고 하더라도 무엇을 인클루드 하느냐에 따라 순식간에 무거운 헤더로 변질될 수 있습니다. 상속의 개념과 비슷하죠. 부모의 특징을 상속받는 것 처럼 다른 녀석을 인클루드 하면 능력을 상속 받음과 동시에 무거움도 함께 상속받게 됩니다. 

무거운 헤더를 피할 수 있는 가장 간단한 해결 방법은 헤더에 불필요한 모듈을 인클루드 하지 않는 것입니다. cpp 에서 인클루드 해도 무방한 녀석들이 있다면 cpp 로 인클루드를 옴기는 것이 좋습니다. 


전방 선언


앞서 이야기했던 방법을 사용했지만 아직도 많은 양의 모듈을 헤더에서 인클루드해야 할 것입니다. 구현(cpp)을 위해 필요한 헤더도 있지만 선언(header)을 위해서도 인클루드가 필요하기 때문이죠. 

전방 선언을 사용하면 모듈을 인클루드 하지 않고도 컴파일 할 수 있습니다. 전방 선언은 다른데에서 인클루드 했으니 이런 녀석이 있다는 것만 알고 있어라 라는 뜻으로 사용합니다. 우리는 이미 전방 선언에 대해 알고 있습니다. 아래 코드를 보시죠. 

// main.cpp
class A 
{
public:
    int childPosition;
};

class B
{
public:
    void goHome();
    A a;
};

void B::goHome()
{
    a.childPosition = 0;
}

int main()
{
    B b;
    b.goHome();

    return 0;
};

전방 선언에 대한 이해를 돕기 위해 이 예제를 전방 선언을 사용해서 다시 만들어 보겠습니다. 주요 관전 포인트는 "class A 를 나중에 선언하고 싶다" 입니다.

// main.cpp
class A;
class B
{
public:
    void goHome();
    A a;
};

void B::goHome()
{
    a.childPosition = 0;
}

class A 
{
public:
    int childPosition;
};

int main()
{
    B b;
    b.goHome();

    return 0;
};

이것이 전방 선언입니다. 어때요 참쉽죠?:D 
하나의 파일(main.cpp) 에 몰려 있던 class A, B 를 적당하게 분리해 보도록 하겠습니다.

// A.h
class A 
{
    int childPosition;
};

// B.h
#include "A.h"

class B
{
public:
    void goHome();
    A a;
};

// B.cpp
#include "B.h"

void B::goHome()
{
    a.childPosition = 0;
}

// main.cpp
#include "B.h"

int main()
{
    B b;
    b.goHome();

    return 0;
};

A.h 는 매우 간단한 파일이지만 무거운 헤더라고 한번 가정해 봅시다. B.h 는 매우 단순한 녀석이었는데 A.h 를 알게 되면서 부터 복잡한 녀석이 되었습니다. 전방 선언을 이용해서 B.h 를 다시 단순하게 만들고 A.h 는 main.cpp 에서만 포함하도록 만들고 싶습니다.

// A.h
class A 
{
    int childPosition;
};

// B.h
class A;
class B
{
public:
    void goHome();
    A a;
};

// B.cpp
#include "B.h"

void B::goHome()
{
    a.childPosition = 0;
}

// main.cpp
#include "B.h"

int main()
{
    B b;
    b.goHome();

    return 0;
};

아쉽게도 위와 같은 방법은 사용할 수가 없습니다. 왜 그럴까요? 전방 선언은 한 가지 제약 조건을 가지고 있습니다. 헤더를 왜 인클루드 해야 하는지 생각해 보면 답을 알 수 있는데요, 컴파일 시점에 컴파일러에게 사용할 클래스를 확실하게 알려주는 것이 목적이죠. 그래야 컴파일을 할 수 있거든요. 하지만 위 코드에서 B.cpp 를 컴파일 하는 시점에 컴파일러는 A 의 모양을 추측할 수가 없습니다. 그렇다면? 제가 앞서 언급한 바와 같이 #include "A.h" 를 헤더에서 cpp 로 옴겨놓으면 되겠죠. 일부 컴파일러에서는 이정도로 문제를 해결할 수 있을 지도 모릅니다. 대충 알아서 컴파일 하거든요. 하지만 이 것도 역시 표준에 맞지 않는 방법입니다. 왜 그럴까요? 

B.h 를 사용하는 녀석들에서 A 가 노출되고 있기 때문입니다. 정확하게 이야기 하지 않으면 컴파일러가 혼동할 수 있는 상황이 생기겠군요. 그렇다면 원점으로 돌아가서 B.h 에 A.h 를 포함하는 방법밖에 없는 걸까요?

컴파일러의 특징을 이용한 꽁수가 하나 있긴 합니다. A 를 포인터로 사용하는 것입니다. 포인터에 대한 정의는 전방 선언만으로 헤더에서 사용가능 합니다.최종 코드는 이런 식이 되겠군요.

// A.h
class A 
{
    int childPosition;
};

// B.h
class A;
class B
{
public:
    void goHome();
    A* a;
};

// B.cpp
#include "B.h"
#include "A.h"

void B::goHome()
{
    a->childPosition = 0;
}

// main.cpp
#include "B.h"

int main()
{
    B b;
    b.goHome();

    return 0;
};

정리해보죠. 전방 선언으로 사용하는 클래스는 전체 디자인을 잘 하신다면 하나의 파일에 몰아둘 수 있을 것입니다. 엔진 형태의 프로젝트를 만들고 계신다면 헤더에 노출하고 있는 모든 클래스가 되겠군요. 실제로 Ogre 와 같은 오픈 소스 게임엔진의 경우 OgrePrerequisites.h 라는 헤더 파일이 있습니다.

...(중략)...
namespace Ogre {
class Vector3;
class Vector2;
class Camera;
...(중략)...
}

또한 모든 헤더에서 이 파일을 인클루드 하고 있죠. 부득이 하게 헤더에서 포인터가 아닌 녀석을 사용한다면? 기존처럼 그저 인클루드 해서 사용하시면 됩니다. 방법이 없네요.


인라인의 문제점


같은 맥락에서 생각해 보면 인라인 코드의 문제점은 쉽게 알 수 있습니다. 인라인 코드가 헤더에 정의하는 "구현" 이라는 것은 아마 다 알고 계실 겁니다. 이 녀석은 컴파일 시점에 함수 진입 없이 접근할 수 있는 코드가 됩니다만, 헤더에 구현이 되어 있으므로 이 녀석을 포함하는 녀석들이 그만큼 뚱뚱해 지는 것을 피할 수 없습니다. 따라서 빠른 빌드를 위해 인라인을 최소화 할 필요가 있습니다. 즉, "꼭 필요한 경우에만" 사용하자는 것이죠.


템플레이트의 유일한 문제점


템플레이트를 잘 모르는 동료들과 함께 작업하게 될 경우, 양처럼 순한 동료들이 여러분이 만든 암호같은 코드를 보면서 자신도 모르게 욕을 할 수도 있다는 점은 템플레이트를 사용하면서 겪게 되는 너무나도 큰 아픔이 아닐 수 없습니다. 하지만 문제는 더 작은 쪽에 있습니다. 너무 사소해서 대부분 신경을 안쓰는 부분입니다만, "컴파일 시간이 다소 느려질 수 있다" 는 문장으로 대부분 책 귀퉁이에 적혀 있고 심지어는 작가의 유머와 함께 쓰여있어 전혀 심각하게 와 닿지 않는다는데 문제가 있습니다.

물론 이 모든 문제점들을 감수하고도 남을 정도로 템플레이트는 강력함을 가지고 있습니다. 하지만 이 문제는 상황에 따라서 크게 문제가 될 수 있으므로 최소한 알고 있어야 하며, 뒤에 이야기할 precompiled header 혹은 typedef 형태로 완전한 조립 객체를 노출하는 것이 빌드 속도에 도움이 될 것입니다.




1.2. 미리 컴파일된 헤더




미리 컴파일된 헤더?


시스템에 따라서 부르는 이름이 다릅니다만, Visual Studio 에서는 미리 컴파일된 헤더(Precompiled Header) 라고 불리는 녀석이 있습니다. 이름에서 알 수 있듯이 매번 빌드할 필요가 없는 녀석들(안정화 버전이나 windows.h 와 같은 녀석들)을 미리 컴파일 해두자는 것이 모티브죠.

혹시 boost library 를 사용하신다면 이 녀석들을 미리컴파일된 헤더에 등록하겠다는 생각은 버리시는 것이 좋을 것 같습니다. boost 는 생각보다 훨씬 커다랗고 템플레이트로 되어 있어서 미리 컴파일된 헤더로 올리면 pch 파일이 어마어마하게 커지고 말겁니다.


간단 사용 방법


vs2008 로 설명 드리겠습니다. vs2005 는 조금 다르긴 한데.. 지면 관계상 vs2008 만 설명합니다. 또 하나 vs2008 을 소개하는 이유가 있는데요, vs2008 에서 설정 방법이 조금 바뀌었는데 모르시는 분들이 많은 것 같아서 소개합니다.

1. 프로젝트 속성창을 열어 C/C++ 카테고리의 Precompiled Headers 를 엽니다.
2. Create/Use Precompiled Header 를 Use Precompiled Header 로 설정합니다.
3. 미리 컴파일된 헤더로 사용할 헤더를 Create/Use PCH Through File 에 등록합니다.
4. 이제 이 프로젝트에 포함된 모든 cpp 파일은 3번에서 등록한 헤더 파일을 가장 먼저 인클루드 해야만 합니다.
5. 미리 컴파일된 헤더는 *.obj 가 아닌 *.pch 로 컴파일이 됩니다. 또한 이 컴파일은 프로젝트에서 가장 먼저 컴파일이 될 것입니다. 이 컴파일을 위해 새로운 cpp 파일을 추가하고 3 번에서 등록한 precompiled header 를 인클루드 합니다. 여기가 중요한 부분인데요, solution explorer 에서 추가한 cpp 파일의 속성창으로 들어가서 C/C++ 카테고리의 Precompiled Header 의 Create/Use Precompiled Header 를 Create Precompiled Header 로 수정(오직 이 cpp 파일만이 create 한다는 설정입니다)해야 합니다.

vs2005 는 한번 create 하고 쓰거나 auto 라는 설정이 있었는데 vs2008 로 오면서 auto 가 사라진 대신 위와 같이 설정 가능하게 변경되었습니다.


Stable


안정화 버전을 묶어서 미리컴파일된 헤더에 넣어두면 빌드 속도에 확실한 도움을 줄 수 있습니다. 단, 이와 같은 형태로 프로젝트를 만들기 위해서는 깔끔한 형태의 개발이 선행되어야 한다고 생각합니다. 그렇지 않고 이거 저거 모아서 미리 컴파일된 헤더에 넣거나 한다면 계속 미리 컴파일된 헤더를 생성하거나 비슷한 종류의 비효율적인 경험을 겪게 될 것입니다. 즉, 이제까지 언급했던 언어/기능 적 관점을 넘어서 아키텍쳐에 대한 접근이 이루어 져야 하는 것이죠.

모듈화


모듈화는 상당히 애메한 단어인데요, 제가 말하고 싶은 것은 프로젝트 분리 정도가 될 것 같습니다. 역시 아키텍쳐와 연관이 깊은데요. 게임의 경우 FSM(유한상태오토마타) 만 불리하거나 데이터 제어, 네트워크, 사운드 모듈 등 각각을 큰 덩어리로 조각내서 개발 및 관리해야 한다는 것입니다. 각각이 엔진 형태로 서로간의 종속성이 없다면 더더욱 좋겠죠.


Plugin


일반적인 용어는 아닌데요, Ogre3D 라는 엔진에서 사용하고 있는 말이라 사용해 보았습니다. 모듈화에서 언급했던 내용과 연관이 있는 내용입니다.

마지막에 각 모듈과의 종속성을 최대한으로 줄인 형태를 플러그인이라고 볼 수 있습니다. 완전 독립되서 개발되는 것이죠. 메인 엔진의 펙토리에 등록하고 사용할 수 있는 형태로 아키텍쳐를 만들고 각 플러그인은 메인 프로그램에서 메인 엔진에 인스톨 해서 사용할 수 있게 구성하는 겁니다. 동시에 동적 라이브러리 형태로 프로젝트 종속성에서 설정하지 않고 프로그램 자체적인 상황에 따라 올릴 수 있게 구성해 둔다면 빌드 시간 뿐만이 아니라 링크 시간도 극적으로 해소시켜줄 수가 있습니다. 말 그대로 빌드와 링크를 제로로 만들어 버리는 것이지요. 메인 엔진에서 인터페이스까지 제공한다면 빌드도 완전히 제로로 만들어 버릴 수가 있습니다.

인터페이스를 만들고 세부 클래스들이 이 인터페이스를 상속받아 구현을 처리하는 것을 알고 계실 겁니다. 플러그인도 비슷합니다. 메인 엔진이 인터페이스를 만들고 각 플러그인이 이를 구현하죠. 즉, 메인 엔진은 플러그인을 잘 모른다는 점과 누구나 새로운 플러그인을 만들어 꼽을 수 있다는 것이 핵심입니다.


Script


Lua 등의 스크립트를 사용하면 유연하고 빌드 없는 프로그램을 개발할 수 있습니다. 이 부분에 대해서는 할 이야기가 많기 때문에 다음 기회로 미루겠습니다.




1.3. 빌드를 도와주는 도구




인크레디빌드


제가 오늘은 말이 많군요. 마지막으로 한가지만 더 이야기 해보겠습니다. 여기까지 다 했다고 하더라도 불편함이 있는 유저를 위한 이야기 입니다. 살다보면 하루에 수만 라인씩 코딩을 하는 동료를 만나게 될 지도 모릅니다. 이런 사람 한둘 있으면 아무리 아키텍쳐를 잘 만들고 모든 방법을 총 동원해도 빌드 시간이 꽤나 부담스러우실 겁니다. 

인크리디 빌드는 그리드 컴퓨팅 기술을 이용하여 여러 컴퓨터를 연결해 병렬 빌드가 가능하도록 도와주는 도구입니다. 단, 가격이 문제인데요, 컴퓨터 한대 당 70 만원 정도 하는 것으로 알고 있습니다. 부담스러운 가격임에도 분명 구입할 가치는 충분한 도구라고 생각됩니다. 빌드 시간으로 버리는 기업/개인의 시간이 훨씬 더 비쌀 테니까요:D


by 신동호 | 2009/01/30 15:39 | dead P society | 트랙백 | 덧글(0)
트랙백 주소 : http://aronze.egloos.com/tb/1284862
☞ 내 이글루에 이 글과 관련된 글 쓰기 (트랙백 보내기) [도움말]

:         :

:

비공개 덧글



< 이전페이지 다음페이지 >