728x90

프로그램을 설계, 작성하다 보면 특정 운영체제 혹은 플랫폼에 맞춰 클래스를 자동으로 생성해서 사용해야 할 경우가 있습니다. 예를 들어 프로그램이 윈도우와 리눅스가 같은 기능을 수행하지만 운영체제에 맞는 클래스를 생성하여 사용해야 할 수 있습니다.

 

간단히 사용하기 위해서는 조건문을 사용해서 객체를 생성할 수 있습니다.

class Func1 { };
class Func2 { };
class WindowsFunc1 : public Func1 { };
class LinuxFunc1 : public Func1 { };
class WindowsFunc2 : public Func2 { };
class LinuxFunc2 : public Func2 { };

void func1(QString osName)
{
    if ( osName == "Win" )  {
    	WindowsFunc1 func1;
    } else if ( osName == "Linux" ) {
    	LinuxFunc1 func1;
    }
}

void func2(QString osName)
{
    if ( osName == "Win" )  {
    	WindowsFunc2 func2;
    } else if ( osName == "Linux" ) {
    	LinuxFunc2 func2;
    }
}

int main()
{
    QString sOSName = getOSName();		// 현재 동작중인 OS를 구하는 함수가 있다고 가정
    func1(sOSName);
    func2(sOSName);
    ...;
}

근데 위와 같이 조건문을 사용하면 새로운 운영체제 환경이 추가될 때마다 조건문들을 일일이 찾아가면서 추가를 해줘야 합니다.

 

이러한 번거로움을 방지하기 위해서 객체를 생성해주는 클래스를 만들어 사용하는 것이 좋습니다. 변경이 있을 때 이 클래스 내부만 변경을 해주면서 변경에 필요한 비용을 최소화하는 것이 효율적입니다.

 

클래스는 내부적으로 조건 비교를 통해 WindowsAction, LinuxAction 등을 포인터 형태로 반환을 하는 멤버함수를 가집니다. 이를 위해서는 WindowsAction과 LinuxAction에 대한 추상 클래스인 Action이 필요하고 WindowsAction과 LinuxAction은 동일한 인터페이스를 구성해야 합니다. 만약 새로운 운영체제가 추가된다면 클래스 내부에 조건과 멤버 함수만 추가해주면 됩니다.

 

위처럼 조건문을 사용한다는 것도 이전 소스코드와 독립적이지 않다고 말할 수 있습니다. 새로운 조건, 환경이 추가될 때 이전 소스코드에 맞추어서 작성을 해야 하기 때문입니다. 더욱 보완을 위해서 상속을 이용하여 독립적이게 새로운 것을 추가할 수 있습니다.

 

상속을 사용하면 상위 클래스가 공통의 데이터를 관리할 수 있는 장점도 있지만 상위 클래스가 하위 클래스를 대표하면서 공통된 인터페이스를 제공할 수 있다는 장점이 있습니다. 새로운 환경이 추가가 되면 상위 클래스의 인터페이스와 자로형을 참조해 구성하고 실제 동작하는 부분만 다르게 만들 수 있습니다. 

 

위와 같이 제품, 환경에 따른 클래스의 객체를 생성을 전담하는 클래스를 두고 새로운 환경, 제품이 추가되기 쉽게 추상 클래스와 상속을 이용한 형태의 구조를 추상 자료형 패턴이라고 합니다.

class Func1 { virtual void doFunc1() = 0; };
class Func2 { virtual void doFunc2() = 0; };

class WindowsFunc1 : public Func1 { 
	void doFunc1() { cout << "Win func1"; }
};
class LinuxFunc1 : public Func1 { 
	void doFunc1() { cout << "Linux func1"; }
};
class WindowsFunc2 : public Func2 { 
	void doFunc2() { cout << "Win func2"; }
};
class LinuxFunc2 : public Func2 { 
	void doFunc2() { cout << "Linux func2"; }
};

class AbFactory {
	virtual Func1* CreateFunc1() = 0;
    virtual Func2* CreateFunc2() = 0;
};

class WinFactory {
	Func1* CreateFunc1(); { new WindowsFunc1; }
    Func2* CreateFunc2(); { new WindowsFunc2; }
};

class LinuxFactory {
	Func1* CreateFunc1(); { new LinuxFunc1; }
    Func2* CreateFunc2(); { new LinuxFunc2; }
};

int main()
{
    AbFactory* pFactory;
    QString sOSName = getOSName();		// 현재 동작중인 OS를 구하는 함수가 있다고 가정
    
    if(sOSName == "Windows") { pFactory = new WinFactory; }
    else if(sOSName == "Linux") { pFactory = new LinuxFactory; }
        
    pFactory->createFunc1()->func1();
    pFactory->createFunc2()->func2();
    ...;
}

위 추상 팩토리를 적용한 코드에서도 조건문을 사용하긴 하지만 처음에 팩토리를 생성할 때 사용하고 나머지는 팩토리의 다형성을 이용하여 처리하게 됩니다.

 

만약 새로운 각 운영체제에 새로운 Func가 추가될 경우 Factory에도 해당 Func 클래스를 new 해주는 함수를 추가해야 합니다. 한 두개일 경우 조건문으로 처리해도 괜찮지만 수가 많아질 경우 관리하기 어려워질 수 있습니다.

이럴 때는 Func들을 상수로 관리해서 create시 상수를 넘겨 switch문에서 create해주는 방법이 있습니다.

enum FuncType {
	enFunc1 = 1,
    enFunc2 = 2,
    enFunc3 = 3,
    enFunc4 = 4
};

class AbFactory {
	virtual Func1* CreateFunc(FuncType enType) = 0;
};

class WinFactory {
	Func* CreateFunc(FuncType enType) { 
    	switch(enType) {
        case enFunc1: return new WindowsFunc1;
        case enFunc2: return new WindowsFunc2;
        case enFunc3: return new WindowsFunc3;
        case enFunc4: return new WindowsFunc4;
    }
};

class LinuxFactory {
	Func* CreateFunc(FuncType enType) { 
    	switch(enType) {
        case enFunc1: return new LinuxsFunc1;
        case enFunc2: return new LinuxFunc2;
        case enFunc3: return new LinuxFunc3;
        case enFunc4: return new LinuxFunc4;
    }
};

int main()
{
    AbFactory* pFactory;
    QString sOSName = getOSName();		// 현재 동작중인 OS를 구하는 함수가 있다고 가정
    
    if(sOSName == "Windows") { pFactory = new WinFactory; }
    else if(sOSName == "Linux") { pFactory = new LinuxFactory; }
        
    pFactory->createFunc(enFunc1)->func();
    ...;
}

생성된 팩토리 객체를 관리할 때 하나만 생성해서 유지할 것이냐, 아니면 생성하는데 원본 객체를 복제하는 방식으로 생성하는 방법이 있습니다.

 

하나만 유지하기 위해서는 추후 다룰 패턴인 싱글톤 패턴을 적용하면 됩니다.

enum FuncType {
	enFunc1 = 1,
    enFunc2 = 2,
    enFunc3 = 3,
    enFunc4 = 4
};

class AbFactory {
	static AbFactory* pInstance;		// 최상위 객체가 static instance를 가짐
	virtual Func1* CreateFunc(FuncType enType) = 0;
};

class WinFactory {
	Func* CreateFunc(FuncType enType) { 
    	switch(enType) {
        case enFunc1: return new WindowsFunc1;
        case enFunc2: return new WindowsFunc2;
        case enFunc3: return new WindowsFunc3;
        case enFunc4: return new WindowsFunc4;
    }
    static WinFactory* createInstance() {
    	if ( pInstance == 0 ) {	pInstance = new WinFactory; }
        return (WinFactory*) pInstance;
    } // 각 하위 클래스에서 객체를 생성하는 createInstance를 가짐
};

class LinuxFactory {
	Func* CreateFunc(FuncType enType) { 
    	switch(enType) {
        case enFunc1: return new LinuxsFunc1;
        case enFunc2: return new LinuxFunc2;
        case enFunc3: return new LinuxFunc3;
        case enFunc4: return new LinuxFunc4;
    }
        static LinuxFactory* createInstance() {
    	if ( pInstance == 0 ) {	pInstance = new LinuxFactory; }
        return (LinuxFactory*) pInstance;
    } // 각 하위 클래스에서 객체를 생성하는 createInstance를 가짐
};

int main()
{
    AbFactory* pFactory;
    QString sOSName = getOSName();		// 현재 동작중인 OS를 구하는 함수가 있다고 가정
    
    if(sOSName == "Windows") { pFactory = WinFactory::createInstance(); }
    else if(sOSName == "Linux") { pFactory = LinuxFactory::createInstance(); }
        
    pFactory->createFunc(enFunc1)->func();
    ...;
}

만약 새로운 운영체제가 추가된다면 그때마다 새로운 Factory를 생성해주고 관리해줘야 합니다. 이런 불편을 없애기 위해서는 하위 Factory를 생성하지 않고 미리 생성해야 할 객체들을 상위 Factory에 생성 후 객체 생성 요청 시 이를 복제하는 방법이 있습니다. 이렇게 생성할 객체를 미리 등록 후 복제해서 새로운 객체를 생성해주는 방식을 Prototype 패턴이라 합니다.

class Func1 { virtual Func1* Clone() = 0; };
class Func2 { virtual Func1* Clone() = 0; };

class WindowsFunc1 : public Func1 { 
	Func1* Clone() { return new WindowsFunc1(*this); }
};
class LinuxFunc1 : public Func1 { 
	Func1* Clone() { return new LinuxFunc1(*this); }
};
class WindowsFunc2 : public Func2 { 
	Func2* Clone() { return new WindowsFunc2(*this); }
};
class LinuxFunc2 : public Func2 { 
	Func2* Clone() { return new LinuxFunc2(*this); }
};

class AbFactory {
public:
	AbFactory(Func1* _func1, Func2* _func2) : pFunc1(_func1), pFunc2(_func2) {}
	Func1* CreateFunc1() { return pFunc1->Clone(); }
    Func2* CreateFunc2() { return pFunc2->Clone(); }
private:
	Func1* pFunc1;
    Func2* pFunc2;
};

int main()
{
    AbFactory* pFactory;
    QString sOSName = getOSName();		// 현재 동작중인 OS를 구하는 함수가 있다고 가정
    
    if(sOSName == "Windows") { 
    	WindowsFunc1 func1;
        WindowsFunc2 func2;
    	pFactory = new abFactory(func1, func2); 
    }
    else if(sOSName == "Linux") { 
    	LinuxFunc1 func1;
        LinuxFunc2 func2;
    	pFactory = new abFactory(func1, func2); 
    }
        
    pFactory->CreateFunc1()->func();
}

위와 같이 구성할 경우 각 운영체체에 따른 Factory는 구성하지 않지만 상위 Factory에서 모든 것을 관리하고 생성 시 객체들을 받아 관리하기 때문에 복잡해질 수 있습니다.

지금까지 본것처럼 상속의 개념을 이용한 추상 팩토리를 이용하면 사용자가 여러 제품군, 환경일 때에 쉽게 객체를 생성해서 사용할 수 있는 방법입니다. 사용자는 추상 팩토리 클래스와 추상 클래스들의 인터페이스만을 이용하면 되기 때문에 사용이 용이하고 변경이 일어나는 부분이 최소화되어 관리도 편리합니다. 

 

하지만 단점으로 새로운 환경이 추가될 경우 모든 Factory 클래스들을 수정해야 한다는 점이 있습니다.

728x90

'Programming > C++' 카테고리의 다른 글

C++ 팩토리 메소드 패턴  (0) 2021.05.08
C++ 빌더 패턴  (0) 2021.05.02
C++ 상속(Inheritance)  (0) 2020.05.08
C++ cout 자리수 지정하기  (0) 2020.02.28
C++ 큰따옴표(") 출력하기  (0) 2020.02.27

+ Recent posts