최종 수정: 2013/01/30
문제사례 설명
- 제품군별 객체 생성 문제
- 컴파일러 (Scanner, Parser, Code Generator, Optimizer, .. 등으로 구성) 를 개발한다고 가정하면,
- 다양한 운영체제를 어떻게 지원
- HPScanner, HPParser, HPxxx
- SunScanner, SunParser, Sunxxx
다양한 접근(해결) 방법
- 기본적인 방법
- 조건 비교 방식
class Scanner { };
class HPScanner : public Scanner { };
void ScanParse()
{
if (_CurrentOs == HP_OS)
{
HPScanner Scanner;
HPParser Parser;
..
}
else if (_CurrentOs == SUN_OS)
{
}
else
{
}
}
이런 비교 방식은 운영체제에 맞추어 객체를 생성해야 하는 소스 곳곳에 조건 비교 문장이 존재하게 되어서 새로운 조건을 추가(새로운 OS 지원)할 경우가 발생하면, 소스 전체를 다시 개발하는 것과 비슷한 노력이나 비용이 들 수 있다.
비용이 많이 든다!
변경될 가능성이 많은 프로그램 부분을 한 곳으로 모아 변경이 필요할 경우 그부분만 고치도록 하는 방법이 있을 것이다.
(변경이 예상되는 부분을 국지화시킴으로써 변경에 소용되는 비용을 최소화하는 것으로 이를 '변경의 국지화 (Localization of Change') 라고 한다. )
객체 생성 전담 클래스를 활용한 컴파일러 관련 클래스의 객체 생성 방식
(Factory Method ?)
---------------------
ComplierFactory
+ CreateScanner() : Scanner*
---------------------
void CreateScanner()
{
if ( _CurrentOS == HP_OS)
{
return new HPScanner;
}
else if (_CurrentOs == SUN_OS)
{
}
else
{
}
위 방식의 문제점은 여전히 소스코드 내에 비교 문장이 존재한다는 것이다.
이는 새로운 조건의 추가를 이전 소스코드와는 무관하게 독립적으로 수행할 수 없다는 것을 의미. => 만약 이때 소스코드가 바이너리 코드 형태로만 존재한다면 이전 소스코드를 아예 분석허거나 변경할 수 없을 것이기 때문에 문제는 더욱 심각해질 것이다.
라고 하는데, 문제의 심각성이 조금은 납득가지 않음. (대부분 소스코드를 수정할 수 있으므로..)
객체 생성을 전담하는 클래스를 활용하는 방식의 문제는,
이전 소스코드와 독립적으로 새로운 것을 추가하기 힘들다는 점이었다.
---------------------
CompilerFactory
+ CreateScanner() : Scanner*
---------------------
---------------------
HPCompilerFactory
+ CreateScanner() : Scanner*
---------------------
int main()
{
CompilerFactory* pFactory = NULL;
if ( CurrentOS == HP_OS)
{
pFactory = new HPCompilerFactory;
}
Scanner* pScanner = pFactory->CreateScanner();
}
Abstract Factory 패턴을 적용한 경우, 두가지문제를 효율적으로 해결할수있다.
1. 개별 제품 클래스의 객체를 생성할 때마다 동일한 제품군에 속하는 객체를 생성하기 위해 일일이 조건 검사를 할 필요가 없어졌고
2. 새로운 제품군을 생성하려고 할 경우 기존 소스코드와는 독립적으로 새로운 제품군을 추가하는 것이 가능하다는 점
샘플 코드
없음
구현 관련 사항
실제 구현에서 고려 사항은, Factory 객체를 하나만 생성&유지할지(Singleton) 혹은 객체 생성시 원본 객체를 복제하는 방식 (Prototype) 으로 객체를 생성할지를 고려가 필요하다
- Factory 객체를 하나만 생성, 유지하는 방법
class CompilerFactory
{
public:
virtual Scanner* CreateScanner() = 0;
protected:
CompilerFactory() {}
CompilerFactory(const CompilerFactory& rhs);
static CompilerFactory* pInstance_;
};
class HPCompilerFactory : public CompilerFactory
{
public:
static HPCompilerFactory* CreateInstance()
{
if (pInstance_ == 0) pInstance_ = new HPCompilerFactory;
return (HPCompilerFactory*) pInstance_;
}
Scanner* CreateScanner() { new HPScanner; }
}
int main()
{
if ( CurrentOS == HP_OS)
{
HPCompilerFactory::CreateInstance();
}
CompilerFactory* pFactory = CompilerFactory::CreateInstance();
Scanner* pScanner = pFactory ->CreateScanner();
}
class Scanner
{
public:
virtual Scanner* Clone() = 0;
};
class HPScanner : public Scanner
{
public:
Scanner* Clone { return new HPScanner(*this); }
}
class CompilerFactory
{
public:
CompilerFactory(Scanner* pScanner, Parser* pParser, ...)
: pScanner_(pScanner), pParser_(pParser), ..
{}
Scanner* CreateScanner() { return pScanner_->Clone(); }
}
int main()
{
CompilerFactory* pFactory = NULL;
if ( CurrentOS == HP_OS)
{
HPScanner Scanner;
HPParser Parser;
..
pFactory = new CompilerFactory(&Scanner, &Parser, &xxx,...);
}
Scanner* pScanner = pFactory->CreateScanner();
}
Prototype 패턴을 활용할 경우에는 다음과 같은 특징이 있다.
1. 제품군별로 concrete Factory 클래스를 정의할 필요가 없다. 대신 제품의 종류별로 한개씩 객체를 생성해서 Factory 클래스에 등록해두어야 하는 비용이 발생한다. 특히 생성해야 할 제품의 종류가 많을 경우, Factory 클래스의 생성자 인터페이스가 복잡해지고, 객체 저장에 따른 비용도 커질 수 있다.
Abstract Factory 패턴은 특정 제품군에 속하는 제품 객체를 생성하는 프로그램 (? 코드) 를 한 곳으로모아, 새로운 제품군의 추가가 용이하게 만든 클래스 설계이다.
이런 Abstract Factory 패턴의 가장 큰 단점은
새로운 제품군이 아닌 제품(예를 들어 ErrorHandler)가 새로 추가될 경우, 모든 Factory 클래스들도 수정해야 하는 단점이 있다. (CreateErrorHandler 메서드 추가 필요)
그렇다면 이런 단점을 극복할 수 있는 방법은 없을까?
(문제의 원인은 Factory 클래스에 새로운 종류의 제품 생성을 요청할 경우 왜 모든 Factory 클래스가 수정되어야 하는가? 그것은 Factory 클래스가 생성할 제품의 종류에 따라 각기 다른 생성 멤버 함수를 가지고 있기 때문이다.)
해결 방법은 생성할 제품의 종류에 무관하게 Factory 클래스가 사용하는 멤버 함수를 하나로 통일 시키면 될것이다. CreateProduct()
허나 아래와 같이 구현하면 해결이 안되고 똑같은 문제를 가지고 있다.
class Product { };
class HPScanner : public Product { };
class HPErrorHandler : public Product { }; // -- 추가 필요
class CompilerFactory
{
public:
virtual Product* CreateProduct( int type) = 0;
};
class HPCompilerFactory : CompilerFactory
{
public:
Product* CreateProduct( int type)
{
switch (type)
{
case SCANNER : return new HPScanner;
case ERRORHANDLER : return new HPErrorHandler; // -- 추가 필요
}
}
};
int main()
{
CompilerFactory* pFactory = NULL;
if (CurrentOS == HP_OS)
{
pFactory = new HPCompilerFactory;
}
else if (CurrentOS == SUN_OS)
{
pFactory = new SUNCompilerFactory;
}
Product* pScanner = pFacotry->CreateProduct( SCANNER );
// -- Product* pErrorHandler = pFacotry->CreateProduct( ERRORHANDLER );
}
위와 같이 하면, 새로운 종류의 제품 추가시에 기존 클래스 구현의 변경이 역시나 필요하기 때문에, Abstract Factory 패턴의 목적을 만족시키지 못한다.
그렇다면 기존 클래스 구현에는 영향을 주지 않고 새로운 종류의 객체를 쉽게 생성할 수 있게 만들어 줄 수 있는 다른 방법은 없을까?
한가지 고려해볼 수 잇는 방법은 CreateProduct() 멤버 함수의 인자로 생성하고자 하는 객체의 원형을 직접 전달하고, CreateProduct() 멤버 함수는 그 객체의 복제 객체를 생성해 주는 Prototype 패턴을 활용하는 것이다.
아래 내용은 이를 구현한 코드이다. 단, 이때 Client 입장에서는 미리 생성할 객체의 종류별로 원형 객체를 하나씩 생성해서 관리하고 있어야 하는 불편은 존재한다. (Prototype 패턴의 불편으로 당연한 불편일 듯..)
class Product
{
public:
virtual Product* Clone() = 0;
};
class HPScanner : public Product
{
public:
Product* Clone() { return new HPScanner(*this); }
};
// -- 추가 필요 부분 시작
class HPErrorHandler : public Product
{
public:
Product* Clone() { return new HPErrorHandler(*this); }
};
class SunErrorHandler : public Product
{
public:
Product* Clone() { return new SunErrorHandler (*this); }
};
// -- 추가 필요 부분 끝
class CompilerFactory
{
public:
virtual Product* CreateProduct( Product* p) = 0;
};
class HpCompilerFactory : public CompilerFactory
{
public:
Product* CreateProduct( Product* p)
{
return p->Clone();
}
};
class SunCompilerFactory : public CompilerFactory
{
public:
Product* CreateProduct( Product* p)
{
return p->Clone();
}
};
CompilerFactory *g_pFactory;
int main()
{
Product *pScanner, *pParser;
Product *pErrorHandler;
if (CurrentOs == HP_OS)
{
pScanner = new HPScanner;
pErrorHandler = new HPErrorHandler; // -- 추가 필요
g_pFactory = new HPCompilerFactory;
}
else if (CurrentOs == SUN_OS)
{
pScanner = new SunScanner;
pErrorHandler = new SunErrorHandler; // -- 추가 필요
g_pFactory = new SunCompilerFactory;
}
Product* pNewScanner = g_pFactory->CreateProduct( pScanner );
// -- Product* pNewErrorHandler = g_pFactory->CreateProduct( pErrorHandler );
}
응용 관련 사항
정리
Abstract Factory 패턴의 유용한 경우를 정리하면 다음과 같다.
- 객체의 생성을 클라이언트가 직접적으로 하는 것이 아니라, 간적적으로 수행함으로써 클라이언트가 객체의 생성이나 구성 또는 표현 방식에 독립적이도록 만들도록 할때.
- 여러 제품군 중 사용할 제품군을 쉽게 선택할 수 있도록 만들고 싶을 때.
- 서로 관련된 제품들이 하나의 제품군을 형성하고, 이런 제품군이 여러 개 존재하는 상황에서 생성되는 제품 객체가 항상 같은 제품군에 속하는 것을 확실히 보장하고 싶을때.
- 제품들에 대한 클래스 라이브러리를 만들어야 하는데 그 인터페이스만 드러내고 구현은 숨기고 싶을 때, 이때 각각의 인터페이스는 Abstract Factory 클래스와 제품 종류별 Abstract Base Class (ABC라고 함)에 의해 외부에 드러나며, 구체적인 구현은 하위 크래스에 의해 이루어진다.
Abstract Factory 장/단점
- Abstract Factory 패턴의 장점은 객체가 생성되는 방식이나 과정 및 책임을 클라이언트가 모르도록 만들어 준다. 이때 클라이언트는 다만 Abstract Factory 클래스와 Abstract Product 클래스의 인터페이스만을 사용하면 된다. 이로서 클라이언트 소스코드는 객체가 생성되는 방식이나 과정 및 생성하는 종류가 변경되더라도 그 부분이 국지화될 수 있다.
- Abstract Factory 패턴의 또 다른 장점은 제품군 (Product Family) 간 교체가 쉽다는 것이다.
즉, Concrete Factory 클래스의 객체가 생성되는 부분만 변경시켜 주면 얼마든지 다른 제품군 (Product Family)를 생성하도록 바꿀 수 있다.
- Abstract Factory 패턴을 사용하게 되면 여러 제품군들이 실수로 섞여서 사용되는 것이 자연스럽게 방지된다. 왜냐하면 특정 Concrete Factory 클래스는 특정한 제품군 (Product Family) 만을 생성하기 때문이다.
- Abstract Factory 패턴의 단점은 제품군의 개수가 늘어날수록 Concrete Factory 클래스의 개수도 늘어나야 한다는 점이다. 따라서 제품군의 개수가 많아 졌을 경우 클래스가 너무 많아져 설계가 복잡해지게 된다.
- 일반적인 Abstract Factory 패넌의 가장 큰 단점은 제품군에 새로운 제품이 추가되어야 할 경우, 모든 Factory 클래스를 수정해야 한다는 것이다. 예를 들어 컴파일러 문제에서 ErrorHandler와 같이 새로운 제품을 생성할 필요가 생기면 모든 Factory 클래스에 CreateErrorHandler() 멤버 함수를 추가해야 하는 문제가 발생한다.
Abstract Factory 패턴은 최선의 해결책이 아니라, 최적의 해결책이라는 사실이다. 상황에 따라서 적절히 변경할 수 있는 능력을 키워야 할 것이다.