[Design Pattern] 팩토리 패턴(Factory Pattern)

2021. 3. 2. 22:07개발/디자인패턴

Head First Design Patterns를 읽고 학습한 내용을 기록합니다.

 

 

0.개요


 

(손이 좀더 가더라도)조건문을 클래스로 뽑아내서 관리할때가 더 좋은 경우가 있다. 

조건문의 내용이 객체 인스턴스를 만드는 작업이라면 더욱더 그렇다.

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

 

 

이전 패턴을 학습할때 Duck이라는 인터페이스를 써서 각종 오리 인스턴스 생성에 유연하게 대처했다. 

1
Duck duck = new MallardDuck();
cs

 

하지만 아래처럼 일련의 구상 클래스(각종 오리)들이 있을때는 부득이하게 조건문을 통해 인스턴스화를 수행할 수 밖에 없다. [ 이 코드만 보면 크게 문제 될건 없다. ]

1
2
3
4
Duck duck;
if(picnic) duck = new MallardDuck();
else if(hunting) duck = new DecoyDuck();
else if(inBathTub) duck = new RubberDuck();
cs

다음으로는 위 같이 조건문에 의해 인스턴스를 생성해야할 경우 발생할 수 있는 문제점에 대해 살펴본다.

 


 

1.  OCP문제


피자집 가게의 사장이 되어 피자주문 처리 시스템을 만든다고 생각해보자. 

 

피자 주문을 처리하는 orderPizza() 메소드를 아래 처럼 작성했다.

1
2
3
4
5
6
7
8
9
Pizza orderPizza(){
    Pizza pizza = new Pizza();
    
    pizza.prepare();
    pizza.bake();
    pizza.cut();
    pizza.box();
    pizza.pizza();
}
cs

Line 2 : Pizza 클래스를 인스턴스화 한다. 

Line 4~8 : pizza 인스턴스의 메소드를 실행한다. 

 

 

 

가게를 운영하던 중에 Pizza의 종류가 다양해졌다. 각 피자들은 조리법이나 박스 포장법이 다르다고 한다. 

인터페이스 개념을 이용해 Pizza 인터페이스를 정의하고 CheesePizza , GreekPizza, PepperoniPizza 클래스가 Pizza를 구현 한다. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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();
    pizza.pizza();
}
cs

Line 4~10 : orderPizza는 type을 Parameter로 받아 인스턴스로 만들 피자 객체를 선택한다. 

피자의 종류별로 클래스를 만들어서 피자의 특성에 맞게 동작을 구현했다. 여기까지는 꽤 합리적인 구현으로 보인다. 

 

 

피자 가게에 메뉴를 몇가지 추가하려고 한다. 

거기다 그리스식 피자(Greek Pizza)의 판매량이 좋지않아 메뉴에서 제외 시키려고한다. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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();
    } else if(type.equals("clam")){
        pizza = new ClamPizza();
    } else if(type.equals("veggie")){
        pizza = new VeggiePizza();
    }
    
 
    pizza.prepare();
    pizza.bake();
    pizza.cut();
    pizza.box();
    pizza.pizza();
}
cs

판매량이 좋지않은 그리스식 피자를 제거(주석처리) 했으며, 조개피자(Clam Pizza)와 야채피자(Veggie Pizza)를 메뉴에 추가했다. 메뉴를 추가,제거하는 과정에서 우리는 위 코드를 크게 두 부분으로 나눌수 있다는걸 알게됐다.

첫번째 부분은 4~14 Line이다. 피자 메뉴에 변경사항이 발생할때 마다 해당 부분의 소스코드를 수정해야 한다.

두번째 부분은 17~21 Line이다. 피자 메뉴가 어떻게 바뀌든지 기본적으로 수행해야하는 동작으로서 현재 코드에서 변하지 않는 부분이다. 

 

위 orderPizza 메소드는 OCP(Open-Close Principle)를 지키지 못한다. (= 소스코드 변경에 대해서 닫혀있지 않다. )

코드변경(메뉴 변경)에 닫혀있게 하기 위해선 4~14번째 Line을 따로 분리해서 처리해줘야 한다. 

 

위 코드의 4~14 Line을 처리하기 위한 클래스를 하나 만들어 보자. 객체 생성을 처리하는 클래스를 팩토리(Factory)라고 부른다.

 

OCP를 지키기 위해 SimplePizzaFactory 클래스를 만들자.

내부에 피자 객체 생성을 담당하는 method인 createPizza를 구현했다. type 파라미터를 통해 인스턴스화 할 클래스를 선택한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 팩토리 클래스
public class SimplePizzaFactory{
    
    // 피자 객체 생성을 위한 Method
    public Pizza createPizza(String type){
        Pizza pizza = null;
 
        if(type.equals("cheese")){
            pizza = new CheesePizza();    
        } else if(type.equals("pepperoni")){
            pizza = new PepperoniPizza();
        } else if(type.equals("clam")){
            pizza = new ClamPizza();
        } else if(type.equals("veggie")){
            pizza = new VeggiePizza();
        }
 
        return pizza;
 
    }
}
cs

 이런식으로 피자 생성을 기존의 orderPizza 메소드에서 분리하면 좋은점 중 하나는 프로젝트의 어느곳에서든지 Factory 클래스를 통해서 객체를 손쉽게 생성할 수 있다는 점이다. ( = 코드의 재사용성이 좋아진다. )

 

SimplePizzaFactory를 사용할 수 있도록 기존의 orderPizza 메소드를 수정한다. 객체의 생성을 factory에 맡긴다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class PizzaStore{
    SimplePizzaFactory factory;
    
    public PizzaStore(SimplePizzaFactory factory){
        this.factory = factory;
    }
 
    public Pizza orderPizza(String type){
        Pizza pizza;
 
        // 객체생성을 factory에 맡긴다.
        pizza = factory.createPizza(type);
        
        pizza.prepare();
        pizza.bake();
        pizza.cut();
        pizza.box();
        pizza.pizza();
 
    }
}
cs

책에선 여기까지의 실습을 Simpe Factory라고 소개하는데 이는 디자인 패턴이라고 하진 않는다고 한다. 

[ 너무 단순해서 그런지 패턴이라고 인정하지 않는듯하다... ]

 


 

2. 팩토리 메소드 패턴 구현해보기


 

뉴욕과 시카고 지역에 매장을 추가하게 되었다.

각 지역별로 피자를 만드는 방식이 조금씩 차이가 난다고한다. 같은 채식(Veggie) 피자라도 지역별로 다른 방식으로 처리해야한다.

따라서 지역별로 피자를 만드는 방법을 관리해야하기때문에 지역별 PizzaFactory를 만든다.

( SimplePizzaFactory를 지역을 기준으로 둘로 쪼갠다. )

 

뉴욕 지역을 위한 피자 인스턴스를 관리하는 클래스 이름을 NYPizzaFactory로 지었다. 

1
2
3
NYPizzaFactory nyFactory = new NYPizzaFactory();
PizzaStore nyStore = new PizzaStore(nyFactory);
nyStore.order("Veggie");
cs
Line 1 : 뉴욕스타일 피자 인스턴스 생성을 담당하는 NYPizzaFactory 클래스를 인스턴스화 한다.
Line 2 : PizzaStore의 생성자에 nyFactory를 주입한다. 이제부터 nyStore는 nyFactory에서 제공하는 방식으로만 피자를 생성할 수 있게된다.
Line 3 : "Veggie" type의 피자를 주문한다.

 

위와 마찬가지로 시카고스타일 피자 생성방법을 매장에 부여한다.

1
2
3
ChicagoPizzaFactory chicagoFactory = new ChicagoPizzaFactory();
PizzaStore chicagoStore = new PizzaStore(chicagoFactory);
chicagoStore.order("Veggie");
cs

뉴욕과 시카고 지역을 위한 피자 제작 방식을 Factory 클래스를 사용하여 분리했다. 

 

 

지역별 피자 인스턴스 생성에 대해 잘 대처했지만 코드에 한가지 문제점이 있다.

각 매장에 너무 높은 자유도를 부여한것인지 pizza를 두번 굽거나, 피자를 자르지 않는 일이 발생했다. 

예를들면 아래와 같은 경우말이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class PizzaStore{
    SimplePizzaFactory factory;
    
    public PizzaStore(SimplePizzaFactory factory){
        this.factory = factory;
    }
 
    public Pizza orderPizza(String type){
        Pizza pizza;
 
        // 객체생성을 factory에 맡긴다.
        pizza = factory.createPizza(type);
        
        pizza.prepare();
       pizza.bake();
        pizza.bake();
        //pizza.cut();
        pizza.box();
        pizza.pizza();
 
    }
}
 
cs

 

그래서 모든 매장에서 피자를 처리하는 활동(주문,굽기,자르기 등)을 하나로 통일하려고 한다.

 

 

해결을 위해 먼저 PizzaStore를 추상클래스로 변경한다. (이제 PizzaStore 클래스를 인스턴스화 할수 없게 되었다. )

createPizza를 추상 메소드로 정의함으로써 서브클래스에서 피자 인스턴스를 생성하도록 했다.

그리고 orderPizza메소드를 추상 클래스에서 구현한다. ( 서브 클래스에서 수정하지 않는다면 orderPizza 메소드에서 정의한 동작을 따라야 할것이다. )

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public abstract class PizzaStore{
    
    public Pizza orderPizza(String type){
        Pizza pizza;
 
        // pizza 객체 생성을 외부에 위임한다
        pizza = createPizza(type);
        
        pizza.prepare();
        pizza.bake();
        pizza.cut();
        pizza.box();
        pizza.pizza();
 
    }
    
    // 구현을 서브클래스에 위임한다
    abstract createPizza(String type);
 
 
}
cs
 

 

NYPizzaStore 클래스를 개선해보자.
추상클래스인 PizzaStore를 상속받으며, createPizza메소드만 구현한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class NYPizzaStore extends PizzaStore{
    
    // PizzaStore에 선언된 createPizza를 구현한다
    Pizza createPizza(String type){
        if(type.equals("cheese")){
            return new CheesePizza();    
        } else if(type.equals("pepperoni")){
            return new PepperoniPizza();
        } else if(type.equals("clam")){
            return new ClamPizza();
        } else if(type.equals("veggie")){
            return new VeggiePizza();
        } else return null;
        
    }
}
cs

 

 

 

위에서 부분부분 작성한 내용을 바탕으로 실제 작동하는 소스코드를 구현해보자.

전체 프로젝트의 구조는 아래와같다.

 

먼저 Pizza.java에 Pizza추상 클래스를 작성한다.

Pizza 추상클래스에는 굽고,자르기,포장하기 등 기본적인 동작이 구현되어있다.

 

 

 

Pizza를 상속받아 ChicagoStyleCheesePizza.java를 작성한다.

여기서는 시카고풍 치즈 피자에 대한 정보를 작성한다.

시카고피자는 사각형(square)형태로 잘라야 하기 때문에 Pizza 추상클래스의 cut 메소드를 override하여 새로 구현한다. 

이런식으로 뉴욕과 시카고에서 필요한 피자 구상 클래스들을 작성한다. 

 

 

이제 PizzaStore.java에 PizzaStore 추상 클래스를 작성한다.

피자 객체를 생성하는 작업은 abstract 메소드로 정의함으로써 서브 클래스에 구현을 위임한다.

주문된 피자를 처리하는 orderPizza 메소드는 모든 가게에서 일관되게 주문을 처리하도록 하기위해서 PizzaStore에서 직접 구현한다.

여기서 알아야 될것은 PizzaStore에서는 어떤 종류의 피자가 만들어 질것인지 전혀 알지 못한다. ( 인스턴스화를 위한 new가 코드상에 없다. )

단지 주문으로 들어온 피자를 준비하고 굽고 자르고 포장할 뿐이다.

이는 Pizza 객체 생성pizza의 주문처리가 클래스로 완전히 분리되었다는 것을 의미한다 ( 느슨한 결합 )

 

ChicagoPizzaStore 클래스를 작성한다.

추상클래스인 PizzaStore를 상속받아 createPizza 메소드를 구현한다.

createPizza는 item문자열을 파라미터로 받아 알맞은 Pizza 인스턴스를 return한다.

(NYPizzaStore도 같은 방식으로 작성했다. )

 

핵심은 슈퍼 클래스인 PizzaStore에서는 어떤 클래스가 인스턴스화 될지 전혀 모른다는것이다.

PizzaStore는 단지 생성된 Pizza 객체에 대해 굽고(bake), 자르는(cut) 등의 작업을 할 뿐이다.  

 

 

 결과 확인을 위한 PizzaTestDrive.java를 작성한다. 시카고 매장 클래스를 인스턴스화하고 orderPizza메소드를 실행한다.

 

시카고 매장의 치즈피자의 주문(orderPizze메소드)에 대한 결과를 출력한다.

 

 


 

생산자와 제품 클래스


 

완성된 소스코드의 클래스를 크게 둘로 나눌 수 있다.

NYPizzaStore클래스와 ChicagoPizzaStore 클래스는 ChicagoStyleCheesePizza, NYStyleCheesePizza 클래스를 비롯한 다양한 스타일의 구상클래스 인스턴스화를 수행하고 있다.

따라서 Pizza의 서브클래스들은 PizzaStore의 서브클래스에 의해 생산되는 제품이라고 할 수 있다.

ChicagoPizzaStore는 모든 ChicagoStyle 피자의 객체 생성에 대한 정보를 가지고 있는 createPizza라는 메소드를 가지고 있는데 이를 팩토리 메소드라고 부른다. ( NYPizzaStore도 마찬가지다. )

 

책에서 팩토리 메소드 패턴을 아래와 같이 정의하고 있다.

 팩토리 메소드 패턴에서는 객체를 생성하기 위한 인터페이스를 정의하는데(PizzaStore 클래스의 createPizza메소드) 어떤 클래스의 인스턴스를 만들것인지는 서브클래스에서 결정하게 만듭니다(NYPizzaStore, ChicagoPizzaStore 클래스에서 구현). 팩토리 메소드 패턴을 이용하면 클래스의 인스턴스를 만드는 일을 서브클래스에게 맡기는 것이죠. ( 객체의 생성과 동작을 분리했다. )

개인적으로 코드 중간에 조건문을 통해 객체를 생성하고 있다고 해서 팩토리 메소드 패턴을 쓰기보다는 조건문에 의해 생성되는 객체의 유사성을 먼저 보고 상황에 따라 팩토리 메소드 패턴을 적용해야하지 않을까 싶다

 


의존성 뒤집기(역전) 원칙


팩토리 메소드 패턴의 소중함을 느껴보기 위해 극악의 의존성을 가진 PizzaStore를 한번보자.

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
public class DependentPizzaStore {
 
    public Pizza createPizza(String style, String type) {
        Pizza pizza = null;
        if (style.equals("NY")) {
            if (type.equals("cheese")) {
                pizza = new NYStyleCheesePizza();
            } else if (type.equals("veggie")) {
                pizza = new NYStyleVeggiePizza();
            } else if (type.equals("clam")) {
                pizza = new NYStyleClamPizza();
            } else if (type.equals("pepperoni")) {
                pizza = new NYStylePepperoniPizza();
            }
        } else if (style.equals("Chicago")) {
            if (type.equals("cheese")) {
                pizza = new ChicagoStyleCheesePizza();
            } else if (type.equals("veggie")) {
                pizza = new ChicagoStyleVeggiePizza();
            } else if (type.equals("clam")) {
                pizza = new ChicagoStyleClamPizza();
            } else if (type.equals("pepperoni")) {
                pizza = new ChicagoStylePepperoniPizza();
            }
        } else {
            System.out.println("Error: invalid type of pizza");
            return null;
        }
        pizza.prepare();
        pizza.bake();
        pizza.cut();
        pizza.box();
        return pizza;
    }
}
 
cs

위 소스코드에 대한 의존성을 그림으로 그려보면 아래와 같다.

 

피자 종류가 추가/제거 될때마다 pizzaStore의 소스코드도 함께 수정해야한다.

이는 "pizzaStore클래스가 각종 스타일의 피자 클래스에 의존하고 있다"라고 표현하며, 이를 나타내기 위해 화살표를 pizzaStore 클래스가 각종 스타일의 피자를 향하도록 그렸다.

 

 

PizzaStore와 각종 스타일의 피자 클래스간의 의존성을 제거하기위해 우리는 Pizza 추상 클래스를 사용했다.

Pizza 추상 클래스를 추가한 뒤에 의존성 그림을 다시 그려보자.

PizzaStore는 이전과 달리 Pizza 추상클래스에만 의존적으로 바뀌었다.

또한 각종 스타일의 피자들은 Pizza 추상 클래스에 의존적으로 바뀌었다.( 다양한 스타일의 피자들은 Pizza 추상클래스의 구상 클래스가 되었다. ) 

이런식으로 의존성의 방향을 뒤집는 행위를 의존성 뒤집기(역전)라고 한다.

이는 디자인패턴의 중요한 원칙 중 하나이고, 의존성 뒤집기 원칙(Dependency Inversion Principle)라는 용어로 사용되고 있다.

 

 

지금까지 팩토리 메소드 패턴을 통해서 의존성을 제거했다면 다음에는 추상 팩토리 메소드 패턴을 통해 의존성을 제거하는 방법을 알아보자.

 


추상 팩토리 메소드


지금까지는 제품 레벨까지 클래스를 정의했다. 

여기서 제품은 ChicagoStyleCheesePizza 또는 NYStyleVeggiePizza등 다양한 스타일의 피자 클래스를 의미한다.

 

한단계 더 나아가 제품레벨이 아닌 재료의 조합으로 자유롭게 제품을 만들어내려고 한다. 

지금까지는 ChicagoStyleCheesePizza 클래스를 보면 내부에 재료에 대한 지식이 모두 다 들어있다. 

도우는 뭘로하고, 소스는 뭘로할지를 모두 ChicagoStyleCheesePizza 클래스 내부에서 했다.

 

이런식으로 말이다. ( 도우는 Extra Thick Crust Dough이며 , 소스는 Plum Tomato Sauce를 사용한다. )

 

이렇게 되면 메뉴가 추가될때마다 새로운 Style의 메뉴 클래스를 만들어 내야한다. 따라서 재료의 조합을 통해서 피자를 만들 수 있도록 클래스 구조를 수정해보자.

 

위 개념을 적용하기 위해 구조를 다음과 같이 정의했다.

 

ingredient(재료) 패키지에 재료 인터페이스와 이를 구현한 클래스를 넣었다.

또한, 각각의 재료를 인스턴스화 해주는 Factory 클래스를 만들었다.

 

이전의 메소드 팩토리 패턴을 구현할때는 각 지역별로 피자 스타일을 관리했지만 이제는 재료들을 자유롭게 조합해서 사용해서 피자를 만들면 되기 때문에 NYStyleCheesePizza라던지 ChicagoStyleVeggiePizza 같은 메뉴들을 모두 제거했다. 

 

ingredient 패키지에 있는 Pepperoni.java를 보자.

Pepperoni 인터페이스를 상속받아 toString 메소드를 구현했다. (모든 재료를 이런 방식으로 구현했다.)

 

구현된 재료 클래스를 인스턴스화 하기 위한 factory클래스를 작성한다.

먼저 factory에서 가능한 동작을 정의한 인터페이스를 작성한다. 

 

PizzaIngredientFactory를 구현하는 NYPizzaIngredientFactory를 작성한다.

뉴욕풍 피자를 만들때 사용가능한 재료 인스턴스를 return하는 메소드들이 작성되어있다.

뉴욕 Style 피자를 만들땐 도우를 항상 ThineCrustDough를 사용한다.

 

이제 다양 스타일의 피자 클래스를 작성해보자. 아래는 야채피자 클래스다

야채피자를 만드는 과정은 모든 지역이 동일하다. 다만 지역마다 사용되는 재료만 다를뿐이다.

( = 모든 지역이 야채피자를 만드는 방법은 모두 같지만 지역별로 사용 재료 차이가 있다. )

 

매장에서 수행가능한 메소드를 작성한 추상 클래스를 작성한다. ( 이전에 구현했던 소스코드와 같다. )

 

뉴욕 스타일 피자를 생산하는 NYPizzaStore구상 클래스를 작성하자. 이전에 작성했던것 보다 조금 복잡해진 모습이다.

Line 15~16 : 뉴욕풍 피자를 위한 재료를 인스턴스화 하는 팩토리다.

Line 23~26 : item 변수를 통해 생성할 피자를 결정한다. VeggiePizza에 그냥 사용가능한 재료(ingredientFactory)를던져준다.

이전에 NYStyleVeggiePizza() 클래스를 return하던 것과 다소 차이가 있다.

 

 

팩토리 메소드 패턴에서 작성한 ChicagoPizzaStore

 

 

 

테스트 해보자. 

 

 

책에서 추상 팩토리 패턴을 다음과 같이 정의하고 있다. [뭔 소릴까.... 사실 아직 이해가 안간다.]

추상 팩토리 패턴에서는 인터페이스를 이용하여 서로 연관된, 또는 의존하는 객체를 구상 클래스를 지정하지 않고도 생성할 수 있습니다.

팩토리 메소드 패턴은 생산자가 요청하는 제품을 인스턴스화 했다면, 추상 팩토리 패턴은 여기서 한단계 더 내려가서 재료를 제공함으로써 제품을 좀더 자유롭게 생산할 수 있도록 한다.

 

 

그림으로 보면 눈에 더 잘들어온다. 좌측이 메소드 팩토리 패턴, 우측이 추상 팩토리 패턴이다.

메소드 팩토리 패턴의 Pizza 생산을 보면 (좌측 붉은색 상자) Pizza를 상속받아 Pizza를 구현한다.

추상 팩토리 패턴은 Pizza이전에 재료 팩토리(우측 붉은색 상자)를 정의함으로써 사용자가 원하는 형태의 제품 생산이 가능하도록 했다.  [ 위 사진을 보면 같은 depth 처럼 보이는데 우측 붉은색 상자 윗쪽으로 Pizza 클래스와 PizzaStore 클래스들이 더 나와야한다. 공간때문에 짤렸나 보다.  ]

 


정리


두 팩토리 패턴은 제품을 생산하는 방식에서 차이가 나는데 간단하게 표현하면 아래와 같다.

 

 

메소드 팩토리 패턴 = 클래스를 통해 제품을 정의함.

 

 

추상 팩토리 패턴 = 객체의 집합을 통해 제품을 정의함.

 

두 패턴은 각각의 장단점이 있다. 

 

메소드 팩토리 패턴은 제품의 변경이 잦은게 아니라면 간단하게 클래스간 결합을 느슨하게 구현할수 있다.

추상 팩토리 패턴은 구현이 다소 복잡하지만 제품에 종속되어 있지 않다보니 재료 변경에 유연하게 대체할수 있다. 

 

 위 두 패턴을 실무에 100% 적용하기는 쉽지 않다. 하지만 꼭 팩토리 패턴을 쓰진 않더라도 객체 인스턴스 생성을 캡슐화 해서 처리한다면 관리측면에서 훨씬 유용할것이다.