* 컴파일 의존성(compilation dependency)을 최소화하는 작업의 배경이 되는 가장 기본적인 아이디어는 '정의' 대신에 '선언'에 의존하게 만들자는 것입니다. 이 아이디어에 기반한 두 가지 접근 방법은 핸들 클래스와 인터페이스 클래스입니다.
가장 단순한 방법으로는 #include 대신에 전방 선언을 이용하는 방법이 있다.
class Date; // 전방 선언 class Address; // 전방 선언 class Person { public: Person(const std::string& name, const Date& birthday, const Address& addr); std::string name() const; std::string birthDate() const; std::string address() const; private: std::string theName; Date thBirthDate; Address theAddress; };
하지만 해당 객체를 할당하기 위해서는 객체들의 크기를 컴파일 도중에 전부 알아야 하는데 있다.
실제 객체가 아니라 포인터로 선언을 하면 그 크기를 몰라도 된다.
'포인터 뒤에 실제 객체 구현부 숨기기' 놀이를 직접 C++에서 하면 된다.
#include <string> // 표준 라이브러리는 전방 선언을 하면 안된다. #include <memory> class PersonImpl; // Person의 구현 클래스에 대한 전방 선언 class Date; // 전방 선언 class Address; // 전방 선언 class Person { public: Person(const std::string& name, const Date& birthday, const Address& addr); std::string name() const; std::string birthDate() const; std::string address() const; private: std::tr1::shared_ptr<PersonImpl> pImpl; // 구현 클래스 객체에 대한 포인터 };
이런 종류의 설계는 거의 패턴으로 굳어져 있다. pImpl 관용구 (pointer to implementation) 이라고 이름도 있다. 그리고 여기서와 같이 관용구를 사용하는 Person 같은 클래스를 핸들 클래스(handle class)라고 한다. 이러면 구현 클래스의 내용은 생각만 있으면 맘대로 고칠 수 있지만, 그래도 Person의 사용자는 컴파일을 다시 할 필요가 없다. 인터페이스와 구현이 뼈와 살이 분리되듯 떨어지는 거다. 이렇게 인터페이스와 구현을 둘로 나누는 열쇠는 '정의부에 대한 의존성(dependencies on definitions)'을 '선언부에 대한 의존성(dependencies of declarations)'으로 바꾸어 놓는데 있다.
이 외 나머지 전략들은 이것을 축으로 해서 흘러가게 되어 있다.
- 객체 참조자 및 포인터로 충분한 경우에는 객체를 직접 쓰지 않는다.
- 할 수 있으면 클래스 정의 대신 클레스 선언에 최대한 의존하도록 만든다.
class Date; // 클래스 선언 Date today(); // Date 클래스의 정의를 가져오지 않고도 함수 선언이 가능하다. void clearAppointments(Date d);
이 함수를 누군가가 호출 할때는 분명히 Date에 대한 정의가 파악이 된 상태일 것이다. 이 함수를 호출하지 않는 사람에게는 이것만으로도 컴파일에 아무런 문제가 없다.
- 선언부와 정의부에 대해 별도의 헤더 파일을 제공한다.
위의 핸들 클래스를 사용하는 경우에 구현부는 다음과 같은 모양으로 구현해야 한다.
#include "Person.h" // Person 클래스를 구현하고 있기 때문에 당연히 #include해야 한다. #include "PersonImpl.h" // PersonImpl의 멤버함수를 호출해야한다. // Person의 멤버함수와 거의 1:1 대응된다. Person::Person(const std::string& name, const Date& birthday, const Address& addr) : pImpl(new PersonImpl(name, birthday, addr)) {} std::string Person::name() const { return pImpl->name(); }
핸들 클레스 방법 대신에 다른 방법을 쓰고 싶다면 Person을 특수 형태이 추상 기본 클래스, 이른 바 인터페이스 클래스(Interface class)로 만드는 방법이 있다.
// Person.h class Person { public: virtual ~Person(); virtual std::string name() const = 0; virtual std::string birthDate() const = 0; virtual std::string address() const = 0; static std::tr1::shared_ptr<Person> create(const std::string& name, const Date& birthday, const Address& addr); }; // RealPerson.h class RealPerson: public Person { public: RealPerson(const std::string& name, const Date& birthday, const Address& addr) : theName(name), theBirthDate(birthday), theAddress(addr) {} virtual ~RealPerson() {} std::string name() const; std::string birthDate() const; std::string address() const; private: std::string theName; std::string theBirthDate; std::string theAddress; }; // Person.cpp #include "Person.h" #include "RealPerson.h" std::tr1::shared_per<Person> Person::create (const std::string& name, const Date& birthday, const Address& addr) { return std::tr1::shared_per<Person>(new RealPerson(name, birthday, addr)); } // main.cpp std::string name; Date dateOfBirth; Address address; ... std::tr1::shared_ptr<Person> pp(Person::create(name, dateOfBirth, address)); ...
이 Class를 Code에 써먹을려면 Person에 대한 포인터 혹은 참조자로 프로그래밍하는 방법밖에 없다. 파생 Class의 생성자 역할을 대신하는 어떤 함수를 만들어 놓고 호출해야 한다. 이런 함수를 가리켜 팩토리 함수 혹은 가상 생성자(cirtual constructor)라고 부른다. 이런 함수는 인터페이스 클래스 내부에 정적 멤버로 선언되는 경우가 많다.
인터페이스 클래스를 구현하는 용도로 가장 많이 쓰이는 메커니즘은 두 가지가 있다. 하나는 인터페이스 명세를 물려 받은 후에, 그 인터페이스에 들어 있는 가상 함수를 구현하는 것이고,두 번째 방법은 다중 상속을 사용하는 것이다.
핸들 클래스와 인터페이스 클래스는 구현부로부터 인터페이스를 뚝 떼어 놓음으로써 파일들 사이의 컴파일 의존성을 완화시키는 효과를 가져다 준다.
핸들 클래스는 멤버 함수를 호출하면 구현부 객체의 데이터까지 가기 위해 포인터를 타야 한다. 즉 접근할 때마다 요구되는 간접화 연산이 한 단계 더 증가하는 셈이다.
인터페이스 클래스를 호출하는 경우는 모두 가상 함수라서 가상 테이블 점프에 따르는 비용이 소모된다.
그리고 핸들 클래스, 인터페이스 클래스 둘 다 인라인 함수의 도움을 제대로 끌어내기 힘들다.
개발 도중에는 핸들 클래스 혹인 인터페이스 클래스를 사용하고, 제품을 출시해야 될 때 다시 고민하는거다. 실행 속력이나 파일 크기에서 많은 손해를 보게 되어, 클래스 사이의 결합도를 높이는 방법박에 없다는 결론이 나온다면 통짜 구체 클래스로 바꾸는 일은 그때 해도 된다.
* 라이브러리 헤더는 그 자체로 모든 것을 갇추어야 하며 선언부만 갖고 있는 형태여야합니다. 이 규칙은 템플릿이 쓰이거나 쓰이지 않거나 동일하게 적용합시다.
댓글 없음:
댓글 쓰기