2016년 4월 2일 토요일

[디자인 패턴] 추상 팩토리 패턴 ( Abstract Factory )

팩토리 자체를 추상으로 가져가는 것 !!! 
제품군이 늘어나면 일일히 제품군 클래스를 만들어줘야함.
제품군안에 제품이 추가되어 지면, 제품군 클래스가 모두 바뀌어야 함.
클라이언트 측 구현에 변화가 없고, 디펜던시를 팩토리로 몰아서 넣을 수 있다는 장점 

1. 객체 생성을 위한 디자인 패턴
  • Singleton(싱글톤) 패턴
  • Abstract Factory 패턴
  • Builder 패턴
  • Factory Method 패턴
  • Prototype 패턴
이제 이번 시간부터는 객체 생성을 위한 디자인 패턴 중에서 Abstract Factory 패턴에 대해 알아보겠습니다. 일단 개념부터 알아보도록 하죠.
Abstract Factory 패턴은 객체를 생성할 때 반드시 필요한 제품군에 해당하는 것만 생성해서 사용하고자 할 경우 사용합니다.
제품군의 대표적인 예는 자동자를 생각하면 될 것 같습니다. 자동차는 흔히 종합 예술이라고도 하잖아요 ? 모든 기술이 축적된 제품이기 때문이죠. 즉, 자동차 내부에는 엔진이라는 제품, 오디오라는 제품, 네비게이터라는 제품 등 여러 제품이 있습니다. 하지만, 이러한 각 제품들은 아마도 소나타에 들어가는 제품과 그랜저에 들어가는 제품, 프라이드에 들어가는 제품이 제각기 다를 것입니다.
소나타에 들어가는 것들은 그러한 제품들끼리, 그리고 프라이드에 들어가는 제품들끼리 묶어놓은 것을 제품군이라고 합니다. 즉, 같은 환경 내에서 동작하게 될 것들끼리만 묶어놓은 것을 말합니다.
소프트웨어에서도 이 같은 제품군을 생각해볼 수 있습니다.
예를 들면, 다양한 플랫폼에서 동작해야 하는 프로그램을 만들어야 한다고 가정해 봅시다. 이 프로그램은 기능 단위로 쪼갠 다양한 Class 를 가지고 있는데, 이 Class 는 아마도 운영체제에 따라 달라져야할 것입니다. 왜냐하면, 운영체제에 따라 Class 내부의 함수 구현 등도 달라질 것이기 때문이다.
제가 전문인 분야가 DB (Database)이니 DB 를 만든다고 가정하고 접근해 봅시다.
DB 에는 다음 3 가지 정도의 모듈은 반드시 필요합니다.
  1. 사용자와의 접속을 제어하는 Session Control Module
  2. 사용자의 Query 를 분석하고 최적화시키는 Query Optimizer
  3. Disk 또는 Memory 에 저장된 데이터에 접근 및 저장을 수행하는 Storage Manager
사용자가 Database 에 접속해서 Query 를 통해 저장된 데이터를 조회해보기까지 DB 엔진에서는 위의 모듈들이 유기적으로 잘 동작하여 결과를 던져주도록 구현되어 있습니다.
그런데, DB 는 다양한 플랫폼을 지원해야하는 경우가 많습니다. SunOS, HP, IBM AIX, Linux 등 다양한 환경을 모두 지원해야 Hardware 및 운영체제 플랫폼에 구애받지 않고 좀 더 다양한 고객을 확보할 수 있기 때문입니다.
위와 같이 DB 를 예를 든다면 3 가지의 모듈이 곧 제품군이 될 것입니다. 위 3 가지의 모듈이 플랫폼에 따라 달라지게 되겠죠.
우리는 개발 인력이 부족해서 Linux 와 AIX 만 지원하는 DB 를 만든다고 가정해 봅시다. 이 때 필요한 모듈을 생각해보면…
  • Session Control Module for Linux
  • Query Optimizer for Linux
  • Storage Manager for Linux
  • Session Control Module for AIX
  • Query Optimizer for AIX
  • Storage Manager for AIX
위와 같이 6 개의 모듈이 필요할 것입니다.
각각을 Class 로 구현한다고 생각하면 총 6 개의 Class 가 필요하겠죠.
하지만, 특정 고객에게 제품을 공급하고자 하는데 그 고객은 Linux 만 사용하는 고객이라고 가정해보면, 그 고객에게 제공할 제품에 AIX 용 Class 들이 필요할까요 ?
아마도 불필요할 것입니다.
이럴 때는 Linux 용 모듈들만 즉, Linux 용 Class 의 객체들만 생성해서 동작하도록 하면 됩니다. Linux 에서 동작하는데 AIX 용 객체들까지 생성할 필요는 없잖아요 ? (설사 생성한다 해도 동작도 제대로 못할 것이기 때문에 생성을 막는 것이 정답이겠죠.)
자, 그럼 우리는 개발자이기 코드로 얘기해 봅시다. ^^
개념적으로 pseudo code 만 작성해 보도록 하죠.
class linux_session_control {};
class linux_query_proc {};
class linux_storage_mgr {};
class aix_session_control {};
class aix_query_proc {};
class aix_storage_mgr {};
int platform;
void make_session_control()
{
    // platform 별로 각각 적합한 객체 생성
    if( platform_is_linux )
    {
        linux_session_control  session_control;
    }
    else 
if( platform_is_aix )
    {
        aix_session_control session_control;
    }
    else
    {
        // invalid platform
    }}
void make_query_proc()
{
    
// platform 별로 각각 적합한 객체 생성
    if( platform_is_linux )
    {
        
linux_query_proc query_proc;
    }
    else 
if( platform_is_aix )
    {
        
aix_query_proc query_proc;
    }
    else
    {
        // invalid platform
    }}
void make_storage_mgr()
{
    // 위와 같은 로직
}
int main()
{
    // check plaform here
    
// platform 만 결정해주면 하위 함수에서 알아서 알맞은 객체를 생성해 줌.
    
make_session_control();    make_query_proc();
    make_storage_mgr();
}
위의 코드를 대강 보면 마치 platform 에 따라 각기 알맞은 객체를 알아서 생성해주고 사용자는 platform 만 결정해주면 되는 좋은 코드처럼 보입니다.
하지만, DB 의 부품이 Session Control, Query Optimizer, Storage Manager 외에 아주 다양하고, 지원해야할 platform 은 날이 갈수록 늘어난다고 가정을 해보죠.
이러한 기능 확장과 환경의 확장은 위의 코드를 구석구석 돌아다니며 모두 else if 를 추가해주어야 한다는 것을 의미합니다. 간단한 소프트웨어라면 상관없지만, 백만 라인이 넘어가고 위와 같은 로직이 여기 저기 산재해 있다면, 아마도 현재 이외의 platform 지원은 거의 포기해야할 지경일 겁니다.
그렇다면 위의 단점을 개선할 방법이 없을까요 ?
물론 있습니다. ^^
위 코드의 문제에 대한 해결방법은 다음 시간에 살펴볼 것인데요. 다음의 2 가지 방식을 살펴볼 것입니다.
  • 객체 생성 전담 클래스를 이용하는 방법
  • Abstract Factory 패턴을 이용하는 방법
이번 시간에는 이전 시간에 살펴보았던 코드의 문제점을 개선하는 두가지 방식 중 첫번째 방법에 대해 살펴보겠습니다.
  • 객체 생성 전담 클래스를 이용하는 방법
  • Abstract Factory 패턴을 이용하는 방법
이전 시간에 살펴보았던 코드의 가장 큰 문제점은 변경될 가능성이 많은 로직들이 프로그램의 곳곳에 산재해 있다는 점입니다. 즉, 무언가 하나 요구조건이 추가되거나 변경되면 프로그램 이곳저곳을 들쑤시고 다녀야 합니다. 이런 경우에는 십중팔구는 기존에 없던 버그를 양산하게 마련입니다.
이러한 단점을 없애는 방법은 당연히 변경이 될 가능성이 높은 프로그램 영역들을 한쪽으로 몰아버리는 것이겠지요. 이것을 변경의 국지화(Localization of Change)라고 합니다.

그렇다면, 어떻게 그런 부분들을 국지화시킬까요 ?
이 때 Class 의 “정보 은닉” 기능을 사용하면 됩니다.
즉, 변경될 가능성이 많은 정보와 그렇지 않은 정보를 구분해서 변경될 가능성이 많은 정보는 Class 로 내부로 숨겨서 필요할 경우 해당 내부만 변경시켜주면 되는 방식입니다.

코드를 한번 보도록 하죠. 이제 실제 실행시킬 수 있는 코드로 만들어볼까요 ?
개발자들은 코드를 실행해보고 눈으로 결과를 봐야 좀 이해가 빠른 경향이 있죠. ^^
코드가 너무 길어지기 때문에 앞에서 살펴보았던 3 가지 database 의 모듈 중에서 한가지(Storage Manager)는 제외하고 2 가지만 이용하여 구현해 보겠습니다.
#include <stdio.h>
#include <stdlib.h>

// linux 의 모듈과 aix 의 모듈의 동일한 인터페이스를 가지도록 class session_control
{
public:
    virtual ~session_control() = 0;
};

class query_proc
{
public:
    virtual ~query_proc() = 0;
};
session_control::~session_control() {}
query_proc::~query_proc() {}

// 사용자에게 open 되는 class
class database_factory
{
public:
    session_control * make_session_control();
    
query_proc * make_query_proc();};

// platform 별 구체 class – 모듈 별 인터페이스를 상속class linux_session_control : public session_control
{
public:
    linux_session_control();
};

class linux_query_proc : public query_proc
{
public:
    linux_query_proc();
};
class aix_session_control : public session_control
{
public:
    aix_session_control();
};

class aix_query_proc : public query_proc
{
public:
    aix_query_proc();
};

// 어떤 객체가 생성되었는지를 보기 위해 생성자에서 찍어봄
linux_session_control::linux_session_control()
{
    printf(“
linux_session_control\n”);
}

linux_query_proc::linux_query_proc()
{
    printf(“
linux_query_proc\n”);
}

aix_session_control::aix_session_control()
{
    printf(“
aix_session_control\n”);
}

aix_query_proc::aix_query_proc()
{
    printf(“ai
x_query_proc\n”);
}

// Platform 결정
int platform;

session_control * 
database_factory::make_session_control()
{    // platform 별로 각각 적합한 객체 생성
    if( platform == 1 )
    {
        return new linux_session_control;
    }
    else 
if( platform == 2 )
    {
        return new aix_session_control;
    }
    else
    {
        // invalid platform
    }}
query_proc * database_factory::make_query_proc()
{
    
// platform 별로 각각 적합한 객체 생성
    if( platform == 1 )
    {
        return new 
linux_query_proc;
    }
    else 
if( platform == 2 )
    {
        return new 
aix_query_proc;
    }
    else
    {
        // invalid platform
    }}
// 사용자 구현 영역
int main(int argc, char * argv[])
{    database_factory  fact;    platform = atoi(argv[1]);  // 편의상. 실제로는 이렇게 사용자 영역에 들어가면 안됨.
    fact.make_session_control();    fact.make_query_proc();
}
위의 코드에서 platform 을 결정하는 것은 그냥 편의 상 command line argument 를 사용했습니다. 실제로는 platform 을 결정하는 코드가 위와 같이 되지는 않을 것이고, struct utsname 등을 사용해야할 것입니다.
위의 코드를 보면 이전 시간에 살펴본 것과는 달리 사용자가 database_factory 라는 class 만 바라보도록 설계되어 있습니다. 만약 platform 이 추가되더라도 사용자의 코드는 변경될 것이 없습니다.
단지 class 를 제공하는 측에서 신규 platform 에 대한 class 를 별도로 만들고, database_factory 의 멤버 함수들의 내부를 수정하면 될 뿐이죠.

한번 실행해보죠.
$ g++ db.cpp
$ ./a.out 1
linux_session_controllinux_query_proc
$./a.out 2
aix_session_control
aix_query_proc
와우… platform 을 1 로 주면 linux 에서 필요한 모듈객체들이 생성되고, 2 로 주면 aix 에서 필요한 모듈객체들이 자동으로 생성되네요.
어때요?  마음에 드시나요 ?
저는 위의 코드가 썩 마음에 들지는 않습니다. 그 이유는 다음의 두 가지 이유때문입니다.
  1. 여전히 객체를 생성하는 곳마다 비교문장들이 여기 저기 산재해 있어서 새로운 조건을 추가하기 위해서는 기존 소스를 아주 세세히 분석해보아야 한다.
  2. 새로운 조건을 추가할 때 기존의 소스코드와의 dependancy 가 심하다. 즉, 기존 소스코드와 무관하게 독립적으로 추가할 수가 없다.
프로그램의 모듈화라는 관점에서 위의 코드가 아직 부족하다는 의미입니다.
그렇다면 위의 단점은 어떻게 보완할 것인가?
그것이 바로 Abstract Factory 패턴을 이용하는 방법입니다. 이 방법은 다음 시간에 살펴보겠습니다.


자, 이제 그럼 마지막으로 이번에는 Abstract Factory 패턴을 이용하여 문제를 해결해보도록 하죠.
  • 객체 생성 전담 클래스를 이용하는 방법
  • Abstract Factory 패턴을 이용하는 방법
“객체 생성 전담 클래스를 이용하는 방법”에서 두 가지 단점에 대해 언급했는데, 첫번째가 객체 생성 시마다 반복해서 일일이 조건 검사가 이루어져야 했다는 점입니다.
객체 생성할 때마다 조건검사를 할 필요가 없게 만들기 위해서는, 이전에 살펴보았던 코드에서 database_factory 를 linux_database_factory 와 aix_database_factory class 가 상속하도록 하고 외부에서는 database_factory 만 보도록 만드는 것입니다.
즉, 아래와 같은 상속을 이용한 class 정의를 합니다. 이전에는 linux platform 이면 linux_session_contol 객체를 생성했고, aix 이면 aix_session_control 객체를 생성하는 조건 검사가 제품(모듈)을 만들 때마다 매번 이루어졌지만, 이번에는 linux 용과 aix 용의 제품군을 만들어내는 factory class 를 중간에 하나 더 끼워넣은 것입니다.
// 사용자에게 open 되는 class – Abstract Base Classclass database_factory
{
public:
    virtual session_control * make_session_control() = 0;
    virtual 
query_proc * make_query_proc() = 0;};

// platform 에 따른 제품군을 생성하는 Factory Class
class linux_database_factory : public database_factory
{
    session_control * make_session_control() { new linux_session_control; }
    query_proc * make_query_proc() { new linux_query_proc; }
}

class aix_database_factory : public database_factory
{
    session_control * make_session_control() { new aix_session_control; }
    query_proc * make_query_proc() { new aix_query_proc; }
}
위의 방식을 이용하면 전체적으로 코드가 어떻게 편리하게 변경되는지 살펴보죠.
#include <stdio.h>
#include <stdlib.h>

// linux 의 모듈과 aix 의 모듈의 동일한 인터페이스를 가지도록 class session_control
{
public:
    virtual ~session_control() = 0;
};

class query_proc
{
public:
    virtual ~query_proc() = 0;
};
session_control::~session_control() {}
query_proc::~query_proc() {}

// platform 별 구체 class – 모듈 별 인터페이스를 상속class linux_session_control : public session_control
{
public:
    linux_session_control();
};

class linux_query_proc : public query_proc
{
public:
    linux_query_proc();
};
class aix_session_control : public session_control
{
public:
    aix_session_control();
};

class aix_query_proc : public query_proc
{
public:
    aix_query_proc();
};

// 사용자에게 open 되는 class
class database_factory
{
public:
    virtual session_control * make_session_control() = 0;
    virtual 
query_proc * make_query_proc() = 0;};

// 동일한 제품군 별로 객체를 생성해주는 class
class linux_database_factory : public database_factory
{
    session_control * make_session_control() { new linux_session_control; }
    query_proc * make_query_proc() { new linux_query_proc; }
};

class aix_database_factory : public database_factory
{
    session_control * make_session_control() { new aix_session_control; }
    query_proc * make_query_proc() { new aix_query_proc; }
};

// 어떤 객체가 생성되었는지를 보기 위해 생성자에서 찍어봄
linux_session_control::linux_session_control()
{
    printf(“
linux_session_control\n”);
}

linux_query_proc::linux_query_proc()
{
    printf(“
linux_query_proc\n”);
}

aix_session_control::aix_session_control()
{
    printf(“
aix_session_control\n”);
}

aix_query_proc::aix_query_proc()
{
    printf(“ai
x_query_proc\n”);
}

// Platform 결정
int platform;

// 사용자 구현 영역
int main(int argc, char * argv[])
{    database_factory  * fact;    platform = atoi(argv[1]);  // 편의상. 실제로는 이렇게 사용자 영역에 들어가면 안됨.
    if( platform == 1 ) fact = new linux_database_factory;    else if( platform == 2 ) fact = new aix_database_factory;
    else
    {
        printf(“invalid platform\n”);
        return 1;
    }

    fact->
make_session_control();    fact->make_query_proc();
}
실행해보면 이전 코드를 실행했을 때와 결과는 같습니다.
위의 코드를 잘 살펴보면 이전의 코드와 다른 두가지 점이 있습니다. (용어를 잘 생각하면서 이해하시기 바랍니다. ^^)
  • 첫째는, session_control 및 query_proc 모듈(제품)에 대한 객체를 생성(make_xx)할 때마다 platform 조건 검사를 할 필요가 없도록 아예 platform 별 제품군 생성을 위한 class(linux_database_factory, aix_database_factory)를 별도로 두었다는 점입니다. 이러한 class 를 최상위의 추상화 Factory Class(Abstract Base Factory Class)에 대응하여 구체화되었다는 의미로 Concrete Factory Class 라고 부르기도 합니다.
  • 둘째는, 첫째와 같이 구성한 결과로 조건 검사 부분은 초기에 딱 한번만 이루어지면 된다는 점입니다. 제품을 만들 때마다 검사가 필요한 것이 아니고 초기에 platform 이 결정되면, 해당 platform 에 맞는 제품군 생성 용 class(Concrete Factory Class)에 대한 객체를 생성하여 이를 사용자 Open 용 껍데기 class(Abstract Base Class)-코드 상에서는 database_factory-를 통해 접근하도록 합니다.
위와 같이 이용하는 것이 Abstract Factory 패턴 방식입니다.
이제 많이 좋아졌나요?
자, 이제 새로운 platform 이 추가되었다고 가정해보면 어떤 작업들이 필요할까요 ? 아마도 다음 3 가지의 변경 및 추가작업을 해야할겁니다. hp platform 에서도 지원해야한다고 가정을 해보죠.
  1. 추가되는 platform 에 대해 모듈 별 class 를 추가해야 합니다. 즉, hp_session_control, hp_query_proc class 를 정의해야 합니다.
  2. HP 용 제품군을 생성할 Concrete Factory Class(hp_database_factory)를 정의해야 합니다.
  3. main 함수에서 patform 이 하나 추가되므로 else if 하나가 추가되어야 합니다.
여기까지가 Abstract Factory 패턴에 대한 내용입니다.
어때요 ? Abstract Factory 패턴을 이용한 최종 결과가 마음에 드시나요 ?
왠지 저는 그리 마음에 들지 않는군요. 2 가지 커다란 단점이 눈에 보입니다.
그 단점들이 무엇인지 살펴보도록 하죠.
이것이 결론적으로 Abstract Factory 패턴의 단점이라고 할 수 있습니다.
  • 첫번째, 새로운 platform 마다 class 가 추가되어야 합니다. 즉, 제품군의 숫자가 많아질수록 Concrete Factory Class(제품군 생성 Class) 의 수도 늘어나서 잘못하면 Class 의 수가 지나치게 많아집니다.
  • 두번째, 만약 새로운 제품이 추가된다면 모든 Factory Class 들을 수정해야 합니다. 즉, 위의 예에서 session_control 과 query_proc 외에 storage_manager 모듈 하나가 추가되면 linux_storage_manager/aix_storage_manager class 추가 뿐 아니라, database_factory/linux_database_factory/aix_databas_factory class 들에 make_storage_manager 함수들이 모두 추가되어야 합니다. 한마디로 새로운 제품이 추가되면 쥐약인 구조이죠.
위의 내용을 보면 절대로 Abstract Factory 패턴 하나만으로는 완벽한 모듈화를 설계할 수 없겠죠. 그래서, 디자인 패턴은 어느 하나의 패턴만으로 이상적인 설계를 구현하기는 힘든 경우가 많습니다.
위의 패턴에 다른 패턴 또는 다른 아이디어가 같이 융합이 되어야 좋은 구조가 나올 수 있다는 것을 명심해야 합니다.
여기까지 하구요.
다음 시간 부터는 Builder 패턴에 대해 알아보도록 하겠습니다.

댓글 없음:

댓글 쓰기

[Effective C++] 항목 30 : 인라인 함수는 미주알고주알 따져서 이해해 두자.

인라인 함수를 사용하면 컴파일러가 함수 본문에 대해 문맥별 최적화를 걸기가 용이해집니다. 인라인 함수의 아이디어는  함수 호출문을 그 함수의 본문으로 바꿔치기하자는 것  남발했다가는 코드의 크기가 커질 게 뻔하다. 인라인 함수로 부풀려진 ...