Featured image of post 4. 팩토리 패턴

4. 팩토리 패턴

헤드 퍼스트 디자인 패턴

느슨한 결합으로 객체지향 디자인을 만들어봅시다.

팩토리 패턴은 객체를 생성하는 부분을 분리하여 캡슐화 하는 방식으로 유연성과 확장성이 뛰어난 구조를 제공한다.

팩토리

객체의 인스턴스를 만드는 작업이 항상 공개되어야 하는 것은 아니며, 오히려 모든 것을 공개했을때 결합 문제가 발생할 수 있다.

팩토리 패턴은 불필요한 의존성을 없애서 결합 문제를 해결하는데 도움을 줄 수 있다.


new를 사용하면 구상 클래스의 인스턴스가 만들어진다.

인터페이스나 추상클래스 같은 상위 개념으로 타입을 선언한다고 하더라도, 결과적으로 구상클래스(하위 개념)을 할당하기 때문에 특정 구현에 의존된다고 볼 수 있다.

  • 구상 클래스를 바탕으로 코딩하면 나중에 코드를 수정해야 할 가능성이 커지고, 유연성이 떨어진다.

여러 구상 클래스가 있고 특정 상황에서 선택하여 만들어야 할 상황이라면 아래와 같은 코드를 만들어야한다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
/* 1장 예시 SimDuck 참조 */
Duck duck;

if (picnic) {
    duck = new MallardDuck();
} else if (hunting){
    duck = new DecoyDuck();
} else if (inBathTub) {
    duck = new RubberDuck();    
}

이런 코드를 변경하거나 확장해야 할 때는 코드를 다시 확인하고 새로운 코드를 추가하거나 기존 코드를 제거해야 한다.

따라서 이런한 방식으로 만들면 관리와 갱신이 어려워지고 오류가 생길 가능성도 커진다.

근본 원인

new 연산자로 오브젝트를 만들때 문제가 발생하는 근본적인 원인은 변화 때문이다.

인터페이스에 맞춰 코딩하면 시스템에서 일어날 수 있는 여러 변화에 대응할 수 있는 이유는 어떤 클래스든 특정 인터페이스만 구현하면 사용할 수 있는 다형성 덕뿐이다.

이와 반대로 구상 클래스를 많이 사용하면 변경에 닫혀있기 때문에 새로운 구상 클래스가 추가될 때마다 즉, 변화할 때마다 코드를 고쳐야 하므로 많은 문제가 생길 수 있다.

이 때문에 새로운 구상 형식을 써서 확장해야 할 때는 어떻게 해서든 다시 열 수 있게 만들어야 하며, 결과적으로 구상 클래스의 인스턴스 생성 부분을 분리해야 한다.

이렇게 분리되어 객체 생성을 전담하는 영역을 팩토리라고 한다.

의존성 뒤집기 원칙

디자인 원칙
추상화된 것에 의존하게 만들고 구상 클래스에 의존하지 않게 만든다.

의존성 뒤집기 원칙은 고수준 구성 요소가 저수준 구성 요소에 의존하면 안되며, 항상 추상화에 의존하게 만들어야 한다는 것을 의미한다.

  • 구현보다는 인터페이스에 맞춰 프로그래밍한다라는 원칙과 유사한 점이 많지만 의존성 뒤집기 원칙에서는 추상화를 더 많이 강조한다.

개발에서 고수준과 저수준의 기준은 기계가 이해할 수 있는가를 기준으로 설명하며, 기계쪽에 가까울수록 저수준을 의미한다.

  • 즉, 추상 클래스와 인터페이스 등을 이용한 추상화된 개념일수록 고수준이고, 컴퓨터가 해석해서 실행하게되는 코드 부분(구상 클래스)에 가까울수록 저수준이다.
flowchart
    a([특정 기능을 선택해야하는 요소])
    a --> b([기능 A])
    a --> c([기능 B])
    a --> d([기능 C])
    a --> e([기능 D])

팩토리 형식의 구현에서 위와 같이 고수준 구성 요소에서 분기를 통해 특정 구상 클래스를 직접 선택하는 형태는 저수준 요소에 의존하고 있기 때문에 구상 클래스의 종류가 추가되거나, 구상 클래스의 구현이 바뀐다면 의존하고있는 고수준 요소의 수정이 필요하다.

flowchart BT
  a([특정 기능을 선택해야하는 요소])
  f([기능 추상화])
  b([기능 A]) --> f
  c([기능 B]) --> f
  d([기능 C]) --> f
  e([기능 D]) --> f
  
  f <--> a

따라서 분기를 통해 특정 구상 클래스를 선택해야 하는 구현을 피할 수 없다면 의존성 뒤집기를 통해 구상 클래스들의 공통되는 부분들을 모아 상위 요소(인터페이스, 추상 클래스)를 만들어 의존하게 팩토리를 구성하고, 해당 기능이 필요한 요소에서 만들어진 팩토리를 의존하도록 하는 방식을 제안하고 있다.

의존성 뒤집기 원칙을 지키는 방법

다음 가이드라인을 따르면 의존성 뒤집기 원칙에 위배되는 객체지향 디자인을 피하는데 도움이된다.

  • 변수에 구상 클래스의 레퍼런스를 저장하지 않는다.
    • new 연산자를 사용하면 구상 클래스의 레퍼런스를 사용하게된다.
  • 구상 클래스에서 유도된 클래스를 만들지 않는다.
    • 구상클래스에서 유도된 클래스를 만들면 특정 구상 클래스에 의존하게된다.
  • 베이스 클래스에 이미 구현되어 있는 메소드를 오버라이드하지 않는다.
    • 베이스 클래스가 제대로 추상화되지 않는다. 모든 서브클래스에서 공유할 수 있는 것만 정리가 필요하다.

간단한 팩토리

간단한 팩토리(Simple Factory)는 디자인 패턴이라기 보다는 프로그래밍에서 자주 쓰이는 관용구에 가깝다.

간단한 팩토리는 단순히 객체 생성 부분을 전담한다.

  • 객체 생성 부분은 공통으로 사용될 수 있기 때문에 팩토리로 캡슐화해 놓으면 구현을 변경할 때 팩토리 하나만 고치면 된다.
classDiagram
    class Client {
        useProduct()
    }
    
    class SimpleProductFactory {
        createProduct()
    }
    
    class Product {
        << interface >>
        action()*
    }
    
    class ConcreteProductA {
        action()
    }

    class ConcreteProductB {
        action()
    }

    class ConcreteProductC {
        action()
    }
    
    Client ..|> SimpleProductFactory
    SimpleProductFactory ..> Product
    Product <|-- ConcreteProductA
    Product <|-- ConcreteProductB
    Product <|-- ConcreteProductC
  • Client: 팩토리를 사용하는 클라이언트
    • 팩토리를 통해 Product의 구상 클래스 오브젝트를 받음
  • SimplePizzaFactory: Product 객체를 참조하는 팩토리
    • 유일하게 구상 Product 클래스를 직접 참조함
  • Product: 팩토리에서 생산하는 구상 클래스들의 인터페이스
  • ConcreteProduct: 팩토리에서 생산하는 제품에 해당하는 구상 클래스
    • Product 인터페이스를 구현해야 한다.
    • 구상 클래스여야 한다.

최첨단 피자 가게의 문제

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
Pizza orderPizza(String type) {
    Pizza pizza;

    /**
     * 인스턴스를 만드는 구상 클래스를 선택
     */
    if (type.equals("cheese")) {
        pizza = new CheesePizza();
    } else if (type.equals("greek")) {
        pizza = new GreekPizza();
    } else if (type.equals("pepperoni")) {
        pizza = new PepperoniPizza();
    }
    
    pizza.prepare();
    pizza.bake();
    pizza.cut();
    pizza.box();
    
    return pizza;
}

orderPizza()에서 가장 문제가 되는 부분은 인스턴스를 만드는 구상 클래스를 선택하는 부분이다.

  • 위 코드에서 피자 가게의 메뉴를 변경하려면 직접 코드를 수정해야한다. 즉 변경에 닫혀있지 않다.

변경되는 부분인 객체 생성 영역을 별도로 분리할 수 있다.

최첨단 피자 가게에 적용

 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
public class SimplePizzaFactory {
    public Pizza createPizza(String type) {
        if (type.equals("cheese")) {
            return new CheesePizza();
        } else if (type.equals("greek")) {
            return new GreekPizza();
        } else if (type.equals("pepperoni")) {
            return new PepperoniPizza();
        }
        
        throw Exeption();
    }
}

public class PizzaStore {
    SimplePizzaFactory factory;
    
    public Pizza orderPizza(String type) {
        Pizza pizza;
        
        pizza = factory.createPizza(type);
        
        pizza.prepare();
        pizza.bake();
        pizza.cut();
        pizza.box();
        
        return pizza;
    }
    
    // ...
}

팩토리 메소드 패턴

팩토리 메소드 패턴(Factory Method Pattern)에서는 객체를 생성할 때 필요한 인터페이스를 만든다.
어떤 클래스의 인스턴스를 만들지는 서브 클래스에서 결정하기 때문에 클래스 인스턴스 만드는 일을 서브 클래스에게 맡기게 된다.

모든 팩토리 패턴은 객체 생성을 캡슐화한다.

팩토리 메소드 패턴은 팩토리를 별도 클래스로 분리하지 않고 생산 클래스 내부에 인터페이스로 팩토리 형식을 구현하도록 하여 객체 생성을 전담하는 방식으로 캡슐화를 수행한다.

  • 팩토리가 필요한 요소에서 자신의 메소드를 이용하여 팩토리를 구성하는 방식
classDiagram
    class Creator {
        << abstract >>
        factoryMethod()*
        anOperation()
    }
    
    class ConcreteCreator {
        factoryMethod()
    }
    
    class Product {
        << abstract >>
    }
    
    class ConcreteProduct
    
    Product <|-- ConcreteProduct
    Creator <|-- ConcreteCreator
    ConcreteCreator ..|> ConcreteProduct  
  • Creator: 추상 클래스로 제품으로 원하는 일을 할 때 필요한 모든 메소드가 구현됨
    • 하지만 제품을 만들어 주는 팩토리 메소드는 추상 메소드로 정의되어 있을 뿐 구현되어 있지 않음.
    • 추상 메소드인 factoryMethod()을 서브클래스에서 직접 구현해야함.
  • ConcreteCreator: 실제로 제품을 생산하는 factoryMetFhod()를 구현
    • 구상 클래스 인스턴스를 만드는 일을 책임진다.
    • 실제 제품을 만드는 방법을 알고 있는 클래스는 이 클래스 뿐이다.
  • Product: 제품 클래스로 모두 똑같은 인터페이스를 구현해야 함
    • 그 제품을 사용할 클래스에서 구상 클래스가 아닌 인터페이스의 레퍼런스로 객체를 참조하게 만들 수 있다.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public abstract class Creator {
    public Product anOperation(String type) {
        Product product;
        
        product = this.factoryMethod(type);
        
        /* somethimg */
      
        return product;
    } 
    
    protected abstract Product factoryMethod(String type);
}

Creator 추상 클래스에서 추상 메소드로 팩토리 메소드용 인터페이스(factoryMethod())를 제공한다.

Creator 추상 클래스에 구현되어 있는 다른 메소드는 팩토리 메소드에 의해 생산된 제품으로 필요한 작업을 처리한다. 팩토리 메소드가 추상 메소드로 선언되었기 때문에 실제 팩토리 메소드를 구현하고 객체 인스턴스를 만드는 일은 서브클래스에서만 할 수 있다.

  • 팩토리 메소드를 추상 메소드로 선언해서 서브클래스가 객체 생성을 책임지도록 한다.
    • 팩토리 메소드는 클라이언트에서 실제로 생성되는 구상 객체가 무엇인지 알 수 없게 만드는 역할도 겸하게된다.
  • 팩토리 메소드는 특정 객체를 리턴하며, 그 객체는 보통 슈퍼클래스가 정의한 메소드 내에서 쓰인다.
  • 매개변수로 만들 객체의 종류를 선택하게 할 수 있다.

생산자 추상 클래스가 실제 생산될 인스턴스를 전혀 알 수 없고, 사용하는 서브클래스에 따라 생산되는 객체 인스턴스가 정해지기 때문에, 어떤 클래스의 인스턴스를 만들지를 서브클래스에서 결정한다고 표현한다.

병렬 클래스 계층구조

classDiagram
    direction TD
    namespace 생산자 클래스 {
        class Creator {
          << abstract >>
          create()* Product
          useProduct()
        }
  
        class ConcreteCreatorA {
          create() Product
        }
  
        class ConcreteCreatorB {
          create() Product
        }
    }
    
    namespace 제품 클래스 {
      class Product {
        << abstract >>
      }

      class ConcreteProductA1
      class ConcreteProductA2
      class ConcreteProductB1
      class ConcreteProductB2
    }

    Creator <|-- ConcreteCreatorA
    Creator <|-- ConcreteCreatorB
    
    Product <|-- ConcreteProductA1
    Product <|-- ConcreteProductB1

    ConcreteProductA1 .. ConcreteProductA2
    ConcreteProductB1 .. ConcreteProductB2

    ConcreteCreatorA <--> ConcreteProductA1
    ConcreteCreatorB <--> ConcreteProductB1

    ConcreteCreatorA <--> ConcreteProductA2
    ConcreteCreatorB <--> ConcreteProductB2
  • Creator: 서브클래스에서 객체를 생산하려고 구현하는 팩토리 메소드의 추상 클래스
    • 제품 클래스의 객체는 클래스의 서브클래스로 만들어지므로 생산자 자체는 어떤 구상 제품 클래스가 만들어질지 미리 알 수 없다.
    • 생산자 클래스에 추상 제품 크래스에 의존하는 코드가 들어있을 때도 있다.
  • ConcreteCreator: 팩토리 메소드로 해당 메소드에서 객체를 생산하는 구상 생산자.
  • Product: 팩토리가 생산하게 될 제품의 인터페이스
  • ConcreteProduct: 팩토리가 생산하게 될 제품

구상 제품은 구상 생산자가 만들어야 할 많은 제품들이고, 구상 생산자 팩토리 메소드를 이용해 많은 구상 제품들 중 필요한 제품을 선택하게 된다.

이처럼 특정 구상 생산자가 팩토리 메소드로 특정 제품군을 만드는 모든 방법을 캡슐화 되어있게되는 병렬 클래스 계층 구조를 가지게 된다.

간단한 팩토리와 차이점

간단한 팩토리는 특정 생산자에서만 사용할 수 있는 반면, 팩토리 메소드 패턴은 더 유연하고 재사용 가능한 설계를 할 수 있다.

  • 생산자 클래스의 서브 클래스로 어떤 구상 제품을 만들지 결정하기 때문에 생성하는 제품을 마음대로 변경하기 쉽다.
  • 추상 클래스의 내부 메소드 구현을 통해 동작을 일반화하여 여러 번 재사용이 가능한 프레임워크를 만들 수 있다.

피자 가게 프레임워크 만들기

팩토리 메소드 패턴을 통해 다양한 팩토리를 구성할 수 있고, 이를 통해 특색있는 피자 가게 지점을 만들 수 있다.

 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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
public abstract class Pizza {
  String name;
  String dough;
  String sauce;
  List<String> toppings = new ArrayList<String>();

  void prepare() {
    System.out.println("준비중: " + name);
    System.out.println("도우 만들기...");
    System.out.println("소스 뿌리기...");
    System.out.println("토핑 올리기:");
    for (String topping : toppings) {
      System.out.println(" " + topping);
    }
  }

  void bake() {
    System.out.println("굽기");
  }

  void cut() {
    System.out.println("자르기");
  }

  void box() {
    System.out.println("담기");
  }

  public String getName() {
    return name;
  }
}

public class NYStyleCheesePizza extends Pizza {
  public NYStyleCheesePizza() {
    name = "뉴욕 스타일 치즈 피자";
    dough = "씬 크러스트";
    sauce = "마리나라";

    toppings.add("레지아노 치즈");
  }
}

public class ChicagoStyleCheesePizza extends Pizza {
  public NYStyleCheesePizza() {
    name = "시카고 스타일 치즈 피자";
    dough = "두꺼운 크러스트";
    sauce = "플럼 토마토";

    toppings.add("모짜렐라 치즈");
  }

  @java.lang.Override
  void cut() {
    System.out.println("네모난 모양으로 자르기");
  }
}

public abstract class PizzaStore {
  public Pizza orderPizza(String type) {
    Pizza pizza;

    pizza = cratePizza(type);

    pizza.prepare();
    pizza.bake();
    pizza.cut();
    pizza.box();

    return pizza;
  }

  protected abstract Pizza createPizza(String type);
}

public class NYPizzaStore extends PizzaStore {
  Pizza createPizza(String type) {
    if (item.equals("cheese")) {
      return new NYStyleCheesePizza("cheese");
    } else null;
  }
}

public class ChicagoPizzaStore extends PizzaStore {
  Pizza createPizza(String type) {
    if (item.equals("cheese")) {
      return new ChicagoStyleCheesePizza("cheese");
    } else null;
  }
}

추상 팩토리 패턴

구상 클래스에 의존하지 않고도 서로 연관되거나 의존적인 객체로 이루어진 제품군을 생산하는 인터페이스를 제공한다.
구상 클래스는 서브클래스에서 만들게된다.

추상 팩토리 패턴을 사용하면 클라이언트에서 추상 인터페이스로 일련의 제품을 공급받을수 있다.

이때, 실제로 어떤 제품이 생산되는지는 전혀 알 필요가 없다. 따라서 클라이언트와 팩토리에서 생산되는 제품을 분리할 수 있게된다.

classDiagram
    direction TD
    class Client {
        
    }
    
    class AbstractFactory {
        << interface >>
        CreateProductA()*
        CreateProductB()*
    }
    
    class ConcreteFactory1 {
        CreateProductA()
        CreateProductB()
    }

    class ConcreteFactory2 {
      CreateProductA()
      CreateProductB()
    }
    
    class AbstractProductA {
        << interface >>
    }
    
    class ProductA1 {
        
    }
    
    class ProductA2 {
        
    }
    
    class AbstractProductB {
        << interface >>
    }
    
    class ProductB1 {
        
    }
    
    class ProductB2 {
        
    }

    Client --> AbstractFactory
    
    Client --> AbstractProductA
    Client --> AbstractProductB

    AbstractFactory <|-- ConcreteFactory1
    AbstractFactory <|-- ConcreteFactory2

    AbstractProductA <|-- ProductA1
    AbstractProductA <|-- ProductA2
  
    AbstractProductB <|-- ProductB1
    AbstractProductB <|-- ProductB2

    ConcreteFactory1 --> ProductA1
    ConcreteFactory1 --> ProductB1

    ConcreteFactory2 --> ProductA2
    ConcreteFactory2 --> ProductB2

추상 팩토리가 일련의 제품을 만드는 데 쓰이는 인터페이스를 정의하려고 만들어졌기 때문에 추상 팩토리 패턴의 메소드가 팩토리 메소드로 구현되는 경우도 종종 있다.

해당 인터페이스에 있는 각 메소드는 구상 제품을 생산하는 일을 맡고, 추상 팩토리의 서브클래스를 만들어서 각 메소드의 구현을 제공하게된다.

팩토리 패턴 활용법

어떤 패턴을 쓰든 객체 생성을 캡슐화해서 애플리케이션의 결함을 느슨하게 만드록, 특정 구현에 덜 의존하도록 만들 수 있다.

팩토리 메소드 패턴, 추상 팩토리 패턴 모두 애플리케이션을 특정 구현으로부터 분리하는 역할을 하지만 방법이 다르기때문에 용도에 맞게 활용한다.

  • 팩토리 메소드 패턴: 클라이언트 코드와 인스턴스를 만들어야 할 구상 클래스를 분리시켜야 할 때 유용하다.
    • 클래스를 써서 제품을 만든다.
      • 상속을 이용하여 객체를 만든다.
    • 서브클래스로 객체를 만들기 위해 클래스를 확장하고 팩토리 메소드를 오버라이드해야 한다.
      • 자신이 사용할 추상 형식만 알면 되므로 클라이언트와 구상 형식을 분리하게된다.
    • 한 가지 제품만 생산하므로 복잡한 인터페이스도 필요하지 않고 메소드도 하나만 있으면 된다.
  • 추상 팩토리 패턴: 클라이언트에서 서로 연관된 일련의 제품을 만들어야 할 때, 즉 제품군을 만들어야 할 때 활용하기 좋다.
    • 객체를 써서 제품을 만든다.
      • 객체 구성(composition)을 이용하여 객체를 만든다.
    • 제품군을 만드는 추상 형식을 제공하고 제품이 생상되는 방법을 이 형식의 서브클래스에서 정의한다.
      • 팩토리를 사용하려면 인스턴스를 만든 다음 추상 형식을 써서 만든 코드에 전달하는 방식으로 클라이언트와 구상 제품을 분리한다.
      • 제품군에 제품을 추가하는 등의 관련 제품을 확대해야 할 경우에 인터페이스를 바꿔야한다.
      • 많은 제품들을 포함하는 제품 군을 생성하기 때문에 인터페이스가 아주 큰 편이다.

디자인 도구상자 안에 들어가야할 도구들

  • 객체지향 원칙
    • 추상화된 것에 의존하게 만드록 구상 클래스에 의존하지 않게 만든다.
  • 추상 팩토리 패턴
    • 구상 클래스에 의존하지 않고도 서로 연관되거나 의존적인 객체로 이루어진 제품군을 생성하는 인터페이스를 제공한다.
    • 구상 클래스는 서브클래스에서 만든다.
  • 팩토리 메소드 패턴
    • 객체를 생성할 때 필요한 인터페이스를 만든다.
    • 어떤 클래스의 인스턴스를 만들지는 서브클래스에서 결정한다.
    • 팩토리 메소드를 사용하면 인스턴스 만드는 일을 서브클래스에 맡길 수 있다.

핵심 정리

  • 팩토리를 쓰면 객체 생성을 캡슐화할 수 있다.
  • 간단한 팩토리는 엄밀하게 말해서 디자인 패턴은 아니지만, 클라이언트와 구상 클래스를 분리하는 간단한 기법으로 활용할 수 있다.
  • 팩토리 메소드 패턴은 상속을 활용한다.
    • 객체 생성을 서브클래스에게 맡기고, 서브클래스는 팩토리 메소드를 구현해서 객체를 생산한다.
  • 추상 팩토리 패턴은 객체 구성을 활용한다.
    • 팩토리 인터페이스에서 선언한 메소드에서 객체 생성이 구현된다.
  • 모든 팩토리 패턴은 애플리케이션의 구상 클래스 의존성을 줄여줌으로써 느슨한 결합을 도와준다.
  • 팩토리 메소드 패턴은 특정 클래스에서 인스턴스를 만드는 일을 서브클래스에게 넘긴다.
  • 추상 팩토리 패턴은 구상 클래스에 직접 의존하지 않고도 서로 관련된 객체로 이루어진 제품군을 만드는 용도로 쓰인다.
  • 의존성 뒤집기 윈칙을 따르면 구상 형식 의존을 피하고 추상화를 지향할 수 있다.
  • 팩토리는 구상 클래스가 아닌 추상 클래스와 인터페이스에 맞춰서 코딩할 수 있게 해주는 강력한 기법이다.