...
Abstract Factory Pattern
추상 팩토리 패턴은 연관성이 있는 객체 군이 여러개 있을 경우 이들을 묶어 추상화하고, 어떤 구체적인 상황이 주어지면 팩토리 객체에서 집합으로 묶은 객체 군을 구현화 하는 생성 패턴이다. 클라이언트에서 특정 객체을 사용할때 팩토리 클래스만을 참조하여 특정 객체에 대한 구현부를 감추어 역할과 구현을 분리시킬 수 있다.
즉, 추상 팩토리의 핵심은 제품 '군' 집합을 타입 별로 찍어낼수 있다는 점이 포인트 이다. 예를들어 모니터, 마우스, 키보드를 묶은 전자 제품군이 있는데 이들을 또 삼성 제품군이냐 애플 제품군이냐 로지텍 제품군이냐에 따라 집합이 브랜드 명으로 여러갈래로 나뉘게 될때, 복잡하게 묶이는 이러한 제품군들을 관리와 확장하기 용이하게 패턴화 한것이 추상 팩토리 이다.
추상 팩토리 패턴 구조
- AbstractFactory : 최상위 공장 클래스. 여러개의 제품들을 생성하는 여러 메소드들을 추상화 한다.
- ConcreteFactory : 서브 공장 클래스들은 타입에 맞는 제품 객체를 반환하도록 메소드들을 재정의한다.
- AbstractProduct : 각 타입의 제품들을 추상화한 인터페이스
- ConcreteProduct (ProductA ~ ProductB) : 각 타입의 제품 구현체들. 이들은 팩토리 객체로부터 생성된다.
- Client : Client는 추상화된 인터페이스만을 이용하여 제품을 받기 때문에, 구체적인 제품, 공장에 대해서는 모른다.
Abstract Factory vs Factory Method
둘다 팩토리 객체를 통해 구체적인 타입을 감추고 객체 생성에 관여하는 패턴 임에는 동일하다. 또한 공장 클래스가 제품 클래스를 각각 나뉘어 느슨한 결합 구조를 구성하는 모습 역시 둘이 유사하다.
그러나 주의할 것은 추상 팩토리 패턴이 팩토리 메서드 패턴의 상위 호환이 아니라는 점이다. 두 패턴의 차이는 명확하기 때문에 상황에 따라 적절한 선택을 해야 한다.
예를들어 팩토리 메서드 패턴은 객체 생성 이후 해야 할 일의 공통점을 정의하는데 초점을 맞추는 반면, 추상 팩토리 패턴은 생성해야 할 객체 집합 군의 공통점에 초점을 맞춘다.
단, 이 둘을 유사점과 차이점을 조합해서 복합 패턴을 구성하는 것도 가능하다.
팩토리 메서드 패턴 | 추상 팩토리 패턴 | |
공통점 | 객체 생성 과정을 추상화한 인터페이스를 제공 객체 생성을 캡슐화함으로써 구체적인 타입을 감추고 느슨한 결합 구조를 표방 |
|
차이점 | 구체적인 객체 생성과정을 하위 또는 구체적인 클래스로 옮기는 것이 목적 | 관련 있는 여러 객체를 구체적인 클래스에 의존하지 않고 만들 수 있게 해주는 것이 목적 |
한 Factory당 한 종류의 객체 생성 지원 | 한 Factory에서 서로 연관된 여러 종류의 객체 생성을 지원. (제품군 생성 지원) | |
메소드 레벨에서 포커스를 맞춤으로써, 클라이언트의 ConcreteProduct 인스턴스의 생성 및 구성에 대한 의존을 감소 | 클래스(Factory) 레벨에서 포커스를 맞춤으로써, 클라이언트의 ConcreteProduct 인스턴스 군의 생성 및 구성에 대한 의존을 감소 |
추상 팩토리 패턴 흐름
클래스 다이어그램 그림을 보면 구성요소와 화살표가 얼키고 섥혀 복잡하게 보일테지만, 실상 파고보면 별거 아닌 구조이니 겁 먹을 필요가 없다.
클래스 구성
제품(Product) 클래스
// Product A 제품군
interface AbstractProductA {
}
// Product A - 1
class ConcreteProductA1 implements AbstractProductA {
}
// Product A - 2
class ConcreteProductA2 implements AbstractProductA {
}
// Product B 제품군
interface AbstractProductB {
}
// Product B - 1
class ConcreteProductB1 implements AbstractProductB {
}
// Product B - 2
class ConcreteProductB2 implements AbstractProductB {
}
공장(Factory) 클래스
interface AbstractFactory {
AbstractProductA createProductA();
AbstractProductB createProductB();
}
// Product A1와 B1 제품군을 생산하는 공장군 1
class ConcreteFactory1 implements AbstractFactory {
public AbstractProductA createProductA() {
return new ConcreteProductA1();
}
public AbstractProductB createProductB() {
return new ConcreteProductB1();
}
}
// Product A2와 B2 제품군을 생산하는 공장군 2
class ConcreteFactory2 implements AbstractFactory {
public AbstractProductA createProductA() {
return new ConcreteProductA2();
}
public AbstractProductB createProductB() {
return new ConcreteProductB2();
}
}
클래스 흐름
class Client {
public static void main(String[] args) {
AbstractFactory factory = null;
// 1. 공장군 1을 가동시킨다.
factory = new ConcreteFactory1();
// 2. 공장군 1을 통해 제품군 A1를 생성하도록 한다 (클라이언트는 구체적인 구현은 모르고 인터페이스에 의존한다)
AbstractProductA product_A1 = factory.createProductA();
System.out.println(product_A1.getClass().getName()); // ConcreteProductA1
// 3. 공장군 2를 가동시킨다.
factory = new ConcreteFactory2();
// 4. 공장군 2를 통해 제품군 A2를 생성하도록 한다 (클라이언트는 구체적인 구현은 모르고 인터페이스에 의존한다)
AbstractProductA product_A2 = factory.createProductA();
System.out.println(product_A2.getClass().getName()); // ConcreteProductA2
}
}
코드를 보면 똑같은 createProductA() 메서드를 호출하지만 어떤 팩토리 객체이냐에 따라 반환되는 제품군이 다르게 된다.
추상 팩토리 패턴 특징
패턴 사용 시기
- 관련 제품의 다양한 제품 군과 함께 작동해야 할때, 해당 제품의 구체적인 클래스에 의존하고 싶지 않은 경우
- 여러 제품군 중 하나를 선택해서 시스템을 설정해야하고 한 번 구성한 제품을 다른 것으로 대체할 수도 있을 때
- 제품에 대한 클래스 라이브러리를 제공하고, 그들의 구현이 아닌 인터페이스를 노출시키고 싶을 때
패턴 장점
- 객체를 생성하는 코드를 분리하여 클라이언트 코드와 결합도를 낮출 수 있다.
- 제품 군을 쉽게 대체 할 수 있다.
- 단일 책임 원칙 준수
- 개방 / 폐쇄 원칙 준수
패턴 단점
- 각 구현체마다 팩토리 객체들을 모두 구현해주어야 하기 때문에 객체가 늘어날때 마다 클래스가 증가하여 코드의 복잡성이 증가한다. (팩토리 패턴의 공통적인 문제점)
- 기존 추상 팩토리의 세부사항이 변경되면 모든 팩토리에 대한 수정이 필요해진다. 이는 추상 팩토리와 모든 서브클래스의 수정을 가져온다.
- 새로운 종류의 제품을 지원하는 것이 어렵다. 새로운 제품이 추가되면 팩토리 구현 로직 자체를 변경해야한다.
예제를 통해 알아보는 추상 팩토리 패턴
많은 사람들이 추상 팩토리와 팩토리 메서드 패턴 차이에 대해 햇깔려 한다. 왜냐하면 둘이 팩토리 객체가 대신 제품 객체를 생성한다는 점에서 구조가 거의 동일하기 때문이다. 그래서 어떤 디자인 패턴 서적에서는 추상 팩토리와 팩토리 메서드를 하나의 팩토리 패턴 카테고리로서 다루기도 한다.
하지만 이 둘은 완전히 별개의 패턴이다. 따라서 이번 섹션에서는 독자들의 완벽한 이해를 돕기 위해 예제를 통해 팩토리 메서드와 추상 팩토리를 각각 구현해보고 이 둘을 짬뽕해보는 시간을 가져볼 예정이다.
컴퓨터 화면에 컴포넌트 요소들을 화면에 그리는 로직을 구현한다고 한다. 버튼(Button), CheckBox(체크박스), TextEdit(텍스트박스) 3가지 요소를 화면에 그리기 위해 각기 객체로 지정한다. 그런데 컴퓨터 OS에는 윈도우(Window)와 맥(Mac) 종류가 나뉘는데 어떤 OS냐에 따라서 모양이 달라질 수 있기 때문에 각기 생성하여 구현하여야 한다고 한다.
정리하면 이 3가지 객체들은 하나의 컴포넌트(Component) 군으로 묶을수 있으며 또한 OS별 군으로 나뉘게 된다.
위의 컴포넌트들을 코드로 표현하면 다음과 같다.
interface Component {
void render(); // 요소 그리기
}
/* ---------------------------------------------------------- */
abstract class Button implements Component {
}
class WindowButton extends Button {
@Override
public void render() {
System.out.println("윈도우 버튼 생성 완료");
}
}
class MacButton extends Button {
@Override
public void render() {
System.out.println("맥 버튼 생성 완료");
}
}
/* ---------------------------------------------------------- */
abstract class CheckBox implements Component {
}
class WindowCheckBox extends CheckBox {
@Override
public void render() {
System.out.println("윈도우 체크박스 생성 완료");
}
}
class MacCheckBox extends CheckBox {
@Override
public void render() {
System.out.println("맥 체크박스 생성 완료");
}
}
/* ---------------------------------------------------------- */
abstract class TextEdit implements Component {
}
class WindowTextEdit extends TextEdit {
@Override
public void render() {
System.out.println("윈도우 텍스트박스 생성 완료");
}
}
class MacTextEdit extends TextEdit {
@Override
public void render() {
System.out.println("맥 텍스트박스 생성 완료");
}
}
팩토리 메서드 패턴으로 구현
위의 구성을 팩토리 메서드 패턴으로 구현해보자. 팩토리 메서드 패턴의 공장 객체는 한가지 종류의 컴포넌트만 생성하는 구조이다. 팩토리 메서드의 초점은 추상화된 팩토리 메서드를 각 서브 공장 클래스가 재정의하여 걸맞는 제품 객체를 생성하는 것이기 때문이다. 그렇기 때문에 버튼을 생성한다고 하더라도 어느 OS 실행환경인지는 메서드 내에서 분기문을 통해 구분해 주어야 한다.
interface ComponentFactoryMethod {
Component createOperation(String type); // 템플릿
Component createComponent(String type); // 팩토리 메서드
}
class ButtonFactory implements ComponentFactoryMethod {
public Button createOperation(String type) {
Button button = createComponent(type);
button.추가설정();
return button;
}
public 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 implements ComponentFactoryMethod {
public CheckBox createOperation(String type) {
CheckBox checkbox = createComponent(type);
checkbox.추가설정();
return checkbox;
}
public 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 implements ComponentFactoryMethod {
public TextEdit createOperation(String type) {
TextEdit txtedit = createComponent(type);
txtedit.추가설정();
return txtedit;
}
public TextEdit createComponent(String type) {
TextEdit txtedit = null;
switch (type.toLowerCase()) {
case "window":
txtedit = new WindowTextEdit();
break;
case "mac":
txtedit = new MacTextEdit();
break;
}
return txtedit;
}
}
public static void main(String[] args) {
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();
}
팩토리 메서드의 문제점
팩토리 메서로 구현해본 코드는 실행 자체는 문제가 없어 보이지만, 만일 기능을 확장할 필요가 있을 때 문제가 생기게 된다. 예를 들어 OS 종류에 Linux를 새로 추가한다고 생각해보자. 그러면 각 메서드마다 있는 분기문 로직을 일일히 수정하여야 하는데, 그러면 OCP 원칙에 위배되는 꼴이 된다.
추상 팩토리 패턴으로 구현
그럼 추상 팩토리 패턴으로 구현하면 어떨까?
팩토리 메서드의 공장 객체는 한 종류의 컴포넌트만 생성하지만, 추상 팩토리의 공장 객체는 하나의 객체에서 여러 종류의 컴포넌트들을 골라 생산할 수 있도록 구성한다.
interface ComponentAbstractFactory {
Button createButton();
CheckBox createCheckBox();
TextEdit createTextEdit();
}
class WindowFactory implements ComponentAbstractFactory {
@Override
public Button createButton() {
return new WindowButton();
}
@Override
public CheckBox createCheckBox() {
return new WindowCheckBox();
}
@Override
public TextEdit createTextEdit() {
return new WindowTextEdit();
}
}
class MacFactory implements ComponentAbstractFactory {
@Override
public Button createButton() {
return new MacButton();
}
@Override
public CheckBox createCheckBox() {
return new MacCheckBox();
}
@Override
public TextEdit createTextEdit() {
return new MacTextEdit();
}
}
public static void main(String[] args) {
ComponentAbstractFactory factory = null;
// 윈도우 버튼 생성
factory = new WindowFactory();
Button WindowBtn = createBtn(factory);
WindowBtn.render();
// 맥 버튼 생성
factory = new MacFactory();
Button MacBtn = createBtn(factory);
MacBtn.render();
}
// 추상 팩토리에서 객체를 생성하는 부분 코드는 같기 때문에 따로 메서드로 묶음 분리
public static Button createBtn(ComponentAbstractFactory fac) {
return fac.createButton();
}
기존 팩토리 메서드에서는 다른 OS의 컴포넌트를 생성하기 위해선 문자열을 인자로 주어 메서드 내에서 분기문으로 객체 생성을 처리하였지만, 추상 팩토리에선 어떠한 팩토리 객체를 생성하느냐에 따라 똑같은 메서드를 호출해도 반환되는 결과가 다르게 된다.
하지만 착각하지 말아야 할 것이 추상 팩토리가 팩토리 메서드보다 무조건 좋다는 말이 아니다. 이 예제 처럼 어떠한 제품들에 대한 '군'을 묶어 생성해야 할때 추상 팩토리로 구성하는 것이 유지보수와 확장에 있어 더 유리하다는 것을 보여주는 것이다.
추상 팩토리의 유연한 확장
예를들어 Linux OS 환경이 추가된다 하더라도, 기존의 코드 수정 없이 리눅스 컴포넌트 구현체 클래스와 리눅스 팩토리 클래스만 적절하게 추가만 해주면 확장이 완료되게 된다.
기존 팩토리 메서드로 설계 했을때는 메서드의 분기문을 일일히 뜯어 고치는 것에 비하면 확실히 OCP 원칙의 수정에는 닫혀있고 확장에는 열려있다는 말이 무슨 의미인지 체감이 온다.
추상 팩토리의 문제점
그러나 모든 확장에 대해 유연하게 대처할수 있는 것은 아니다. 이번에는 새로운 OS 환경이 아닌 새로운 컴포넌트인 툴팁 을 추가한다고 생각해보자. 그러면 모든 서브 팩토리 클래스마다 툴팁 객체를 생성하는 createToolTip() 메서드를 추가 해야 되니 이때는 오히려 문제점으로 작용하게 된다.
추상 팩토리 객체 싱글톤화
기본적으로 팩토리 클래스는 호출되면 객체를 생성하기만 하면 되기 때문에 메모리 최적화를 위해 각 팩토리 클래스마다 싱글톤 적용 하는 것이 옳다.
물론 위와 같이 메서드 호출용으로 일회용으로 쓰인 인스턴스는 JVM의 가비지 컬렉션(GC) 에 의해 자동으로 지워지지만, 이런 가비지 값이 늘어나게되면 나중에 객체 제거 과정에서 Stop-the-world 가 일어나게 된다. (프로그램이 렉걸림)
따라서 각 팩토리 클래스들을 싱글톤(Singleton) 화 시켜 메모리 적으로 최적화를 시킨다.
class WindowFactory implements ComponentAbstractFactory {
// 싱글톤 객체화
private WindowFactory() {
}
private static class SingleInstanceHolder {
private static final WindowFactory INSTANCE = new WindowFactory();
}
public static WindowFactory getInstance() {
return SingleInstanceHolder.INSTANCE;
}
@Override
public Button createButton() {
return new WindowButton();
}
@Override
public CheckBox createCheckBox() {
return new WindowCheckBox();
}
@Override
public TextEdit createTextEdit() {
return new WindowTextEdit();
}
}
class MacFactory implements ComponentAbstractFactory {
// 싱글톤 객체화
private MacFactory() {
}
private static class SingleInstanceHolder {
private static final MacFactory INSTANCE = new MacFactory();
}
public static MacFactory getInstance() {
return MacFactory.SingleInstanceHolder.INSTANCE;
}
@Override
public Button createButton() {
return new MacButton();
}
@Override
public CheckBox createCheckBox() {
return new MacCheckBox();
}
@Override
public TextEdit createTextEdit() {
return new MacTextEdit();
}
}
추상 팩토리 + 팩토리 메서드 패턴 조합
사람들이 많이들 착각하는 부분이 추상 팩토리와 팩토리 메서드는 2지 선다형으로 알고 있다는 점이다. 이 둘은 엄연히 별개의 코드 패턴이다.
팩토리 메서드는 추상 메서드를 통한 다른 제품 구현과 더불어 객체 생성에 관한 전처리, 후처리를 해주는 로직이 핵심이며, 추상 팩토리는 여러 타입의 객체 군을 생성할 수 있는 것이 핵심이다. 따라서 둘을 적절히 조합하여 사용하여도 무관하다. 아니 필요하면 둘을 조합하여 사용하여야 한다. 추상 팩토리와 팩토리 메서드 패턴 둘을 조합하게 된다면, 여러 타입의 객체 군을 생성하면서 동시에 템플릿 메서드를 통해 전처리, 후처리 작업을 해주는 것이 가능해진다.
아래의 예제 코드는 OS 군 별로 추상 팩토리를 구성하며, 각 객체 생성 메서드에 대해서 팩토리 메서드로 구성한 예제이다. 팩토리 메서드의 템플릿은 한꺼번에 컴포넌트들을 생성하고 추가 세팅한다는 컨셉으로써 하나의 리스트로 묶어서 반환한다.
만일 버튼 따로, 체크박스 따로 하고 싶다면 별도의 createButtonOperation() 이나 createCheckBoxOperation() 메서드로 각기 구현해주면 된다.
interface ComponentAbstractFactoryMethod {
// 서브 클래스에서 구현할 팩토리 메서드들
Button createButton();
CheckBox createCheckBox();
TextEdit createTextEdit();
// 팩토리 템플릿
default List<Component> createOperation() {
Button btn = createButton();
CheckBox chkbox = createCheckBox();
TextEdit txtEdt = createTextEdit();
btn.추가세팅();
chkbox.추가세팅();
txtEdt.추가세팅();
return new ArrayList<Component>(Arrays.asList(btn, chkbox, txtEdt));
}
}
// 추상 팩토리
class WindowFactoryMethod implements ComponentAbstractFactoryMethod {
@Override
public Button createButton() {
return new WindowButton();
}
@Override
public CheckBox createCheckBox() {
return new WindowCheckBox();
}
@Override
public TextEdit createTextEdit() {
return new WindowTextEdit();
}
}
// 추상 팩토리
class MacFactoryMethod implements ComponentAbstractFactoryMethod {
// ...
}
public static void main(String[] args) {
ComponentAbstractFactoryMethod factory = null;
// 윈도우 컴포넌트 생성
factory = new WindowFactoryMethod();
List<Component> list = factory.createOperation();
System.out.println(list);
for(Component c: list) {
c.render();
}
}
실무에서 찾아보는 추상 팩토리 패턴
Java
- javax.xml.parsers.DocumentBuilderFactory 의 newInstance()
- javax.xml.transform.TransformerFactory 의 newInstance()
- javax.xml.xpath.XPathFactory 의 newInstance()
DocumentBuilderFactory 의 newInstance()
Java는 xml형식의 문서를 객체화 시켜 살펴볼 수 있는 DocumentBuilder 객체를 제공하는데 이 객체는 바로 DocumentBuilderFactory로부터 얻어올 수 있다.
newInstance()를 통해 얻어오는 DocumentBuilderFactory 타입의 객체가 ConcreteFactory에 해당하고 ConcreteFactory가 제공하는 DocumentBuilder가 추상화된 객체(제품)에 해당하게 된다.
public static void main(String[] args) throws ParserConfigurationException, IOException, SAXException {
// 1. 공장 객체 생성
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
// 2. 공장 객체를 통해 추상화된 제품 객체 생성
DocumentBuilder builder = factory.newDocumentBuilder();
// 3. 구현체 메소드 실행
Document document = builder.parse("src/main/resources/config.xml");
System.out.println(document.getDocumentElement());
}
Spring Framework
FactoryBean
FactoryBean은 스프링이 제공하는 인터페이스로, 기본으로 제공되는 스프링 구문으로는 생성 및 관리할 수 없는 객체를 Bean으로 활용할 수 있게끔 어댑터 역할을 한다. 여기서 생성 및 관리할 수 없는 객체란 생성자가 private한 싱글톤 객체 혹은 정적 팩토리 메서드를 통해 얻어오는 객체가 이에 해당한다.
예를 들면 아래 Ship 클래스는 싱글톤 처리된 객체이다.
public class Ship {
private static final Ship INSTANCE = new Ship();
private String name = "Singleton_TurtleShip";
private String color = "Green";
private String logo = "\uD83D\uDC22";
private Ship(){}
public static Ship getInstance() {
return INSTANCE;
}
}
만약 싱글톤 객체를 Bean으로 활용하고 싶다면 FactoryBean을 이용해 등록할 수 있다. FactoryBean의 구현체(ShipFactory)를 만들고 구현체를 Bean으로 등록하면, ShipFactory의 getObject()에서 반환하는 객체가 최종적으로 Bean으로 등록되게 된다.
즉, 여기서 FactoryBean은 추상 팩토리의 AbstractFactory에 해당하고 ShipFactory는 ConcreteFactory에 해당한다.
public class ShipFactory implements FactoryBean<Ship> {
@Override
public Ship getObject() throws Exception {
return Ship.getInstance();
}
@Override
public Class<?> getObjectType() {
return Ship.class;
}
}
클라이언트에서 실행 해보면 xml에 등록된 정보에 따라 ApplicationContext로부터 가져올 떄 ShipFactory가 제공하는 싱글톤 객체를 얻어오게 된다.
<!-- config.xml -->
<bean id="myShip" class="ShipFactory">
</bean>
public static void main(String[] args) throws Exception {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("config.xml");
Ship myShip = applicationContext.getBean("myShip");
System.out.println(myShip);
}
# 참고자료
코딩으로 학습하는 GoF의 디자인 패턴 - 백기선
https://refactoring.guru/design-patterns/abstract-factory
https://reactiveprogramming.io/blog/en/design-patterns/abstract-factory
이 글이 좋으셨다면 구독 & 좋아요
여러분의 구독과 좋아요는
저자에게 큰 힘이 됩니다.