본문 바로가기
디자인패턴

[디자인패턴] 팩토리 메서드 패턴

by 방준이 2023. 10. 22.
반응형

 안녕하세요. 이번 글에서는 팩토리 메서드 패턴을 정리해보고자 합니다. 저는 현재 NEXTSTEP 플레이그라운드의 과제를 스터디로 진행하고 있습니다. 진행하고 있는 미션에서 팩토리 메서드 패턴을 사용하여 구현하라는 과제가 있어서 디자인 패턴을 구글링 하고, 명확히 이해해 보고자 헤드퍼스트 디자인 패턴이라는 책을 읽게 되었는데 책에서 나오는 코드 예시가 굉장히 이해하기 쉽고 단계별로 코드를 작성하며 디자인패턴을 적용하다 보니 왜 이 디자인패턴을 적용해야 하며, 적용하면서 얻는 장점들을 명확히 이해할 수 있었습니다. 또 이를 오래 기억하고자 블로그에 정리하게 되었습니다.

 

(정리 내용도 단계별로 코드를 수정하며 팩토리 메서드 패턴을 적용하는 방식으로 정리해보고자 합니다.)

 

 

팩토리 메서드 패턴을 이해하기 위한 예시 - 피자가게

 팩토리 메서드 패턴을 알아보기 전에 피자가게를 예시로 아래의 코드를 살펴보겠습니다. 피자 type을 압력받아 Pizza를 선택하고 피자를 조리하고 반환하는 메서드입니다. Pizza는 인터페이스이며 아래의 Cheese, Greek, Pepperoni는 Pizza 인터페이스를 구현하였습니다. 아래의 주석에도 표기해놨듯이 피자의 종류가 추가된다면 코드수정은 불가피할 것입니다. 반면에 Pizza를 준비하고 포장하는 부분은 바뀌지 않을 것입니다. 여기서 가장 문제가 되는 부분은 인스턴스를 만드는 구상 클래스를 선택하는 부분입니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class PizzaStore {
 
    Pizza orderPizza(String type) {
 
        Pizza pizza = null;
 
        // 피자의 종류가 추가된다면 코드는 변경이 필요함.
        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;
    }
}
 
 

 

 가장 문제가 되는 부분, 피자의 종류가 추가가 된다면 코드의 변경이 일어날 가능성이 있는 부분을 따로 빼서 피자 객체를 생성하는 일만 전담하도록 객체에 넣어보도록 해보면 ( 따로 캡슐화하여 클래스로 분리) 피자종류가 추가되더라도 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
37
38
public class PizzaStore {
    private SimplePizzaFactory factory;
 
    public PizzaStore(SimplePizzaFactory factory) {
        this.factory = factory;
    }
 
    Pizza orderPizza(String type) {
        // 피자를 생성하는 부분을 캡슐화
        Pizza pizza = factory.createPizza(type);
 
        // 아래 부분은 바뀌지 않는다.
        pizza.prepare();
        pizza.bake();
        pizza.cut();
        pizza.box();
 
        return pizza;
    }
}
 
// 피자 객체를 생성하는 일만 전담하는 클래스 이를 Factory 라고 부른다.
public class SimplePizzaFactory {
    public static Pizza createPizza(String type) {
        Pizza pizza = null;
 
        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("veggie")) {
            pizza = new PepperoniPizza();   // 피자 종류의 추가
        }
        return pizza;
    }
}
 
 

 

 우리는 여기서 객체 생성을 처리하는 클래스 (SimplePizzaFactory)를 팩토리라고 부릅니다. 용어는 팩토리이지만 아직 팩토리 메서드 패턴이라고 볼 수는 없습니다. 위와 같은 방식을 간단한 팩토리 방식이라고 하겠습니다.  위 코드에서는 Pizza의 종류가 변경이 되거나, 추가가 되더라도 PizzaStore 에서는 코드를 고칠 필요가 없습니다.  피자 객체 생성을 전담하는 팩토리 클래스만 변경하면 되는거죠. 어쨌든 코드 변경은 똑같은 거 아니냐고 생각하실 수도 있는데요.. 생각해 보면 변경이 일어날 수 있는 부분을 캡슐화해놓음으로써 코드를 여기저기 수정할 필요 없이 한 클래스에서 수정만 해주면 되는 거죠.지금까지 구현한 코드를 다이어그램으로 살펴보면 아래와 같습니다. 

 
클래스 다이어그램

 

 하지만, 간단한 팩토리를 적용한 코드에는  문제점이 있습니다.  객체 마을의 피자가게가 인기를 끌면서 여러 지점을 낼 때가 온거죠 지점을 낼 때는 그 지역의 입맛과 스타일을 반영한 피자를 만들어야 합니다. 피자에는 뉴욕스타일의 피자, 시카고 스타일의 피자가 있듯 말이죠.  코드로 살펴본다면 아래와 같습니다. 

 

1
2
3
4
5
6
7
NYPizzaFactory nyFactory = new NYPizzaFactory();
PizzaStore nyStore = new PizzaStore(nyFactory);
nyStore.orderPizza("Veggie");
 
ChicagoPizzaFactory chicagoPizzaFactory = new ChicagoPizzaFactory();
PizzaStore chicagoStore = new PizzaStore(chicagoPizzaFactory);
chicagoStore.orderPizza("Veggie");
 
 

 

 위와 같은 코드 스타일이면 지점에서 만든 팩토리로 피자를 만들긴 하겠지만  지점마다 피자를 만드는 방식이 달라질 수도 있을 것입니다.  따라서 지점을 관리할 필요가 생긴거죠.  조금 더 쉽게 말하면 지점들마다 피자 제작과 관련된 기능을 하나로 묶는 기능이 필요합니다. 이는 지점들 마다 PizzaStore라는 추상 클래스를 상속하면서 관리할 수 있을 것입니다.

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public abstract class PizzaStore {
     
    // 팩토리 메서드가 추상 메서드로 변경!! 
    abstract Pizza createPizza(String item);
 
    public Pizza orderPizza(String type) {
        
        // 팩토리 객체를 호출 -> 추상메서드 호출
        Pizza pizza = createPizza(type);
 
        pizza.prepare();
        pizza.bake();
        pizza.cut();
        pizza.box();
 
        return pizza;
    }
}
 
 

 

 위 구조와 같이 PizzaStore 추상 클래스를 상속하는 방식으로 구현을 한다면 모든 지점들은 슈퍼클래스의 정해진 orderPizza() 방식으로 주문을 받아야 할 것입니다. 여기서 달라질 수 있는 건 createPizza(), 즉 피자 스타일 뿐인 거죠. 코드 관점 덧붙여 본다면 추상 메서드로 선언하여 각 지점마다 orderPizza()를 재정의 할 수 없도록 final로 선언한다면 이를 상속하는 서브클래스에서 주문하는 방식을 관리할 수 있습니다. 이 방식으로 지점을 관리할 수 하나의 프레임워크가 생긴 것이라 볼 수 있습니다. 이어서 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
abstract class PizzaStore {
 
    abstract Pizza createPizza(String item);
 
    public Pizza orderPizza(String type) {
        Pizza pizza = createPizza(type);
        System.out.println("--- Making a " + pizza.getName() + " ---");
        pizza.prepare();
        pizza.bake();
        pizza.cut();
        pizza.box();
        return pizza;
    }
}
 
class NYPizzaStore extends PizzaStore {
 
    Pizza createPizza(String item) {
        if (item.equals("cheese")) {
            return new NYStyleCheesePizza();
        } else if (item.equals("veggie")) {
            return new NYStyleVeggiePizza();
        } else if (item.equals("clam")) {
            return new NYStyleClamPizza();
        } else if (item.equals("pepperoni")) {
            return new NYStylePepperoniPizza();
        } else return null;
    }
}
 
 

 

 

 위 코드에서 슈퍼클래스인 PizzaStore 추상클래스의 orderPizza()를 조금 더 살펴보겠습니다. 해당 메서드 안에서 호출하는   createPizza()를 호출하면 어떤 종류의 피자를 받는 걸까요? 사실 전혀 알 수가 없죠.. 어떤 서브 클래스를 선택했느냐에 따라서 결정됩니다. NYPizzaStore에서 주문을 하게 된다면 뉴욕스타일의 피자를 생성해서 리턴할 것입니다. 그저 받은 피자를 가지고 주문을 받고 처리를 하게 됩니다. 

 

클래스 계층구조 및 다이어그램

 

 처음 시작하기를 코드의 변경 가능성이 있는 부분인, 피자 객체를 생성하는 로직을 따로 클래스로 분리하여 캡슐화하는 것으로부터 시작하였습니다.  이처럼 모든 팩토리 메서드 패턴은 서브 클래스에서 어떤 클래스를 만들지 결정함으로써 객체 생성을 캡슐화합니다. 위의 다이어그램에서는 "어떤 종류의 피자를 만들지, 어떤 스타일의 피자를 만들지" 가 되겠네요. NYPizzaStore에서는 뉴욕스타일의 피자를 만드는 방법이 ChicagoPizzaStore에서는 시카고 스타일의 피자를 만드는 방법이 캡슐화되어 있습니다.

 

 

1. 팩토리 메서드 패턴의 정의

 

 

 이제 마지막으로 일반화된 클래스 다이어그램을 가지고 팩토리 메서드 패턴을 정의해 보겠습니다!

팩토리 메서드 패턴에서는 어떤 클래스의 인스턴스를 만들지는 서브클래스에서 결정합니다.  해당 패턴을 사용하면 클래스의 인스턴스를 만드는 일은 서브클래스에게 맡기게 됩니다. 여기서 "결정한다"라는 표현을 쓴 이유를 좀 더 정확하게 풀어서 설명하자면 사용하는 ConcreteCreator에 따라서 생산되는 객체 인스턴스가 결정되기 때문입니다.  추가로 Creator에서는 추상 클래스 및 추상 메서드를 사용한 예시를 보였으나 간단한 구상 제품의 경우 Creator의 서브클래스 없이 팩토리 메서드 패턴을 정의할 수 있습니다. 

 

 간단하게 팩토리 메서드 패턴에 대해서 알아보았습니다. 바뀌는 부분을 캡슐화하는 것으로부터 시작해서 팩토리라는 테크닉을 자연스럽게? 알아보았는데요 디자인 패턴이라는 용어 때문인지 처음에 해당 패턴을 학습할 때 막연하게 어려울 것이다라고 생각을 하고 공부를 하였는데.. 팩토리 메서드 패턴도 결국에는 객체지향의 특징 중 하나인 캡슐화를 적용하여 중복이 되는 코드를 제거하고 유연성과 확장성이 뛰어난 코드를 위한 것임을 알 수 있었습니다. 이상으로 팩토리 메서드 패턴에 관하여 마무리하겠습니다.

 

참고자료


https://www.yes24.com/Product/Goods/108192370

 

https://inpa.tistory.com/entry/GOF-%F0%9F%92%A0-%ED%8C%A9%ED%86%A0%EB%A6%AC-%EB%A9%94%EC%84%9C%EB%93%9CFactory-Method-%ED%8C%A8%ED%84%B4-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EB%B0%B0%EC%9B%8C%EB%B3%B4%EC%9E%90

반응형