ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [헤드퍼스트 디자인패턴] 4장. 객체지향 빵 굽기: 팩토리 패턴
    Book/헤드 퍼스트 디자인 패턴 2023. 5. 5. 22:18

    4장. 객체 지향 빵굽기: 팩토리 패턴

    4장에 본격적으로 들어가기 전에, new 라는 키워드를 생각해보자.

     

    우리는 앞으로 new 연산자를 만나면, 구상이라는 단어를 떠올려야 한다.

    new 를 사용하면 구상 클래스의 인스턴스가 만들어진다. 당연히 인터페이스가 아닌 특정 구현을 사용하는 것이다. 그런데, 우리는 앞에서 구상 클래스를 바탕으로 프로그래밍 하면 나중에 코드를 수정해야 할 가능성이 커지고, 유연성이 떨어지기 때문에 특정 구현을 바탕으로 프로그래밍 하지 않아야한다는 원칙을 배웠다. 

     

    그렇다면 new 에 어떤 문제가 있는 걸까?


    인터페이스에 맞춰서 코딩하면 시스템에서 일어날 수 있는 여러 변화에 대응할 수 있다. 왜냐하면, 인터페이스를 바탕으로 만들어진 코드는 어떤 클래스든 특정 인터페이스만 구현하면 사용할 수 있기 때문이다. 다형성

     

    반대로 구상 클래스를 많이 사용하면 새로운 구상 클래스가 추가될 때마다 코드를 고쳐야 하므로 수많은 문제가 생길 수 있다. 즉 변경에 닫혀있는 코드가 된다. 따라서 새로운 구상 형식을 써서 확장해야 할 때는 어떻게 해서든 다시 열 수 있게 만들어야 한다.

     

    해결책은 1장에서 배웠던 디자인 원칙, 바뀌는 부분과 바뀌지 않는 부분을 분리해야 한다에 담겨있다.

    피자 코드 만들기

    Pizza orderPizza(){
        Pizza pizza = new Pizza(); //이 부분은 유연성을 감안해서 추상 클래스와 인터페이스로 만들면 좋겠지만, 실제 인스턴스를 만들기 위해 이렇게 구현한다.
    
        pizza.prepare();
        pizza.bake();
        pizza.cut();
        pizza.box();
        return pizza;
    }

    피자 종류가 여러가지인만큼 피자 종류를 고르고 그에 맞게 피자를 만드는 코드도 필요하다.

    Pizza orderPizza(String type){ //에소드 인자로 피자 종류를 전달한다.)
    
        Pizza pizza;
    
        if(type.equals("cheeze")){
            pizza = new CheesePizza();
        } else if (type.equals("greek")){
            pizza = new GreekPizza();
        }
    
        pizza.prepare();
        pizza.bake();
        pizza.cut();
        pizza.box();
        return pizza;
    }

     

    만일 새로운 메뉴가 추가되었다고 생각하면 위 코드의 if 문 부분은 계속해서 변경해야 될 것이다. 그러면 여기서 바뀌는 부분은 바로 if 절 부분이다! 

     

    우리는 if 절에서 인스턴스를 만드는 구상 클래스를 선택하고 있다.

    이 부분에서 orderPizza() 메소드에서 가장 문제가 되는 부분은 인스턴스를 만드는 구상 클래스를 선택하는 부분인 것을 알 수 있다. 이 부분 때문에 상황이 변하면 코드를 변경해야 한다.

     

    우리는 이제 객체 생성 부분을 orderPizza() 메소드에서 뽑아내야 한다. 그리고 객체 생성 코드만 따로 빼서 피자 객체를 만드는 일만 전담하는 객체(SimplePizzaFactory)에 넣어보자.

     

    여기서부터 우리는 새로 만들게 되는 객체를 팩토리라고 부른다.

     

    일단 SimplePizzaFactory를 만들고 나면 orderPizza() 메소드는 새로 만든 객체의 클라이언트가 된다. 즉, 새로 만든 객체를 호출한다. 이제 더 이상 orderPizza()메소드 안에서 어떤 피자를 만들지 고민하지 않아도 된다. 그런데 여기서 해결해야 할 부분이 있다. orderPizza() 메소드에서 객체를 생성하는 부분에 썼던 코드 대신 코드 같은 것들을 어떻게 해야 할지 생각해야 한다.

     

    public class SimplePizzaFactory{
    
    	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;
       }
    }

     

    이제 클라이언트 코드를 수정해 봅시다.

    public class PizzaStore {
    	SimplePizzafactory factory;
    
    	public PizzaStore(SimplePizzafactory factory) {
    		this.factory = factory;
    	}
    	//Pizzastore의 생성자에 팩토리 객체가 전달
    	public Pizza orderPizza(String type) {
    		Pizza pizza;
    		pizza = factory.createPizza(type);
            //orderPizza() 메소드는 팩토리로 피자 객체를 만든다. new 연산자 대신 create 메소드를 사용. 더 이상 구상 클래스의 인스턴스를 만들 필요가 없다.
    		pizza.prepare();
    		pizza.bake();
    		pizza.cut();
    		pizza.box();
    		return pizza;
    	}
    //기타 메소드
    }

    '간단한 팩토리'의 정의

    간단한 팩토리는 디자인 패턴이라기 보다는 프로그래밍에서 자주 쓰이는 관용구에 가깝다. 간단한 팩토리는 일종의 워밍업이라고 생각하면 된다. 

    다양한 팩토리?

    피자 가게의 체인점이 생기면서 앞에서 봤던 SimplePizzaFactory가 아닌 다양한 지역별 피자(뉴욕의 치즈피자와 그릭 피자가 있을 수 있고, 시카고의 치즈피자와 그릭 피자가 있을 수도 있다)를 만들어내야하는 상황이 발생했다. 그런데 이 팩토리들을 나누다 보면, 각 팩토리마다 달라지는 부분이 생긴다. 이러한 문제를 해결하려면, 피자 제작 코드 전체를 하나로 묶어주는 프레임워크를 만들어야 한다. (물론 그렇게 만들면서도 유연성을 잃어버리면 안된다) 

     

    위의 코드에서 factory의 메소드였던 createPizza()를 다시 pizzaOrder에 넣어보자. 이번에는 그 메소드를 추상 메소드로 선언하고 지역별 스타일에 맞게 pizzaStore의 서브 클래스를 만든다.

     

    public abstract class PizzaStore {//abstract로 바뀐 클래스
    	
    	public Pizza orderPizza(String type) {
    		Pizza pizza;
    		pizza = createPizza(type);
            //팩토리 객체가 아닌 PizzaStore에 있는 createPizza를 호출한다.
            
    		pizza.prepare();
    		pizza.bake();
    		pizza.cut();
    		pizza.box();
            
    		return pizza;
    	}
    	
        abstract Pizza createPizza(String type);
    	//이제 팩토리 메소드가 PizzaStore의 추상 메소드로 바뀌었다.
    }

     

    그러면 이 때 서브 클래스가 결정하는 것은 무엇일까?

     

    달라지는 점은 피자 스타일뿐이다. 그래서 이러한 달라지는 점은 createPizza()메소드에 넣고 그 메소드에서 해당 스타일의 피자를 만들도록 할 계획이다. 

     

    한번 orderPizza()를 살펴보면, 그 메소드는 추상 클래스인 PizzaStore안에 정의되어 있다. 그 클래스의 서브 클래스를 만들기 전까지는 구상 클래스가 만들어지지 않는다. 조금 더 생각해보면 orderPizza()메소드 안에서 Pizza 객체를 가지고 여러 가지 작업을 하지만, Pizza는 추상 클래스라서 orderPizza()는 실제로 어떤 구상 클래스에서 작업이 처리되고 있는지 전혀 알 수 없다. 

     

    어떤 종류의 피자가 만들어질지는 피자를 주문하는 피자 가게에 따라 다르다. 그렇다면 서브 클래스에서 피자 종류를 실시간으로 결정하는 걸까? 아니다. 피자의 종류는 어떤 서브 클래스를 선택했는냐에 따라 결정된다.

     

    하지만 여기서 OrderPizza() 메소드는 서브클래스에서 어떤 일이 일어나는지 모르며, 완성된 피자를 받아서 주문을 처리하므로 서브 클래스에서 결정해서 전달해주는 것처럼 보인다.

     

    예를 보며 이해할 수 있게 피자 스타일 서브 클래스를 만들어 봤다.

    public 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;
    	}
    }

    팩토리 메소드 선언하기

    팩토리 메소드는 객체 생성을 서브클래스에 캡슐화할 수 있다. 그러면 슈퍼클래스에 있는 클라이언트 코드와 서브클래스에 있는 객체 생성 코드를 분리할 수 있다.

     

    abstract Product factoryMethod(String type)

    이 선언의 특징을 살펴보자.

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

    피자 주문 과정 살펴보기

    1.  사용자는 PizzaStore의 인스턴스를 확보해야 한다. 본인이 원하는 스타일 피자에 해당하는 PizzaStore 인스턴스를 만들어야한다.

    PizzaStore nyPizzaStore = new NYPizzaStore();

    2.  PizzaStore이 만들어지면 각각 orderPizza()를 호출한다. 이때 인자를 사용해 원하는 피자 메뉴를 골라야 한다.

    nyPizzaStore.orderPizza("cheese");

    3.  피자를 만들 때는 createPizza()메소드가 호출되는데 이 메소드는 PizzaStore 서브 클래스인 NYPizzaStore와 ChicagoPizzaStore에 정의되어 있다. 각각 뉴욕 스타일 피자, 시카고 스타일 피자의 인스턴스를 만든다. 어떤 서브 클래스를 쓰든지 Pizza객체가 orderPizza() 메소드로 리턴된다.

    Pizza pizza = createPizza("cheese");

    4.  createPizza()메소드는 어떤 스타일의 피자가 만들어졌는지 전혀 알지 못한다. 하지만 피자라는 것을 알고 있어서 피자를 준비하고 굽고 자르고 포장하는 작업을 완료한다.

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

    (번외) 이제 그럼 피자 클래스를 만들어보자.

    public abstract class Pizza {
    	String name;
    	String dough;
    	String sauce;
    	List<String> toppings = new ArrayList<String>();
    	void prepare() {
    		System.out.printIn("준비 중: "+ name):
    		System.out.printIn("도우를 돌리는 중...");
    		System.out.printIn("소스를 뿌리는 중...");
    		System.out.printin("토핑을 올리는 중: ");
    		for (String topping : toppings) {
    			System.out.printIn(" " + topping);
    	}
    
    	void bake() {
    		System.out.printin("175도에서 25분 간 굽기");
    	}
    	void cut() {
    	System.out.printin("피자를 사선으로 자르기");
    	}
    	void box() {
    		System.out.printIn("상자에 피자 담기");
    	}
    	public String getName () {
    		return name;
    	}
     }

    이 외의 피자 코드 테스트를 하는 부분은 생략

     

    팩토리 메소드 패턴 살펴보기

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

    팩토리 메소드 패턴은 서브클래스에서 어떤 클래스를 만들지 결정함으로써 객체 생성을 캡슐화한다.

     

    정의

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

     

    다른 팩토리를 쓸 때와 마찬가지로 팩토리 메소드 패턴으로 구상 형식 인스턴스를 만드는 작업을 캡슐화할 수 있다.

     

    밑에 있는 클래스 다이어그램을 보면, Creator 추상 클래스에서 객체를 만드는 메소드, 즉 팩토리 메소드용 인터페이스를 제공한다는 사실을 알 수 있다. Creator 추상 클래스에있는 다른 메소드는 팩토리 메소드에 의해 생산된 제품으로 필요한 작업을 처리한다. 하지만 실제 팩토리 메소드를 구현하고 제품(객체 인스턴스)를 만드는 일은 서브클래스에서만 할 수 있다.

     

    여기서 "팩토리 메소드 패턴에서는 어떤 클래스의 인스턴스를 만들지를 서브클래스에서 결정한다" 라는 이야기에서 "결정한다"라는 표현을 쓰는 이유는 실행 중에 서브 클래스에서 어떤 클래스의 인스턴스를 만들지를 결정해서가 아니라, 생산자 클래스가 실제 생산될 제품을 전혀 모르는 상태로 만들어지기 때문이다. 

     

    정확히 말하면 "사용하는 서브 클래스에 따라 생산되는 객체 인스턴스가 결정된다."라고 할 수 있다.

    의존성 뒤집기 원칙

    객체 인스턴스를 직접 만들면 구상 클래스에 의존해야 한다.

    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.printIn("오류: 알 수 없는 피자 종류");
    			return null;
    		}
    		pizza.prepare();
    		pizza.bake();
            	pizza.cut(); 
            	pizza.box();
    		return pizza;
       }
    }

    위에 팩토리를 사용하지 않는 심하게 의존적인 PizzaStore 클래스를 보자.  이 코드에서는 모든 피자 객체를 팩토리에 맡겨서 만들지 않고 PizzaStore클래스 내에서 직접 만들었다. 

    의존성을 살펴보면 다음과 같다.

     

    이와 관련해서 구상 클래스 의존성을 줄이는 것을 의미하는 객체 지향 디자인 원칙이 있다.

     

    그것이 바로 의존성 뒤집기 원칙(Dependency Inversion Principle)이다.

    💡 디자인 원칙

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

    이 원칙에는 고수준 구성 요소가 저수준 구성 요소에 의존하면 안되며, 항상 추상화에 의존하게 만들어야 한다는 뜻이 담겨 있다.

     

    의존성 뒤집기 원칙에 따르면 구상 클래스처럼 구체적인 것이 아닌 추상 클래스나 인터페이스와 같이 추상적인 것에 의존하는 코드를 만들어야 한다. 

     

    다시 아까 심하게 의존적이었던 PizzaStore로 돌아가서 그것의 문제점을 생각해보면, 모든 종류에 의존하는 PizzaStore이다. orderPizza() 메소드에서 구상 형식의 인스턴스르 직접 만들기 때문이다.

     

    그러면 어떻게 해야 인스턴스 만드는 부분을 orderPizza()에서 뽑아낼 수 있을까? 앞에서 얘기한 대로 팩토리 메소드 패턴으로 인스턴스만드는 부분을 뽑아낼 수 있다.

     

    팩토리 메소드 패턴을 적용하면 고수준 구성 요소인 PizzaStore와 저수준 구성 요소인 피자 객체 모두가 추상 클래스이 Pizza에 의존하게 된다. 

     

    일반적인 디자인 절차에 숨겨져 있는 생각 과정을 따라가보면서 의존성 뒤집기 원칙을 적용했을 때 생각 과정이 어떻게 뒤집히는지 알아보자.

     

    1. 피자 가게 에서는 피자를 준비하고 굽고 포장하고... 다양한 메뉴를 갖춰야 한다: PizzaStore를 구현해야 한다는 의미이다.
    2. 치즈 피자든 야채 피자든 모두 다 피자이므로 Pizza라는 동일한 인터페이스를 공유하면 된다: 위에서부터 내려오는 것을 생각하는 대신 Pizza클래스를 먼저 생각해 보고 어떤 것을 추상화할 수 있는지 생각해보자. ( 위에서부터 내려오면 구피자 가게가 구상 피자 형식을 알게 되어 구상 클래스에 의존해야 하는 문제가 생길 수도 있다.)
    3. 이제 Pizza라는 클래스로 추상화했으니까 PizzaStore를 만들 때는 구상 피자 클래스를 신경쓰지 않아도 된다. : 이 방법이라면 구상 클래스를 없애는 팩토리를 사용해야 한다.

    이제 PizzaStore는 추상화된 Pizza 클래스에 의존하고, 구상 피자 형식이 추상화된 Pizza 클래스에 의존하며 의존성이 뒤집혔다. 

     

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

    📌 가이드 라인

    • 변수에 구상 클래스의 레퍼런스를 저장하지 않는다: new 연산자를 사용하면 구상 클래스의 레퍼런스를 사용하게 된다. 그러니 팩토리를 사용해서 구상 클래스의 레퍼런스를 변수에 저장하는 일을 미리 방지해야 한다.
    • 구상 클래스에서 유도된 클래스를 만들지 않는다: 구상 클래스에서 유도된 클래스를 만들면 특정 구상 클래스에 의존하게 된다. 인터페이스나 추상 클래스처럼 추상화된 것으로부터 클래스를 만들어야 한다.
    • 베이스 클래스에 이미 구현되어 있는 메소드를 오버라이드 하지 않는다: 이미 구현되어 있는 메소드를 오버라이드 한다면 베이스 클래스가 제대로 추상화되지 않는다. 베이스 클래스에서 메소드를 정의할 때는 모든 서브클래스에서 공유할 수 있는 것만 정의해야 한다.

     

    추상 팩터리 패턴

    우와.. 이렇게 많이 했는데 팩터리 패턴 중에 살펴볼 것이 아직 남아있다. 

     

     

    우리는 추상 팩토리라는 방식을 살펴봐야한다. 추상 팩토리로 제품군을 생성하는 인터페이스를 제공할 수 있고, 이 인터페이스를 사용하면 코드와 제품을 생산하는 팩토리를 분리할 수 있다. 

     

    추상 팩토리까지 도입하고 난 후 피자 주문 과정을 다시 한 번 살펴 보자.

    • 주문 첫 단계는 똑같다. 뉴욕 피자 가게에서 주문하고 싶다면 NYPizzaStore이 필요하다.
    PizzaStore nyPizzaStore = new NYPizzaStore();
    • 가게가 준비되었다면 주문을 진행한다.
    nyPizzaStore.orderPizza("cheeze");
    • orderPizza()메소드는 일단 createPizza()메소드를 호출한다.
    Pizza pizza = createPizza("cheese");
    • 원재료 팩토리를 사용하므로 createPizza() 메소드가 호출되면 원재료 팩토리가 돌아가기 시작한다.
    Pizza pizza = new CheesePizza(nyIngredientFactory);
    • 이제 피자를 준비해야 한다. prepare()메소드를 호출하면 팩토리에 원재료 주문이 들어간다.
    void prepare(){
    	dough = factory.createDough();
        sauce = factory.createSauce();
        cheese = factory.createCheese();
    }
    • 피자 준비가 끝났다. orderPizza()메소드는 피자를 굽고 자르고 포장한다.

    추상 팩토리 패턴의 정의

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

     

    제품군을 만드는 추상 형식을 제공하고 제품이 생산되는 방법은 이 형식의 서브클래스에서 정의한다. 팩토리를 사용하고 싶으면 일단 인스턴스를 만든 다음 추상 형식을 써서 만든 코드에 전달하면 된다. 따라서 팩토리 메소드 패턴을 쓸 때와 마찬가지로 클라이언트와 실제 구상 제품이 분리된다. 따라서 일련의 연관된 제품을 묶을 수 있다는 장점도 있다.

     

      추상 팩토리 패턴 팩토리 메소드 패턴
    객체 생성 방식 객체 구성(Composition) 상속
    사용 서로 연관된 일련의 제품을 만들어야 할 때, 즉 제품군을 만들어야 할 때 클라이언트 코드와 인스턴스를 만들어야 할 구상 클래스를 분리시켜야 할 때 

     

Designed by Tistory.