2016년 4월 6일 수요일

[디자인 패턴] 데코레터 패턴 ( Decorator Pattern )

Decorator Pattern

객체를 장식하는 형식으로 실행 중에 클래스를 꾸미는 패턴이다.
원래 클래스의 코드는 전혀 바꾸지 않고도 객체에 새로운 임무를 부여할 수도 있다.
( 즉, 부모클래스를 건드리지않고, 원하는 함수를 추가할 수 있다. 이래서 쓰는것임)

1. Decorator Pattern의 정의
데코레이터 패턴에서는 객체에 추가적인 요건을 동적으로 첨가한다.
데코레이터는 자식클래스를 만드는 것을 통해서 기능을 유연하게 확장할 수 있는 방법을 제공한다.

2. Decorator Pattern의 특징
1) 데코레이터의 부모클래스는 자신이 장식하고 있는 객체의 부모클래스와 같으며, 한 객체를 여러 개의 데코레이터로 감쌀 수 있다.
2) 또한 데코레이터는 자신이 감싸고 있는 객체와 같은 부모클래스를 가지고 있기 때문에 원래 객체가 들어갈 자리에 데코레이터 객체를 집어넣어도 상관 없다.(이게 포인트!  클래스 다이어그램을 보고 이해할수 있어야함)
데코레이터는 자신이 장식하고 있는 객체에게 어떤 행동을 위임하는 것 외에 원하는 추가적인 작업을 수행할 수 있고 객체는 언제든지 감쌀 수 있기 때문에 실행에 필요한 데코레이터를 마음대로 적용할 수 있다.

3. Decorator Pattern의 클래스 다이어그램
 Component
   


나의 해석:
우리가 쓰는것은
 concreateDecoratorA,B,C,D....등등 이런 클래스이다.
이런 클래스에 어떤 행동같은것을 추가하려면 보통은 상속을 이용해서 부모클래스에 추가해주는게 보통이다.
근데, 너무 많은 자식클래스가 있으면 부모클래스에 쓸데없는 멤버변수들이 자꾸 추가되는게 불만이다!!!
그래서 실행중에 동적할당을 이용해서 데코레이션을 해줄 수 있는 패턴이 있는데 이가 데코레이션 패턴이다!!!!

기본상속개념을 위해
 ConcreateComponent 같은 클래스로 자식클래스를 만들어준다.
A,B,C,D...이런클래스에서 Concreateponent 포인터를 하나 가지고있다.

이 포인터를 가지고 꾸며주기가 가능한 것이다. ( 덮어쓰기 개념을 이용 )
A,B,C,D 에서 행동을 한 후 레퍼런스를 계속 넘겨주는 식으로 하면 된다. 

A,B,C,D에 반드시 정의되야하는 함수들이 있으므로, 순수가상함수를 만들어주기위한 Decorator란 클래스가 필요
그리고, 
Decorator에서 Concreateponent를 계속적으로 받아줘야하므로 둘은 같은 부모로 부터 상속받은 클래스여야한다. 그래서 최상위 Component 클래스가 있는것이다.

 이해되나?~

이해하고 보니 상속개념을 역으로 이용한 것 같다는 느낌 ! ( is-a 관계가 아닌 상속의 원리만 이용한것이다! )
즉, "부모포인터는 자식을 받을 수 있다"를 이용함 ㅎㅎㅎ
다형성을 아주 잘 이용한 설계라고 볼수 있겠다.



4. Decorator Pattern의 구성
A. 각 구성요소는 직접 쓰일 수도 있고 데코레이터로 감싸져서 쓰일 수도 있다.
B. ConcreteComponent에 새로운 행동을 동적으로 추가하게 된다.
C. 각 데코레이터 안에는 Component 객체가 들어있다.
 
    즉, 데코레이터에는 구성요소에 대한 레퍼런스가 들어있는 인스턴스 변수가 있다. D. Decorator는 자신이 장식할 구성요소와 같은 인터페이스 또는 추상 클래스를 구현한다.
E. Decorator는 Component의 상태를 확장할 수 있다.
F. ConcreteDecorator에는 그 객체가 장식하고 있는 것(데코레이터가 감싸고 있는 Component 객체)을 위한 인터페이스 변수     가 있다.
G. 데코레이터에서 새로운 메소드를 추가할 수도 있다.
     하지만 일반적으로 새로운 메소드를 추가하는 대신 
Component에 원래 있던 메소드를 호출하기 전, 또는 후에 별도의 작업     을 처리하는 방식으로 새로운 기능을 추가한다.

5. Decorator Pattern의 OCP(Open-Closed Principle) 디자인 원칙
OCP(Open-Closed Principle)이란 ?
클래스는 확장에 대해서는 열려 있어야 하지만 코드 변경에 대해서는 닫혀 있어야 한다는 뜻
으로 기존 코드는 건드리지 않은 채로 확장을 통해서 새로운 행동을 간단하게 추가할 수 있도록 코드의 수정을 허용하는 것이다. 새로운 기능을 추가하는 데 있어서 매우 유연해서 급변하는 주변 환경에 잘 적응할 수 있으면서도 강하고 튼튼한 디자인을 만들 수 있다.

6. Decorator Patternd의 장단점
장점: 
데코레이터 패턴은 기본적인 데이터에 첨가할 데이터가 다양하고 일정하지 않을 때 효율적이다.

단점: 
데코레이터 패턴을 사용하면 자잘한 객체들이 매우 많이 추가될 수 있고, 데코레이터를 너무 많이 사용하면 코드가 필요 이상으로 복잡해 질 수 도 있다.
( 가독성이 떨어진다. 일단 이게 데코레이션 패턴인지 분석을 해야하니까~ )

 
--------------------------------------------------------------------------------------------------------------
제가 보는 책(head first design patterns)에서는 데코레이션 패턴을 커피 예를 들어서 설명을 했습니다.
커피집에 가면 커피가 굉장히 많은데 그 커피에 대한 클래스를 만들고 모든 재료, 기능들을 넣는 것 보단 커피 클래스 따로 재료 클래스 따로 만들고 있었습니다.
커피 클래스, 재료 클래스를 따로 만들게 되었을 때 서로 유기적으로 잘 묶기만 한다면 한 클래스에 모든 기능을 넣는 것 보단 당연히 구조적으로 좋겠죠?
데코레이션 패턴에서는 유기적으로 묶는 방법으로 "감싸"는 것을 선택했습니다.




위 클래스 다이어그램을 보시면 가장 상위에 Coffee 추상 클래스가 보입니다. 
그 아래로 그것을 상속받는 Americano, Espresso, Cafelatte같은 클래스가 있습니다. ( 꾸며주는 클래스들~~~ )
또, Decorator라는 추상클래스가 있는데 이 클래스는 getDescription이라는 순수가상함수를 갖고 있습니다. ( 순수가상함수를 위해 만들어진 클래스 )
그래서 아래 재료들이 무조건 이 함수를 새로 정의하게 만들었습니다.




이 패턴의 시나리오는 위 소스에서 보는 것과 같이 원하는 커피 인스턴스를 생성하고 원하는 재료를 만들어서 서로에게 계속 참조를 시킨다는 것입니다. (감싼다는 의미가 이것입니다.)
사용자가 getDescription함수를 호출하거나 cost 함수를 호출하면 제어가 계속 참조하는 곳으로 올라가게 됩니다.
(아래 소스 참조)



이런 식으로 사용자는 예전부터 있던 클래스를 하나도 수정하지 않고 단지 "감싸"서 예전 클래스에 새로운 기능을 부여하는 것이 가능합니다.


출저:http://cafe.naver.com/ccjmaster.cafe?iframe_url=/ArticleRead.nhn%3Farticleid=530&디자인패턴중에 데코레이션 패턴을 공부해보았다.
첨에 뭐가 뭔지 몰라서 완전 헷갈렸다.
하지만, 완벽분석 후 내 나름대로 정리해보았다.
출저는 저기이고, 회색 네모가 내가 정리한 내용이다. 

2016년 4월 5일 화요일

[디자인 패턴] 전략 패턴 ( Strategy Pattern )




Strategy Pattern - 전략 패턴

  • 동적으로 알고리즘을 교체할 수 있는 구조
  • 알고리즘 인터페이스를 정의하고, 각각의 알고리즘 클래스별로 캡슐화하여 각각의 알고리즘을 교체 사용 가능하게 한다
  • 즉, 하나의 결과를 만드는 목적(메소드)은 동일하나, 그 목적을 달성할 수 있는 방법(전략, 알고리즘)이 여러가지가 존재할 경우
  • 기본이 되는 템플릿 메서드(Template Method Pattern) 패턴과 함께 가장 많이 사용되는 패턴 중에 하나이다



샘플 코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
//------------------------------------------------------------------
// Strategy Interface class
class Strategy
{
public:
    virtual void AlgorithmInterface() = 0;
};
 
//------------------------------------------------------------------
// Strategy Algorithm A
class ConcreteStrategyA : public Strategy
{
public:
    void AlgorithmInterface() override { cout << "Processed by Strategy A" << endl; }
};
 
//------------------------------------------------------------------
// Strategy Algorithm B
class ConcreteStrategyB : public Strategy
{
public:
    void AlgorithmInterface() override { cout << "Processed by Strategy B" << endl; }
};
 
//------------------------------------------------------------------
// Strategy Algorithm C
class ConcreteStrategyC : public Strategy
{
public:
    void AlgorithmInterface() override { cout << "Processed by Strategy C" << endl; }
};
 
//------------------------------------------------------------------
// Context
class Context
{
public:
    Context() : m_pStrategy(0) {}
    ~Context() { if (m_pStrategy) delete m_pStrategy; }
public:
    void ChangeStrategy(Strategy* pStrategy) 
    { 
        if (m_pStrategy) delete m_pStrategy; 
        m_pStrategy = pStrategy;
    }
    void ContextInterface() { m_pStrategy - > AlgorithmInterface(); }
private:
    Strategy* m_pStrategy;
};
 
//------------------------------------------------------------------
// Main
int _tmain(int argc, _TCHAR* argv[])
{
    Context* pContext = new Context();
    pContext->ChangeStrategy(new ConcreteStrategyA());
    pContext->ContextInterface();
 
    pContext->ChangeStrategy(new ConcreteStrategyB());
    pContext->ContextInterface();
 
    pContext->ChangeStrategy(new ConcreteStrategyC());
    pContext->ContextInterface();
    delete pContext;
 
    return 0;
}



예제를 통한 전략 패턴(Strategy Pattern) 알아보기

예제 1) 정렬 알고리즘

다음 그림과 같이 데이터 정렬을 하는 알고리즘이 있다고 한다면, 무수히 많은 알고리즘이 존재할 것이다. 그럴 경우 하나의 정렬 인터페이스를 정의하고 각각의 알고리즘들을 하위 클래스에서 작성하고 캡슐화하여 쓰고 싶은것을 골라서 사용할 수 있는(전략적) 구조로 설계한다.


예제 코드 1)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
//------------------------------------------------------------------
// 정렬 인터페이스
class 정렬_인터페이스
{
public:
    virtual void 정렬() = 0;
};
 
//------------------------------------------------------------------
// 퀵 정렬 알고리즘 클래스
class 퀵정렬 : public 정렬_인터페이스
{
public:
    void 정렬() override { cout << "퀵 정렬" << endl; }
};
 
//------------------------------------------------------------------
// 버블 정렬 알고리즘 클래스
class 버블정렬 : public 정렬_인터페이스
{
public:
    void 정렬() override { cout << "버블 정렬" << endl; }
};
 
//------------------------------------------------------------------
// 머지 정렬 알고리즘 클래스
class 머지정렬 : public 정렬_인터페이스
{
public:
    void 정렬() override { cout << "머지 정렬" << endl; }
};
 
//------------------------------------------------------------------
// 정렬 관리자 클래스
class 정렬관리자
{
public:
    정렬관리자() : pInterface(0) {}
    ~정렬관리자() { if (pInterface) delete pInterface; }
 
public:
    void 정렬() { pInterface->정렬(); }
    void 인터페이스_설정(정렬_인터페이스* _interface) 
    { 
        if (pInterface) delete pInterface; 
        pInterface = _interface; 
    }
 
private:
    정렬_인터페이스* pInterface;
};
 
//------------------------------------------------------------------
// Main
int _tmain(int argc, _TCHAR* argv[])
{
    정렬관리자 *pManager = new 정렬관리자();
    pManager->인터페이스_설정(new 버블정렬());
    pManager->정렬();
 
    pManager->인터페이스_설정(new 퀵정렬());
    pManager->정렬();
 
    pManager->인터페이스_설정(new 머지정렬());
    pManager->정렬();
    delete pManager;
 
    return 0;
}

예제 결과 1)




예제 2) 농구 점수 (슛팅) 알고리즘

이번 예제도 위와 동일하며, 전략(슛팅)이 여러가지가 존재하고, 농구선수(context)가 필요할 때마다 알고리즘(슛)을 변경, 교체해서 사용할 수 있음을 보여주는 전형적인 전략 패턴이다.

예제 코드 2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
//------------------------------------------------------------------
// 슛팅 인터페이스
class 슛팅_인터페이스
{
public:
    virtual void 슛() = 0;
};
 
//------------------------------------------------------------------
// 슛팅 (2점) 알고리즘 클래스
class 슛팅_2점 : public 슛팅_인터페이스
{
public:
    void 슛() override { cout << "2점 슛" << endl; }
};
 
//------------------------------------------------------------------
// 슛팅 (3점) 알고리즘 클래스
class 슛팅_3점 : public 슛팅_인터페이스
{
public:
    void 슛() override { cout << "3점 슛" << endl; }
};
 
//------------------------------------------------------------------
// 슛팅 (덩크슛) 알고리즘 클래스
class 슛팅_덩크슛 : public 슛팅_인터페이스
{
public:
    void 슛() override { cout << "덩크 슛" << endl; }
};
 
//------------------------------------------------------------------
// 농구선수 클래스
class 농구선수
{
public:
    농구선수() : m_pInterface(0) {}
    ~농구선수() { if (m_pInterface) delete m_pInterface; }
public:
    void 슛_설정(슛팅_인터페이스* pInterface) 
    { 
        if (m_pInterface) delete m_pInterface;
        m_pInterface = pInterface; 
    }
    void 슛() { m_pInterface->슛(); }
private:
    슛팅_인터페이스* m_pInterface;
};
 
//------------------------------------------------------------------
// Main
int _tmain(int argc, _TCHAR* argv[])
{
    농구선수* pPlayer = new 농구선수();
    pPlayer->슛_설정(new 슛팅_2점());
    pPlayer->슛();
 
    pPlayer->슛_설정(new 슛팅_3점());
    pPlayer->슛();
 
    pPlayer->슛_설정(new 슛팅_덩크슛());
    pPlayer->슛();
    delete pPlayer;
 
    return 0;
}

예제 결과 2)



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 : 인라인 함수는 미주알고주알 따져서 이해해 두자.

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