추상 팩토리 패턴 (Abstract Factory Pattern) |
1) 어려운 설명
추상 팩토리 패턴은 연관성이 있는 객체 군이 여러개 있을 경우 이들을 집합으로 묶어서 추상화하고
구체적인 상황이 주어지면 팩토리 객체에서 집합으로 묶은 객체군을 구현화하는 생성 패턴
2) 쉬운 설명
예를들어 Button, CheckBox, TextEdit 세가지의 Type이 있는데 또 여기에 Window, Mac OS가 있다면
WinButton, WinCheckBox, WinTextEdit / MacButton, MacCheckBox, MacTextEdit처럼 여러 갈래로 나뉘게 되므로 복잡해지는데, 이러한 제품군들을 관리와 확장하기 용이하게 만든것이 추상팩토리 패턴
자세한 이해는 그냥 코드를 보는게 더 빠르지만 설명해보자면
Button은 Button끼리 묶어서 구현하고, CheckBox는 CheckBox끼리 묶어서 구현한다음 클라이언트가 객체를 생성하는 시점에 팩토리를 지정하면 해당 팩토리에서 해당 타입을 만들어 내는 것
사용하는 이유
1) 관련 제품의 다양한 제품군과 함께 작동해야 할때 해당 제품의 구체적인 클래스에 의존하고 싶지 않은 경우
- 예를 들면 Window Button / Window CheckBox / Mac Button / Mac CheckBox 4개의 클래스가 필요하다면 각 클래스에 구체적으로 구현하지 않고 Button에 대해 공통적인 내용은 Button클래스에 구체적으로 구현하고 winButton과 macButton은 해당 내용을 상속받게 함
2) 여러 제품군 중 하나를 선택해서 시스템을 설정해야 하고 한번 구성한 제품을 다른것으로 대체할 수도 있을때
3) 제품에 대한 자세한 구현이 아닌 인터페이스만 노출하고 싶을때
- 클라이언트는 팩토리 클래스만을 참조하여 객체를 생성하므로 구현부를 감출 수 있음
팩토리 메서드 vs 추상 팩토리 |
추상 팩토리 패턴을 배우기 전에 추상 팩토리 패턴과 팩토리 메서드 패턴을 구분해야 한다는 사실을 인지해야 한다.
둘 다 객체를 생성하는 팩토리 패턴이지만 엄연히 전혀 다른 역할을 한다.
둘을 동시에 사용할수도 있고, 목적 또한 다르며 상하관계가 아닌 개별의 패턴이므로 상황에 따라 적절히 선택해야 한다.
팩토리 메서드 패턴 | 추상 팩토리 패턴 | |
공통점 | 팩토리 객체를 통해 객체 생성 과정을 추상화한 인터페이스만 제공하여 객체 생성을 캡슐화하여 구체적인 타입을 감추고 공장 클래스와 제품 클래스를 나눠 느슨한 결합구조를 표방 |
|
차이점 | 객체 생성 전/후에 해야할 일의 공통점에 초점 | 생성해야할 객체 집합군의 공통점에 초점 |
구체적인 객체 생성 과정을 하위 또는 구체적인 클래스로 옮기는것이 목적 | 관련있는 여러 객체를 구체적인 클래스에 의존하지 않고 만들 수 있게 해주는 것이 목적 | |
한 Factory당 한 종류의 객체 생성 지원 | 한 Factory에서 서로 연관된 여러 종류의 객체 생성 지원 | |
메소드 레벨에 포커스 | 클래스 레벨에 포커스 |
추상 팩토리 패턴 구조 |
1) AbstractFactory : 최상위 공장 클래스, 여러개의 제품들을 생성하는 여러 메소드를 추상화한다.
2) ConcreteFactory : 서브 공장 클래스, 타입에 맞는 제품 객체를 반환하도록 메소드를 구체화한다.
3) AbstractProduct : 각 타입의 제품들을 추상화한 인터페이스
4) ConcreteProduct(ProductA ~ ProductB) : 각 타입의 제품 구현체들, 팩토리 객체로부터 생성됨
5) Client : 클라이언트는 추상화된 인터페이스만 이용하여 제품을 받기 때문에 구체적인 공장/제품에 대해 모른다.
구현 코드 예시 |
1) 디자인 패턴 없이 만든다면
Button, CheckBox, TextEdit의 3가지의 Type과 Windows, Mac의 2가지 OS가 있다고 할때
Button클래스의 하위 클래스로 WindowButton클래스와 MacButton클래스를 만들고
CheckBox클래스의 하위 클래스로 WindowCheckBox클래스와 MacCheckBox클래스를 만들고
TextEdit클래스의 하위 클래스로 WindowTextEdit클래스와 MacTextEdit클래스를 만드는 식으로 구현 할 수 있다.
이를 코드로 작성한다면 아래와 같다.
#include <iostream>
class Component {
virtual void render() = 0; // 요소 그리기
};
/* ---------------------------------------------------------- */
class Button : public Component { };
class WindowButton : public Button {
public:
void render() override { std::cout << "윈도우 버튼 생성 완료"; }
};
class MacButton : public Button {
public:
void render() override { std::cout << "맥 버튼 생성 완료"; }
};
/* ---------------------------------------------------------- */
class CheckBox : public Component { };
class WindowCheckBox : public CheckBox {
public:
void render() override { std::cout << "윈도우 체크박스 생성 완료"; }
};
class MacCheckBox : public CheckBox {
public:
void render() override { std::cout << "맥 체크박스 생성 완료"; }
};
/* ---------------------------------------------------------- */
class TextEdit : public Component { };
class WindowTextEdit : public TextEdit {
public:
void render() override { std::cout << "윈도우 텍스트박스 생성 완료"; }
};
class MacTextEdit : public TextEdit {
public:
void render() override { std::cout << "맥 텍스트박스 생성 완료"; }
};
깔끔하고 문제될건 없으며 추후에 Linux를 지원하게 된다고 해도 Linux 클래스를 새로 만들면 될 뿐 기존 코드들을 수정할 필요는 없으므로 OCP원칙도 준수될것이다.
다만 문제는 Type이나 OS가 늘어날때마다 클래스의 갯수가 무지막지하게 늘어나게 된다는점이 있겠다.
또한 객체군에 공통점이 있다면 같은 코드를 모든 클래스에 중복해서 작성하게 되는 비효율이 생길 수 있다.
2) 팩토리 메서드 패턴으로 구현한다면
팩토리 메서드 패턴의 공장 객체는 한가지 종류의 컴포넌트만 생성하며 팩토리 메서드의 초점은 추상화된 팩토리메서드를 각 서브 공장 클래스가 구체화하여 생성하는 것이므로 버튼을 생성한다면 구체화하는 파트에서 어느 OS인지 조건문으로 구분해줘야 한다.
이를 코드로 작성한다면 아래와 같다.
#include <iostream>
using namespace std;
class ComponentFactoryMethod {
virtual Component createOperation(string type) = 0; // 템플릿
virtual Component createComponent(string type) = 0; // 팩토리 메서드
};
/* ---------------------------------------------------------- */
class ButtonFactory : public ComponentFactoryMethod {
public:
Button createOperation(string type) {
Button button = createComponent(type);
button.추가설정();
return button;
}
Button createComponent(string type) {
Button button = null;
switch (type.toLowerCase()) {
case "window":
button = new WindowButton();
break;
case "mac":
button = new MacButton();
break;
}
return button;
}
};
/* ---------------------------------------------------------- */
class CheckBoxFactory : public ComponentFactoryMethod {
public:
CheckBox createOperation(string type) {
CheckBox checkbox = createComponent(type);
checkbox.추가설정();
return checkbox;
}
CheckBox createComponent(string type) {
CheckBox checkbox = null;
switch (type.toLowerCase()) {
case "window":
checkbox = new WindowCheckBox();
break;
case "mac":
checkbox = new MacCheckBox();
break;
}
return checkbox;
}
};
/* ---------------------------------------------------------- */
class TextEditFactory : public ComponentFactoryMethod {
public:
TextEdit createOperation(string type) {
TextEdit txtedit = createComponent(type);
txtedit.추가설정();
return txtedit;
}
TextEdit createComponent(string type) {
TextEdit txtedit = null;
switch (type.toLowerCase()) {
case "window":
txtedit = new WindowTextEdit();
break;
case "mac":
txtedit = new MacTextEdit();
break;
}
return txtedit;
}
};
/* ---------------------------------------------------------- */
void main() {
ComponentFactoryMethod factory = null;
Button btn = null;
CheckBox chkBox = null;
// 윈도우 버튼 생성
factory = new ButtonFactory();
btn = (Button)factory.createOperation("Window");
btn.render();
// 맥 버튼 생성
btn = (Button)factory.createOperation("Mac");
btn.render();
// 윈도우 체크 박스 생성
factory = new CheckBoxFactory();
chkBox = (CheckBox)factory.createOperation("Window");
chkBox.render();
// 맥 체크 박스 생성
chkBox = (CheckBox)factory.createOperation("Mac");
chkBox.render();
}
이 상황에서 Linux를 추가해야한다면 모든 메서드에 switch case에 case "linux"를 추가해줘야 하므로 코드를 일일히 수정해줘야하고 OCP원칙을 위배하게 된다.
3) 추상 팩토리 패턴으로 구현한다면
팩토리 메서드의 공장객체는 한 종류의 컴포넌트만 생성하지만
추상 팩토리의 공장객체는 하나의 객체에서 여러 종류의 컴포넌트들을 골라 생산할 수 있다.
class Button {};
class WindowButton : public Button {};
class MacButton : public Button {};
class CheckBox {};
class WindowCheckBox : public CheckBox {};
class MacCheckBox : public CheckBox {};
class TextEdit {};
class WindowTextEdit : public TextEdit {};
class MacTextEdit : public TextEdit {};
/* ---------------------------------------------------------- */
class ComponentAbstractFactory {
virtual Button createButton();
virtual CheckBox createCheckBox();
virtual TextEdit createTextEdit();
};
/* ---------------------------------------------------------- */
class WindowFactory : public ComponentAbstractFactory {
public:
Button createButton() override { return new WindowButton(); }
CheckBox createCheckBox() override { return new WindowCheckBox(); }
TextEdit createTextEdit() override { return new WindowTextEdit(); }
};
/* ---------------------------------------------------------- */
class MacFactory : public ComponentAbstractFactory {
public:
Button createButton() override { return new MacButton(); }
CheckBox createCheckBox() override { return new MacCheckBox(); }
TextEdit createTextEdit() override { return new MacTextEdit(); }
};
void main() {
ComponentAbstractFactory factory;
// 윈도우 버튼 생성
factory = new WindowFactory(); // Factory는 싱글톤으로 구현하는게 좋다.
Button WindowBtn = factory.createButton();
WindowBtn.render();
// 맥 버튼 생성
factory = new MacFactory(); // Factory는 싱글톤으로 구현하는게 좋다.
Button MacBtn = factory.createButton();
MacBtn.render();
}
이 상황에서는 Linux를 추가해야 한다면 LinuxFactory를 만들고 Linux객체들과 create함수들만 만들어주면 되므로 OCP원칙을 준수한다.
하지만 운영체제가 아닌 Type인 Rectangle을 추가해야한다면 모든 클래스마다 createRectangle()함수를 추가해줘야 하므로 이는 OCP원칙을 위배한다.
그러므로 제품군을 묶어 생성해야 할때는 추상팩토리가 유리하긴 하지만
항상 추상 팩토리가 팩토리 메서드보다 좋은것은 아니다.
또한 팩토리메서드와 추상팩토리는 양자택일이 아닌 둘 다 동시에 적용 가능하다는 점도 잊어서는 안된다.
장단점 |
1) 장점
- 객체를 생성하는 코드를 분리하므로 결합도를 낮출 수 있다
- 제품 군을 쉽게 대체 할 수 있다
- 단일 책임 원칙 준수
- 개방 / 폐쇄 원칙 준수
2) 단점
- 팩토리 메서드 패턴과 마찬가지로(팩토리 패턴 공통의 문제점) 각 구현체마다 팩토리 객체를 구현해주어야 하므로 객체가 많아지면 클래스도 많아져서 코드가 복잡해진다
- 새로운 종류의 제품을 지원하기 어렵다. 새로운 제품을 추가하려면 팩토리 구현 로직 자체를 변경해야 한다.
- 기존 추상 팩토리의 세부사항이 변경되면 모든 팩토리에 대해 수정이 필요해진다.(추상팩토리와 서브클래스 모두 수정해야함)
※ 참고문헌