본문 바로가기

프로그래밍

[Modern C++ Design] 단위전략 기반의 클래스 디자인(2)

클래스를 단위전략으로 분리해 내기

단위전략을 기반으로 클래스를 디자인 하는데 가장 어려운 작업은 클래스의 각 기능 요소들을 올바르게 적절한 단위전략으로 분리해 내는 일일 것이다. 가장 중요한 규칙은 다음과 같다. 먼저 클래스의 동작을 구성하는 데 필요한 결정 사항들을 파악해내야 하며, 그 다음에는 거기에 적절한 이름을 부여해야 한다. 한가지 이상의 방법으로 수행될 수 있는 일이 있다면 놓치지 말고 클래스로부터 단위전략으로 분리해 내기 바란다. 잊지 마라! 클래스를 디자인 하면서 발견해내지 못한 채 묻혀 버린 재약 사항들은 아무 설명 없이 코드속에 묻혀 있는 마법 상수만큼이나 치명적인 요소가 된다.

예를 들어 WidgetManager 클래스를 한번 생각해 보자. 만일 WidgetManager 가 내부적으로 새로운 Widget 객체를 생성 한다면, 그 생성 과정은 전적으로 단위전략에게 맡겨져야 한다. 만일 WidgetManager가 Widget에 대한 집합을 저장해야 한다면, 별도로 다른 특별한 저장 메커니즘이 필요한 상황이 아닌 이상, 그 집합체에 대한 저장 기능을 담당하는 별도의 단위전략을 만들어 처리하는 것이 바람직할 것이다.

극단적인 경우에, 호스트 클래스는 완전히 단위전략을 가지고 있는 것 자체가 전부인 경우도 있다. 이런 경우, 호스트 클래스는 모든 디자인 상의 경정 사항이나 제한점들을 단위전략에 일임해 버린다. 이러한 호스트 클래스는 마치 단위전략을 모아주는 쉘과도 같이 동작하며, 오직 해당 단위전략들의 동작을 밀접하게 엮어내는 데에만 관여하게 된다.

이렇게 과도하게 일반화된 호스트 클래스의 단점은 템플릿 인자가 지나치게 많아질 수 있다는 점이다. 실질적으로 네다섯 개, 그리고 여섯 개가 넘는 템플릿 인자들을 다루며 작업한다는 것은 아주 골치 아픈 일이다. 물론 그 호스트 클래스가 정말로 복잡하고 유용한 기능을 제공한다면 여전히 자신들의 존재에 대한 당위성을 주장할 수 있겠지만 말이다.

클래스를 단위전략으로 분리해 낼 때에는 서로 상호작용하지 않는 요소를 찾아 분리해 내는 것이 매우 중요하다. 각각의 단위전략들이 서로 완전히 독립적으로 동작하도록 만들어 주어야 하기 때문이다. 그렇지 못할 경우에는 다양한 단위전략들이 서로에 관한 정보를 모두 알고 있어야 한다는 문제점이 발생한다.

예를 들어 스마트 포인터에 Array 단위전략을 추가하는 경우를 생각해 보도록 하자. Array는 스마트 포인터가  배열의 시작점을 가리키는지, 아니면 그저 하나의 객체만을 가리키는지를 말해주는 매우 단순한 단위전략이다. 이 단위전략은 T& ElementAt(T* ptr, size_t inex) 멤버 함수를 가지는 것으로 정의될 수 있다. 배열을 가리키지 않는 경우에는, 단지 ElementAt 멤버 함수가 정의되지 않은 단위전략을 사용하기만 하면 된다. 그러면 불필요하게 이를 사용하는 경우를 정확히 찾아내어 컴파일러가 에러를 나타낼 것이다. ElementAt 함수는 Array 단위전략에서 제공하는 보강된 인터페이스라 할수 있을것이다.

Array 단위전략을 구현하는 두개의 단위전략 클래스는 다음과 같다.

template <typename T>
struct IsArray
{
  T& ElementAt(T* ptr, size_t index)
  {
    return ptr[index];
  }
};

template <typename T>
struct InNotArray {};

문제는 Array 단위전략의 목적 - 이 스마트 포인터가 배열에 대한 포인터인지 그렇지 않은지를 나타내고자 하는 - 자체가 불행하게도 다른 단위전략과 상호작용을 하게 된다는 데에 있다. 다른 단위전략이란 다름이 아니라 파괴자와 관련된 단위전략인다. 단일 객체를 삭제할 때에는 delete 연산자를 사용해야 하지만, 배열로 이루어진 객체들을 삭제하고자 할때에는 delete[] 연산자를 사용해야 한다.

서로 상호작용을 하지 않는 단위전략을 상호 독립적이라고 한다는 정의에 비추어 볼때 Array 단위전략과 Destroy 단위전략을 독립적이라고 말할 수 없다.

만일 이 두 단위전략을 꼬 독립된 단위전략으로 구성하기를 원한다면, 두 단위전략이 서로 정보를 주고받을수 있는 어떤 수단을 마련해 주어야 할 것이다. 예를 들어, Array 단위전략이 하나의 bool 값 상수를 추가적으로 노출하면서, 이것을 Destroy 단위전략에게 넘겨줄 수 있도록 만들어 줘야 한다. 하지만, 이렇게 하는 것은 두 단위전략 모두를 보다 복잡하게 만들 뿐 아니라, 예기치 못한 제한 사항을 발생시키는 결과는 가져오게 된다.

비 독립적인 단위전략은 절대 피해가야만 할 잠재적인 재앙의 원천이다. 이것은 컴파일 타임에서 보장되어야 할 자료형에 대한 안정성에 악영향을 미칠 뿐 아니라. 호스트 클래스와 단위전략 클래스 모두의 디자인을 복잡하게 만들어 버린다.

만일 어쩔수 없이 비독립적인 단위전략을 사용할 수밖에 없다면, 하나의 단위전략을 다른 단위전략의 템플릿 멤버 변수에 대한 인자로 넘겨주도록 함으로써, 서로 간의 의존성을 최소화시키는 방법을 사용해야 한다. 이러한 방법을 통하면 템플릿 기반의 인터페이스가 제공하는 유연성이라는 장점을 그대로 누릴 수 있게 된다. 몰론 그 아래쪽에는 하나의 단위전략이 다른 단위전략에게 자기 자신에 대한 상세 정보를 노출해 주어야 한다는 이우로 인해 구조의 캡슐화에는 여전히 실패하게 되는 문제점이 남게 되겠지만 말이다.

요약

하나의 클래스를 단위전략을 쪼개 나갈 때에는, 다음의 중요한 규칙을 따라야 한다. 첫째, 클래스를 디자인 하는데 트레이드 오프 관계가 되거나, 다양한 방법으로 구현될 수 있는 문제가 있다면, 그것을 찾아내어 지역화 시키고, 거기에 적절한 이름을 붙여 주어야 한다는 것이다. 또 하나의 규칙은 하나의 단위 전략이 변하더라도 다른 단위전략에 영향을 끼치지 않도록, 서로 독립적인 요소로 단위전략을 구헝해야 한다는 것이다.