책정리/GoF 디자인 패턴

3장 제품군별 객체 생성 문제 : ABSTRACT FACTORY 패턴

GONII 2015. 4. 3. 12:28

제품군(Product Family)

제품 여러 개가 있고, 각 제품들은 또 다시 여러 종류로 나뉠 때 같은 종류의 제품들을 모아 놓은 것

   

  • 다양한 접근 방법 및 Abstract Factory 패턴

    문제를 요약하면, 어떻게 하면 같은 제품군에 속한 제품들의 객체만을 생성해서 사용하도록 명확히 보장받을 수 있는 가 하는 것

       

    기본적인 방법 : 조건 비교 방식

if ( HP == 0 )
{

//< HP용 객체 생성

}

else if ( SUN == 0 )

{

//< SUN 용 객체 생성

}

else

{

//< 지원하지 않는 시스템 환경

}

프로그램 곳곳에 조건 비교 문장이 존재하게 됨

새로운 조건을 추가할 일이 발생하면 프로그램 전체를 찾아서 일일이 수정해야 함

   

좀 더 나은 방법 : 객체 생성 전담 클래스 활용 방식

변경될 가능성이 많은 프로그램 부분을 한 곳으로 모아 변경이 필요할 경우 그 부분만 고치면 원하는 작업을 수행할 수 있게 만들어주는 것

이처럼 객체 지향 설계의 가장 핵심적인 접근 방법은 바로 변경이 예상되는 부분을 국지화시킴으로써 변경에 소요되는 비용을 최소화하는 것으로 이를 변경의 국지화(Localization of Change) 라고 함

이를 위해 객체지향 설계에서 제공되는 도구가 바로 클래스다.

변경될 가능성이 많은 정보와 그렇지 않은 정보를 구분해서 변경될 가능성이 많은 정보를 클래스 내부로 숨기는 방식을 통해 변경이 필요할 경우 해당 클래스 내부만 변경시켜주면 원하는 작업이 수행될 수 있게 만들어주는 것으로 변경 대상폭을 줄여주는 역할을 함.

이를 객체지향 패러다임에서는 '정보 은닉(Information Hidign)'이라고 하며, 객체지향 설계는 이와 같은 정보은닉을 통해 프로그램에 대한 변겨 ㅇ요청이 있더라도 그 범위를 최소화할 수 있는 장점을 가지게 됨

   

객체 생성을 전담하는 클래스를 활용

여전히 소스코드 내에 비교문장이 존재함

새로운 조건을 추가할 경우 이전 소스코드를 면밀히 분석해서 수정해주어야 한다는 점이 문제가 됨

이는 새로운 조건의 추가를 이전 소스코드와는 무관하게 독립적으로 수행할 수 없다는 사실을 의마한다

   

패턴 활용 방법 : Abstract Factory 패턴

객체지향 설계에서 기존의 것과는 독립된 새로운 것을 추가, 확장시키기 위한 가장 좋은 방법 중 하나가 클래스 상속(Inheritance)을 이용하는 것

   

클래스 상속은 크게 두 가지 목적을 가짐

  • 상위 클래스가 여러 하위 클래스들의 공통된 데이터를 추출해서 관리하도록 만드는 것
  • 상위 클래스가 전반적인 자료형을 대표하면서 외부에 공통된 인터페이스를 제공하고, 하위 클래스들은 각기 다른 구현 내용을 가지도록 만들어주기 위한 것

   

이 때 후자의 목적으로 클래스 상속을 이용하게 되면 상속 구조 내의 클래스를 사용하는 입장에서는 자료형과 인터페이스는 최상위 클래스를 참조하면서 구체적으로 실행되는 구현 내용은 다형성(Polymorphism)에 따라 결정되도록 만들 수 있음. 따라서 새로운 국현 내용이 추가된다 하더라도 새로운 하위 클래스만 정의해서 사용하면 기존 소스코드의 변경을 최소화하면서 새로운 구현 내용을 적용할 수 있게 됨

외부 Client에서는 CompilerFactory클래스의 자료형과 인터페이스를 사용

각 클래스의 구현에 시스템 환경에 따른 비교 문장이 없어도 됨

각각의 하위 클래스들이 시스템 환경별로 정의되었기 때문에 어떤 하위 클래스의 객체를 사용하느냐에 따라 시스템 환경이 결정될 수 있음

   

  • Abstract Factory 패턴

    제품군을 구성하는 클래스의 객체를 전달 생성하는 클래스를 두되, 새로운 제품군 생성을 추가하는 것이 쉽도록 클래스 상속을 도입하고, 구체적인 제품군별 Factory 클래스 상위에 Abstract Base Class를 정의한 형태의 설계 구조

       

  • Abstract Factory 패턴 사용 시 장점

    객체를 생성하기 위해 일일이 조건 검사를 할 필요가 없음

    새로운 제품군을 생성하려고 할 경우 기존 소스코드와는 독립적으로 새로운 제품군을 추가하는 것이 가능함

  • 샘플코드

class scanner
{

public :

virtual ~scanner() = 0 ;

} ;

   

class HPScanner : public scanner {} ;

   

class compilerFactory

{

public :

virtual scanner* createScanner() = 0 ;

} ;

   

class HPCompilerFactory : public compierFactory

{

public :

scanner* createScanner() { new HPScanner ; }

}

   

compilerFacotry *pFactory ;

void main ( void )

{

struct utsname sysInfo ;

   

//<

if( strncasecmp( sysInfo.sysname, HPUX, strlen(HPUX)) == 0 )

pFactory = new HPCompilerFactory ;

   

scanner * pScanner = pFactory->createScanner() ;

}

   

  • 구현 관련 사항

    Abstract Factory 패턴을 실제 구현할 경우 고려해야 할 사항

    • Factory 객체를 하나만 생성,, 유지하는 방법
    • 객체 생성 시 원본 객체를 복제하는 방식으로 객체를 생성하는 방법
    • 새로운 종류의 제품이 추가되었을 때 어떻게 대처 가능한가에 대한 것

         

    Factory 객체를 하나만 생성, 유지하는 방법

    Singleton 패턴을 적용

class compilerFactory

{

public :

protected :

compilerFactor( void ) {}

compilerFactory( const compilerFactory& rhs ) ;

static compilerFactory * pInstance ;

} ;

   

class HPCompilerFactory : public compilerFactory
{

public :

static HPCompilerFactory* createInstance()

{

if ( pInstance == 0 )

pInstance = new HPCompilerFactory ;

return (HPCompilerFactory*) pInstance ;

}

}

  • 일반적인 Singleton 패턴의 경우 클래스 상속 구조 상에서 최상위 클래스의 createInstance() 멤버 함수가 하위 클래스의 객체를 생성시켜주는 형태

    그러나 여기서는 하위 클래스가 대행함

  • createInstance() 멤버 함수가 최상위 클래스에 존재할 경우에는 새로운 하위 클래스를 추가해서 객체를 생성하고 싶을 경우 최상위 클래스의 createInstance() 멤버 함수 내부를 수정해야 하지만 여기서는 각각의 하위 클래스에 createInstance() 멤버 함수가 놓이므로 새로운 하위 클래스의 추가 시 이전 소스 코드 수정이 불필요

       

    복제를 통해 제품 객체를 생성하는 방법

    Abstract Factory패턴은 기본적으로 Factory Method 패턴을 활용해서 구현하는 형태다. 그런데 이런 방식의 구현은 결과적으로 새로운 제품군이 추가될 때마다 새로운 Concrete Factory클래스를 정의하고 구현해야 하는 불편을 초래한다.

    이런 불편을 해결하기 위한 방법 중 하나로 생성해야 할 객체를 종류별로 Factory클래스에 등록해두었다가 객체 생성 요청 시 이를 복제해주는 방식을 사용하는 것이다. 이때 어떤 제품군의 객체를 생성할지는 처음 등록해두는 객체에 의해 결정된다. 여기서 생성할 객체를 미리 등록해두고 이를 복제해서 새로운 객체를 생성해주는 방식을 Prototype 패턴이라고 한다.

    • 예제 Prototype패턴을 활용해서 Abstract Factory 패턴을 구현

#include <iostream>

#include <string>

#include <sys/utsname.h>

   

using namespace std;

#define HPUX "HPUX"

#define SUNOS "SunOS"

   

class scanner

{

public:

virtual scanner* clone() = 0;

};

   

class hpScanner : public scanner

{

public:

scanner* clone() { return new hpScanner(*this); }

};

   

class SunScanner : public scanner

{

public:

scanner* clone() { return new SunScanner(*this); }

};

   

class compilerFactory

{

public:

compilerFactory(scanner* pScanner) : m_pScanner(pScanner)

{}

   

scanner* createScanner() { return m_pScanner->clone(); }

private:

scanner* m_pScanner;

};

   

compilerFactory* pFactory;

   

int main()

{

struct utname sysInfo;

   

// os 버전 및 하드웨어 타입 정보 얻기 위한 시스템 함수

if( uname(&sisInfo) < 0 )

{

cout << "Error Occurred" << endl;

return (-1);

}

   

if( strncasecmp(sysInfo.sysname, HPUX, strlen(HPUX)) == 0 )

{

// HP 용 개체 생성 및 사용

hpScanner scanner;

   

pFacetory = new compilerFactory(&scanner);

}

else if( strncasecmp(sysInfo.sysname, SUNOS, strlen(SUNOS)) == 0 )

{

// Sun용 객체 생성 및 사용

SunScanner scanner;

   

pFactory = new compilerFactory(&scanner);

}

else

{

// 지원하지 않는 시스템 환경

cout << sysInfo.sysname << endl;

return(0);

}

   

//

scanner* pScanner;

pScanner = pFactory->createScanner();

}

Prototype 패턴을 활용할 경우 다음과 같은 특징을 가진다.

  • 제품군별로 별도의 Concrete Factory 클래스를 정의할 필요가 없다. 대신 미리 생성할 제품의 종류별로 한 개씩 객체를 생성해서 Factory 클래스에 등록해두어야 하는 비용이 발생한다. 특히 생성해야 할 제품의 종류가 많을 경우, Factory클래스의 생성자(Constructor) 인터페이스도 복잡해지고 객체 저장에 따른 비용도 커질 수 있다.
  • 같은 제품군에 속하는 제품을 생성하려면 처음에 Factory클래스에 등록시키는 객체가 같은 제품군에 속하는 객체여야 한다. 만약 그렇지 않으면 생성되는 객체가 서로 다른 제품군에 속하는 제품이 될 가능성도 있다.

새로운 제품 종류의 추가 시 문제 해결

Abstract Factory 패턴은 특정 제품군에 속하는 제품 객체를 생성하는 프로그램을 한 곳으로 모아, 제품군의 추가가 용이하게 만든 클래스 설계다.

그렇다면 이 같은 Abstract Factory 패턴은 모든 경우에 문제가 없는 것일까?

Factory클래스에 의해 생성되는 제품의 종류가 새로 추가되어야 하는 상황이 발생했다고 가정해보면, 이 때 Abstract Factory 패턴에서는 새로운 종류의 제품에 해당하는 클래스도 추가해야 하겠지만, 각각 Factory 클래스들도 모두 서장되어야 할 것이다. 왜냐하면 새로운 종류의 제품을 생성하기 위한 멤버 함수를 각 Factory클래스에 추가해야 하기 때문이다.

우선 문제의 원인을 분석해 보자.

Factory클래스에 새로운 종류의 제품 생성을 요청할 경우 왜 모든 Factory 클래스가 수정되어야 하는가? 그것은 Factory 클래스가 생성할 제품의 종류에 따라 각기 다른 멤버 함수를 가지고 있기 때문이다. 따라서 문제를 해결할 수 있는 방법은 생성할 제품의 종류에 무관하게 Factory 클래스가 사용하는 멤버 함수를 하나로 통일시키면 될 것이다. 즉, Factory클래스에서 객체 생성을 위한 모든 멤버 함수를 CreateProduct()와 같은 하나의 멤버 함수로 통일시키면 될 것이다.

먼저 Client 입장에서는 자신이 원하는 제품의 객체를 생성하기 위해 createProduct()멤버 함수를 부를 때, 생성하기 원하는 제품의 종류를 함수 인자로 전달할 필요가 있다. 그런데 이때 전달하는 인자 값이 제품의 종류에 따라 정의된 상수 값이라면 새로운 종류의 제품 객체를 추가로 생성하고자 할 때마다 createProduct()멤버 함수의 내부 구현은 매번 바뀌어야 하는 문제를 또다시 안게 될 것이다.

예를 들어 컴파일러 문제의 경우 createProduct() 멤버 함수를 구현한다면 Factory클래스가 새롭게 ErrorHandler클래스 객체를 생성하고자 할 때 createProduct()멤버 함수의 수정이 불가피할 것이다.

  • 예제 Abstract Factory 패턴에서 상수 값에 의해 생성할 객체를 판단하는 방식의 구현

#include <iostream>

#include <string>

   

using namespace std;

#define HPUX "HPUX"

#define SUNOS "SunOS"

   

class product

{

public:

virtual ~product() = 0;

};

   

product::~product(){}

   

class hpScanner : public product{};

class hpParser : public product{};

class hpErrorHandler : public product {}; // 추가 필요

class SunScanner : public product{};

class SunParser : public product{};

class SunErrorHandler : public product {}; // 추가 필요

   

class compilerFactory

{

public:

virtual product* createProduct(int type) = 0;

};

   

#define SCANNER                        1

#define PARSER                        2

#define ERRORHANDLER        3 // 추가 필요

   

class hpCompilerFactory : public compilerFactory

{

public:

product* createProduct(int type)

{

switch(type)

{

case SCANNER: return new hpScanner;

case PARSER: return new hpParser;

case ERRORHANDLER : return new hpErrorHandler; // 추가 필요

}

}

};

   

class SunCompilerFactory : public compilerFactory

{

public:

product* createProduct(int type)

{

switch(type)

{

case SCANNER: return new SunScanner;

case PARSER: return new SunParser;

case ERRORHANDLER : return new SunErrorHandler; // 추가 필요

}

}

};

   

compilerFactory* pFactory;

   

int main()

{

struct utsname sysInfo;

   

// OS 버전 및 하드웨어 타입 정보 얻기 위한 시스템 함수

if( uname(&sysInfo) < 0 )

{

cout << "Error Occurred" << endl;

return (-1);

}

   

if( strncasecmp(sysInfo.sysname, HPUX, strlen(HPUX)) == 0 )

{

// HP용 객체 생성 및 사용

pFactory = new HpCompilerFactory;

}

else if( strncasecmp(sysInfo.sysname, SUNOS, strlen(SUNOS)) == 0 )

{

// sun용 객체 생성 및 사용

pFactory = new SunCompilerFactory;

}

else

{

// 지원하지 않는 시스템 환경

cout << sysInfo.sysname << endl;

return(0);

}

   

product *pScanner = pFactory->createProduct(SCANNER);

product *pParser = pFactory->createProduct(PARSER);

//product *pErorHandler = pFactory->createProduct(ERRORHANDLER);

}

결론적으로 위의 예제 형태로 createProduct()멤버 함수를 구현하면 새로운 종류의 제품 객체를 생성하도록 추가할 때 기존 클래스 구현의 변경이 불필요하게 만들겠다는 Abstract Factory패턴의 목적을 만족시키지 못한다.

한가지 고려해볼 수 있는 방법은 createProduct()멤버 함수의 인자로 생성하고자 하는 객체의 원형을 직접 전달하고, createProduct()멤버 함수는 그 객체의 복제 객체를 생성해주는 protytype패턴을 활용하는 것이다.

  • 예제 Prototype패턴을 활용하여 새로운 종류의 제품 생성 추가 문제 해결방법

#include <iostream>

#include <string>

   

using namespace std;

#define HPUX "HPUX"

#define SUNOS "SunOS"

   

class product

{

public:

virtual product* clone() = 0;

};

   

class hpScanner : public product

{

public:

product* clone() { return new hpScanner(*this); }

};

   

class hpParser : public product

{

public:

product* clone() { return new hpParser(*this); }

};

   

class SunScanner : public product

{

public:

product* clone() { return new SunScanner(*this); }

};

   

class SunParser : public product

{

public:

product* clone() { return new SunParser(*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:

virtual product* createProduct(product *p)

{

return p->clone();

}

}

   

class SunCompilerFactory : public compilerFactory

{

public:

virtual product* createProduct(product *p)

{

return p->clone();

}

}

   

compilerFactory* pFactory;

   

int main()

{

product* pScanner, *pParser;

product* pErrorHandler; // 추가 필요

struct utname sysInfo;

   

// os 버전 및 하드웨어 타입 정보 얻기 위한 시스템 함수

if( uname(&sisInfo) < 0 )

{

cout << "Error Occurred" << endl;

return (-1);

}

   

if( strncasecmp(sysInfo.sysname, HPUX, strlen(HPUX)) == 0 )

{

// HP 용 개체 생성 및 사용

pScanner = new HpScanner;

pParser = new HpParser;

pErrorHandler = new HpErrorHandler; // 추가 필요

   

pFactory = new HpCompilerFactory;

}

else if( strncasecmp(sysInfo.sysname, SUNOS, strlen(SUNOS)) == 0 )

{

// Sun용 객체 생성 및 사용

pScanner = new SunScanner;

pParser = new SunParser;

pErrorHandler = new SunErrorHandler; // 추가 필요

   

pFactory = new SunCompilerFactory;

}

else

{

// 지원하지 않는 시스템 환경

cout << sysInfo.sysname << endl;

return(0);

}

   

//

product* pNewScanner = pFactory->createProduct(pScanner);

product* pNewParser = pFactory->createProduct(pParser);

// product* pNewErrorHandler = pFactory->createProduct(pErrorHandler);

}

Client는 AbstractFactory 클래스와 AbstractProductA, AbstractProductB 클래스를 사용하는 형태인데, 이는 실제 만들어지는 객는 각각의 하위 클래스에 해당하는 것이어도 client가 참조하는 자료형은 상위의 추상클래스임을 의미한다.

Abstract Factory 패턴이 유용한 경우를 정리하면 다음과 같다.

  • 객체의 생성을 클라이언트(Client)가 직접 하는 것이 아니라, 간접적으로 수행함으로써 클라이언트가 객체의 생성이나 구성 또는 표현 방식에 독립적이도록 만들고자 할때
  • 여러 제품군 중 사용할 제품군을 쉽게 선택할 수 있도록 만들고 싶을 때
  • 서로 관련된 제품들이 하나의 제품군을 형성하고, 이런 제품군이 여러 개 존재한느 상황에서 생성되는 제품 객체가 항상 같은 제품군에 속하는 것을 확실히 보장하고 싶을 때
  • 제품들에 대한 클래스 라이브러리를 만들어야 하는데 그 인터페이스만 드러내고 구현은 숨기고 싶을 때, 이때 각각의 인터페이스는 Abstract Factory 클래스와 제품 종류별 Abstract Base Class(ABC)에 의해 외부에 드러나며, 구체적인 구현은 하위 클래스에 의해 이루어진다.

Abstract Factory패턴은 제품군별 객체 생성이 필요할 경우 매우 유용한 패턴이며, 장단점은 다음과 같다

  • Abstract Factory 패턴의 장점은 객체가 생성되는 방식이나 과정 및 책임을 클라이언트가 모르도록 만들어준다. 이때 클라이언트는 다만 AbstractFactory클래스와 AbstractProduct클래스들의 인터페이스만을 사용하면 된다. 이로써 클라이언트 소스코드는 객체가 생성되는 방식이나 과정 및 생성하는 종류가 변견되더라도 그 부분을 국지화될 수 있다.
  • Abstract Factory 패턴의 또 다른 장점은 제품군(Product Family)간 교체가 쉽다는 것이다. 즉, ConcreteFactory 클래스의 객체가 생성되는 부분만 변경시켜주면 얼만든지 다른 제품군을 생성하도록 바꿀 수 있다.
  • Abstract Factory패턴을 사용하게 되면 여러 제품군들이 실수로 섞여서 사용되는 것이 자연스럽게 방지된다. 왜냐하면 특정 ConcreteFactory 클래스는 특정한 제품군만을 생성하기 때문이다.
  • Abstract Factory패턴의 단점은 제품군의 개수가 늘어날수록 Concrete Factory클래스의 개수도 늘어나야 한다는 점이다. 따라서 제품군의 개수가 많아졌을 경우 클래스가 많아져 설계가 복잡해지게 된다.
  • 가장 큰 단점은 제품군에 새로운 제품이 추가되어야 할 경우, 모든 Factory 클래스를 수정해야 한다는 것이다. 예를 들어 컴파일러 문제에서 ErrorHandler와 같이 새로운 제품을 생성할 필요가 생기면 모든 Factory 클래스에 createErrorHandler()멤버 함수를 추가해야 하는 문제가 발생한다.
반응형