본문 바로가기
번역/Bjarne Stroustrup's C++ Style and Technique FAQ

Why do my compiles take so long?

by 겜게준 2019. 1. 15.

왜 컴파일이 오래걸릴까요?


컴파일러 문제일 수 있는데, 컴파일러가 옛날 버전이거나 설치가 잘못되었거나, 또 컴퓨터가 구식이어서 일 수 있습니다. 이런 문제는 제가 도와드릴순 없습니다. 하지만, 컴파일 하려는 프로그램의 설계가 형편없어서 컴파일러가 컴파일할 때 수백개의 헤더파일과 수만라인의 코드를 검사해야하는 경우도 있습니다. 원칙적으로는 이런 문제는 피할 수 있습니다. 만약 이 문제가 사용중이신 라이브러리 공급업체의 설계에 있다면 할 수 있는것이 거의 없습니다 (라이브러리나 공급업체를 변경하는 것을 제외하곤), 하지만 수정 후 재컴파일을 최소화하기 위해 코드의 구조를 바꿀 수 있습니다. 재컴파일을 최소하 하는 디자인은 전형적으로 더 나은, 더 유지보수에 유용하며, 관계의 분리에 더 좋은 모습을 보여주기 때문에 설계됩니다.


전형적인 객체지향 프로그램을 예제로 생각해봅시다 : 


class Shape {

public: // interface to users of Shapes

virtual void draw() const;

virtual void rotate(int degrees);

// ...

protected: // common data (for implementers of Shapes)

Point center;

Color col;

// ...

};


class Circle : public Shape {

public:

void draw() const;

void rotate(int) { }

// ...

protected:

int radius;

// ...

};


class Triangle : public Shape {

public:

void draw() const;

void rotate(int);

// ...

protected:

Point a, b, c;

// ...

};


이 생각은 사용자가 Shape의 public 인터페이스로 모양을 처리하고, 파생 클래스들의 구현자가 protected 멤버로 설정된 구현체의 외형을 공유하는 것입니다.

이 보기에 간단한 생각에는 3가지 심각한 문제가 있습니다.


모든 파생클래스에 도움이 되는 구현체의 공유된 외형을 정의하기가 쉽지 않습니다. 이러한 이유로는, protected 멤버의 집합이 아마도 public 인터페이스보다 훨씬 더 종종 변경되어야 할 수 있기 때문입니다. 예를 들자면, 'center' 모든 도형들에 유효한 개념이 거의 틀림없더라도, 삼각형의 'center'의 위치를 유지해야 하는것은 상당히 귀찮은 일입니다. 누군가 관심을 가질 때에만 'center'를 계산하는 것이 더 합리적 입니다.


protected 멤버가 Shape들의 사용자가 의존하지 않아도 되는 구현 세부 정보에 의존하게 됩니다. 예를 들자면, Shape를 사용한 많은 코드 (거의?)들이 "color"의 정의와 논리적으로 독립적이게 될 것이고, 아직도 Shape 정의에 있는 색깔의 존재가 os에 색 개념을 정의한 헤더 파일들의 컴파일을 필요로 할 것입니다.

protected 부분에 있는 무언가가 변경될 경우, Shape의 사용자는 재컴파일을 해야하고, 파생 클래스들의 구현자들만 protected 멤버에 접근할 수 있습니다. 


그러므로, 사용자 인터페이스의 역할을 하는 기본 클래스에 있는 "구현자에게 유용한 정보"[각주:1]의 존재는 구현 불안전성의 근원이며, 구현 정보가 변경될 때 쓸대없이 사용자 코드를 재컴파일 하며, 헤더파일을 사용자 코드에 과도하게 포함시키게 합니다("구현자에게 유용한 정보"가 이 헤더파일들을 필요로하기 때문에). 이 문제는 "brittle base class problem." (Fragile base class로 더 잘알려져있음) 으로 알려져 있습니다.


이 문제의 확실한 해결책은 사용자에게 인터페이스로 사용된 클래스의 "구현자에게 유용한 정보"를 제거하는것입니다. 그러니까, 인터페이스를 만들때는 순수 인터페이스를 만들어야 합니다. 즉, 추상클래스로 인터페이스를 표현하려면:


class Shape {

public: // interface to users of Shapes

virtual void draw() const = 0;

virtual void rotate(int degrees) = 0;

virtual Point center() const = 0;

// ...


// no data

};


class Circle : public Shape {

public:

void draw() const;

void rotate(int) { }

Point center() const { return cent; }

// ...

protected:

Point cent;

Color col;

int radius;

// ...

};


class Triangle : public Shape {

public:

void draw() const;

void rotate(int);

Point center() const;

// ...

protected:

Color col;

Point a, b, c;

// ...

};


이렇게 하여 사용자는 파생 클래스 구현이 변경되는것에 대해 영향을 받지 않게 됩니다. 저는 이 기술이 규모의 정도에 따라 빌드 시간을 줄이는것을 봤습니다.


그런데, 정말로 모든 파생클래스에 공통된 정보가 있다면 (아니면 간단하게 몇개의 파생 클래스만 그런다면)? 간단하게 그 정보를 클래스로 만들고, 구현 클래스를 파생하면 됩니다.


class Shape {

public: // interface to users of Shapes

virtual void draw() const = 0;

virtual void rotate(int degrees) = 0;

virtual Point center() const = 0;

// ...


// no data

};


struct Common {

Color col;

// ...

};

class Circle : public Shape, protected Common {

public:

void draw() const;

void rotate(int) { }

Point center() const { return cent; }

// ...

protected:

Point cent;

int radius;

};


class Triangle : public Shape, protected Common {

public:

void draw() const;

void rotate(int);

Point center() const;

// ...

protected:

Point a, b, c;

};




  1. 위의 코드에서 protected 멤버, 괜히 중앙 위치랑 색깔을 주려다가 오히려 필요없는데도 손절도 못하고 필요도 없는데 괜히 protected 멤버 건드리면 안쓰는것도 다시 컴파일함 [본문으로]

댓글