[헤드 퍼스트 디자인 패턴] 04. 팩토리 패턴

akim·2023년 5월 7일
0
post-thumbnail

느슨한 결합으로 객체지향 디자인을 만들어 봅시다
new 연산자를 사용한다고 해서 새로운 객체가 만들어지지는 않습니다. 4장에서는 객체의 인스턴스를 만드는 작업이 항상 공개되어야 하는 것은 아니며, 오히려 모든 것을 공개했다가는 결합 문제가 생길 수 있다는 사실을 배웁니다. 설마 그런 골치 아픈 문제를 원하는 사람은 없겠죠? 팩토리 패턴으로 불필요한 의존성을 없애서 결합 문제를 해결하는 방법을 알아봅시다.

이전에 특정 구현을 바탕으로 프로그래밍하지 않아야 한다는 원칙을 배웠다. 그러나 new 를 사용하게 되면 구상 클래스의 인스턴스가 만들어지므로 결국 특정 구현을 사용하게 되는 셈이다.

new 는 자바의 뼈대를 이루는 연산자이므로 아예 사용하지 않을 수는 없다. 이 때 인터페이스에 맞춰서 코딩하면 시스템에서 일어날 수 있는 여러 변화에 대응할 수 있다. 인터페이스를 바탕으로 만들어진 코드는 어떤 클래스든 특정 인터페이스만 구현하면 사용할 수 있기 때문이다. 이는 모두 다형성 덕분이다.

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

어떻게 하면 애플리케이션에서 구상 클래스의 인스턴스 생성 부분을 전부 찾아내서 애플리케이션의 나머지 부분으로부터 분리(캡슐화)할 수 있을까?


1. 최첨단 피자 코드 만들기

피자 가게에서 피자를 주문하는 메소드(orderPizza())를 만들어보자.

특정 메뉴에 대한 주문이 들어오면 해당 피자 종류를 바탕으로 구상 클래스의 인스턴스를 만들고 인스턴스 변수에 해당 인스턴스를 대입하는 식으로 만들 수 있다. 그런데 만약 판매하는 피자 종류가 바뀐다면 매번 코드를 변경해줘야 한다.

그렇다면 인스턴스를 만드는 구상 클래스를 선택하는 부분을 캡슐화하면 되지 않을까?


객체 생성 부분 캡슐화하기

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

이렇게 새로 만들 객체는 팩토리 가 된다. 객체 생성을 처리하는 클래스를 팩토리라고 하기 때문이다.

simplePizzaFactory 를 만들고 나면 orderPizza() 메소드는 새로 만든 이 객체의 클라이언트가 된다. 즉, 새로 만든 객체를 호출하는 것이다. 피자가 필요할 때마다 피자 공장에 피자를 하나 만들어 달라고 부탁하는 셈이다.

이제 orderPizza() 메소드에서는 Pizza 인터페이스를 구현하는 피자를 받아서 그 인터페이스에서 정의했던 준비, 굽기, 자르기, 박싱하기 등의 메소드만 호출하면 된다.


객체 생성 팩토리 만들기

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

이렇게 캡슐화하면 무슨 장점이 있을까? 원래 코드에 있던 일부를 그냥 다른 객체로 넘긴 게 전부 아닌가?

앞서 본 예시에서는 orderPizza() 메소드만 있었지만, 이렇게 새로 만들어낸 겍체를 사용하는 클라이언트가 매우 많을 수도 있다. 그런 상황에서 피자 객체 생성 작업을 팩토리 클래스로 캡슐화해 놓으면 구현을 변경할 대 여기저기 고칠 필요 없이 팩토리 클래스 하나만 고치면 된다.


클라이언트 코드 수정하기

팩토리를 새로 만들어주었다면 이전 orderPizza() 메소드에서 객체를 생성하는 부분에 썼던 코드 대신 들어갈 코드가 필요하다. 클라이언트 코드는 어떻게 고치면 될까?

public class PizzaStore {
	SimplePizzaFactory factory; // PizzaStore에 SimplePizzaFactory의 레퍼런스를 저장한다.
 
	public PizzaStore(SimplePizzaFactory factory) { // PizzaStore의 생성자에 팩토리 객체가 전달된다.
		this.factory = factory;
	}
 
	public Pizza orderPizza(String type) { // orderPizza() 메소드는 팩토리로 피자 객체를 만든다.
		Pizza pizza;
 
		pizza = factory.createPizza(type); // new 연산자 대신 팩토리 객체의 메소드를 쓴다.
 
		pizza.prepare();
		pizza.bake();
		pizza.cut();
		pizza.box();

		return pizza;
	}

}

위 코드를 보면 new 연산자 대신 팩토리 객체의 메소드를 쓴 것을 볼 수 있다. 이제 더 이상 구상 클래스의 인스턴스를 만들 필요가 없는 것이다!


2. '간단한 팩토리'의 정의

Simple Facotry 는 디자인 패턴이라기 보다는 프로그래밍에서 자주 쓰이는 관용구에 가깝다. 이 간단한 팩토리를 '팩토리 패턴' 이라고 부르는 사람들도 있다. 하지만 엄밀히 말하면 패턴은 아니다!

간단한 팩토리는 왜 쓰이는 것일까?

  • PizzaStore : 팩토리를 사용하는 클라이언트
    이제 PizzaStore는 SimplePizzaFactory로부터 피자 인스턴스를 받게 된다.

  • SimplePizzaFactory : 피자 객체를 생성하는 팩토리
    이 애플리케이션에서 유일하게 구상 Pizza 클래스를 직접 참조하는 부분이다.

  • Pizza : 팩토리에서 만드는 피자
    메소드를 오버라이드해서 쓸 수 있도록 추상 클래스로 정의했다.

  • CheesePizza , VeggiePizza , ClamPizza , PepperomniPizza : 팩토리에서 생산하는 제품에 해당하는 구상 클래스
    각 피자는 Pizza 인터페이스를 구현해야 하며, 구상 클래스여야 한다.
    이 두 조건을 만족하면 팩토리에서 피자를 만들고 클라이언트로 넘길 수 있다.


다양한 팩토리 만들기

다양한 스타일의 피자를 만들 수 있도록 여러 팩토리를 만들어보자.

앞서 배운 방식대로 하면 SimplePizzaFactory 를 삭제하고, 피자 종류별 여러 팩토리를 만든 다음 PizzaStore 에서 적당한 팩토리를 사용하도록 하면 된다.

하지만 이렇게 하게 되면 한 가지 문제점이 생긴다. 지점별로 회사에서 만든 팩토리로 피자를 만들긴 하는데, 굽는 방식이 달라진다거나 종종 피자를 자르는 것을 까먹는 일이 생길 수도 있다.

이 문제를 해결하려면 PizzaStore 와 피자 제작 코드 전체를 하나로 묶어주는 프레임워크를 만들어야 한다. 물론 유연성도 잃어서는 안된다. 그러나 SimplePizzaFactory 를 만들기 전에 썼던 코드에서는 피자를 만드는 코드가 PizzaStore 와 직접 연결되어 있긴 했지만 유연성이 전혀 없었다.

어떻게 해야 피자 가게와 피자 만드는 과정을 하나로 묶을 수 있을까?


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

피자를 만드는 일 자체는 전부 PizzaStore 클래스에서 진행하면서도 지점의 스타일을 살릴 수 있는 방법이 있다.

createPizza() 메소드를 다시 PizzaStore 에 넣고 이번에는 이 메소드를 추상 메소드로 선언한 뒤, 지역 스타일에 맞게 PizzaStore 의 서브 클래스를 만들어보도록 하자.

public abstract class PizzaStore {
 
	public Pizza createPizza(String type){
    	Pizza pizza;
 
		Pizza pizza = createPizza(type); // 팩토리 객체가 아닌 PizzaStore에 있는 createPizza를 호출한다.

		pizza.prepare();
		pizza.bake();
		pizza.cut();
		pizza.box();
        
		return pizza;
	}
    
    abstract Pizza createPizza(Stirng type); // 팩토리 객체 대신 이 메소드를 사용한다. 즉, 팩토리 메소드가 PizzaStore의 추상 메소드로 바뀌었다.
}

서브클래스가 결정하는 것

PizzaStoreorderPizza() 메소드에 이미 주문 시스템이 잘 갖춰져 있다. 이제 각 지점마다 달라질 수 있는 것은 피자 스타일 뿐이다. 달라지는 점은 createPizza() 메소드에 넣고 그 메소드에서 해당 스타일의 피자를 만들도록 하면 된다. 따라서 PizzaStore 의 서브클래스에서 createPizza() 메소드를 구현한다.

PizzaStoreorderPizza() 메소드는 추상 클래스인 PizzaStore 클래스에 정의되어 있다. 그 클래스의 서브클래스를 만들기 전까지는 구상 클래스가 만들어지지 않는다.

orderPizza() 메소드에서 Pizza 객체를 가지고 여러 가지 작업을 하지만, Pizza는 추상 클래스라서 orderPizza() 는 실제로 어떤 구상 클래스에서 작업이 처리되고 있는지 전혀 알 수 없다. 즉, PizzaStore와 Pizza는 서로 완전히 분리되어 있다.

orderPizza() 에서 createPizza() 를 호출하면 Pizza의 서브 클래스가 그 호출을 받아서 피자를 만든다. 어떤 종류의 피자가 만들어지는지는 피자를 주문하는 피자 가게에 따라 달라진다.


피자 스타일 서브클래스 만들기

각 지점에서는 PizzaStore의 서브클래스를 만들고 지역별 특성에 맞게 createPizza() 메소드만 구현하면 PizzaStore의 기능을 그대로 받아서 쓸 수 있다.

예를 들어 뉴욕 스타일의 피자는 아래의 코드를 사용해 만들 수 있다.

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

팩토리 메소드 선언하기

PizzaStore 를 수정한 결과, 구상 클래스 인스턴스 만드는 일을 하나의 객체가 전부 처리하는 방식에서 일련의 서브클래스가 처리하는 방식으로 바뀌었다.

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

		pizza.prepare();
		pizza.bake();
		pizza.cut();
		pizza.box();
        
		return pizza;
	}
    
    protected abstract Pizza createPizza(Stirng type); 
}

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

abstract Product factoryMethod(String type)

위와 같은 코드를 자세히 분석해보자.

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

피자 팩토리 메소드로 피자 주문하기

  1. 주문을 위해서는 우선 NYPizzaStore 가 필요하다.
PizzaStore nyPizzaStore = new NYPizzaStore();

NYPizzaStore 인스턴스를 생성한다.


  1. 피자 가게가 확보되었으니 이제 주문을 받을 수 있다.
nyPizzaStore.orderPizza("cheese");

nyPizzaStore 인스턴스의 orderPizza() 메소드가 호출된다. 그러면 PizzaStore에 정의된 메소드가 호출된다.


  1. orderPizza() 메소드에서 createPizza() 메소드를 호출한다.
Pizza pizza = createPizza("cheese");

팩토리 메소드인 orderPizza() 메소드는 서브클래스에서 구현했다. 이 경우에는 뉴욕 스타일 치즈 피자가 리턴된다.


  1. 아직 준비가 되지 않은 피자를 받았다. 따라서 피자 만드는 작업을 마무리해야 한다.
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();

이 메소드들은 모두 orderPizza() 팩토리 메소드에서 리턴한 특정 피자 객체 내에 정의되어 있다. (orderPizza() 메소드는 NYPizzaStore 에 정의되어 있다.)

또한orderPizza() 메소드에서 피자 객체를 받았지만 그 피자 객체가 어느 구상 클래스의 객체인지는 전혀 알지 못한다.


Pizza 클래스 만들기

public abstract class Pizza { // 우선 Pizza 추상 클래스를 만든 다음 클래스를 확장해서 구상 클래스를 만든다.
	// 피자마다 이름, 반족, 소스, 토핑이 필요하다.
	String name;
	String dough;
	String sauce;
	List<String> toppings = new ArrayList<String>();
 
 	// 피자 준비 과정에서 몇 가지 정해진 단계를 따라야 한다.
	void prepare() {
		System.out.println("Prepare " + name);
		System.out.println("Tossing dough...");
		System.out.println("Adding sauce...");
		System.out.println("Adding toppings: ");
		for (String topping : toppings) {
			System.out.println("   " + topping);
		}
	}
  
  	// 추상 클래스에서 피자를 굽고, 자르고, 상자에 담는 일에 몇 가지 기본 값을 제공한다.
	void bake() {
		System.out.println("Bake for 25 minutes at 350");
	}
 
	void cut() {
		System.out.println("Cut the pizza into diagonal slices");
	}
  
	void box() {
		System.out.println("Place pizza in official PizzaStore box");
	}
 
	public String getName() {
		return name;
	}
}

이제 구상 서브클래스를 만들어야 한다. 예시로 뉴욕 스타일 치즈 피자를 만들어보면 아래와 같다.

public class NYStyleCheesePizza extends Pizza {

	public NYStyleCheesePizza() { 
		name = "NY Style Sauce and Cheese Pizza";
		dough = "Thin Crust Dough";
		sauce = "Marinara Sauce";
 
		toppings.add("Grated Reggiano Cheese");
	}
}

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

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

Creator 클래스

PizzaStore : 추상 생산자 클래스

  • 나중에 서브 클래스에서 제품(객체)를 생산하려고 구현하는 팩토리 메소드(추상 메소드)를 정의함
  • 생산자 클래스에 추상 제품 클래스에 의존하는 코드가 들어있을 때도 있음
  • 이 제품의 클래스 객체는 클래스의 서브 클래스에 의해 만들어지므로 생산자 자체는 어떤 구상 제품 클래스가 만들어질지 미리 알 수 없음

NYPizzaStore, ChicagoPizzaStore : 구상 생산자 클래스

  • 제품을 직접 생산하는 클래스로, createPizza() 메소드가 팩토리 메소드가 되어 이 메소드에서 제품(객체)를 생산함
  • 각 분점마다 PizzaStore 의 서브클래스가 따로 있으므로 createPizza() 메소드 구현을 활용해서 그 가게 고유의 피자를 마음대로 만들 수 있음

Product 클래스

  • 팩토리는 제품을 생산함 (PizzaStore는 Pizza를 만듦)
  • 구상 클래스 피자 가게에서 여러 피자들이 만들어짐

팩소리 메소드 패턴의 정의

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

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

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

  • 제품 클래스는 모두 똑같은 인터페이스를 구현해야 한다. 그래야 그 제품을 사용할 클래스에서 구상 클래스가 아닌 인터페이스의 레퍼런스로 객체를 참조할 수 있기 때문이다.
  • 구상 클래스 인스턴스를 만드는 일은 ConcreteCreator가 한다. 실제 제품을 만드는 방법을 알고 있는 클래스는 이 클래스뿐이다.
  • ConcreteCreator는 실제로 제품을 생산하는 팩토리 메소드를 구현한다.
  • Creator에는 제품으로 원하는 일을 할 때 필요한 모든 메소드가 구현되어 있다. 하지만 제품을 만들어주는 팩토리 메소드는 추상 메소드로 정의되어 있을 뿐 구현되어 있지는 않다.
  • Creator의 모든 서브클래스에서 추상 메소드를 구현해야 한다.

5. 객체 의존성

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

아래 심하게 의존적인 PizzaStore를 한번 살펴보자.

이 코드에서는 모든 피자 객체를 팩토리에 맡겨서 만들지 않고 PizzaStore 클래스 내에서 직접 만들었다. 이 때의 단점은 아래와 같다.

  • 모든 피자 객체를 직접 생성해야 하므로 PizzaStore는 모든 피자 객체에 직접 의존하게 된다.
  • 피자 구상 클래스가 변경되면 PizzaStore 까지도 바꿔야 할 수 있으므로, 'PizzaStore는 피자 클래스 구현에 의존한다'고 말할 수 있다.
  • 피자 클래스들의 구현이 변경되면 PizzaStore 까지 고쳐야 할 수도 있다.
  • 피자 종류를 새로 추가하면 PizzaStore는 더 많은 피자 객체에 의존하게 된다.

의존성 뒤집기 원칙

구상 클래스 의존성을 줄이면 좋다는 사실은 이제 알 것이다. 이를 정리해놓은 디자인 원칙이 있다. 바로 의존성 뒤집기 원칙(Dependency Inversion Principle) 이다.

의존성 뒤집기 원칙(Dependency Inversion Principle) : 추상화된 것에 의존하게 만들고 구상 클래스에 의존하지 않게 만든다.

이전에 봤던 "구현보다는 인터페이스에 맞춰서 프로그래밍한다." 라는 디자인 원칙과 비슷해 보인다. 물론 비슷하긴 하지만 의존성 뒤집기 원칙에서는 추상화를 더 많이 강조한다.

이 원칙에서는 고수준 구성 요소가 저수준 구성 요소에 의존하면 안되며, 항상 추상화에 의존하게 만들어야 한다는 뜻이 담겨있다. (PizzaStore는 고수준 구성 요소, 피자 클래스는 저수준 구성 요소라고 할 수 있다.)


의존성 뒤집기 원칙 적용하기

앞서 살펴본 심하게 의존적인 PizzaStore의 가장 큰 문제점은 PizzaStore가 orderPizza() 메소드에서 구상 형식의 인스턴스를 직접 만들기 때문에 모든 종류의 피자에 의존하게 된다는 점이다.

사실 Pizza라는 추상 클래스를 만들기는 했지만, 이 코드에서 구상 피자 객체를 생성하는 것은 아니기에 추상화로 얻는 것이 별로 없다.

여기에 팩토리 메소드 패턴을 적용하면 인스턴스 만드는 부분을 orderPizza() 에서 뽑아낼 수 있다.

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

팩토리 메소드 패턴이 의존성 뒤집기 원칙을 준수하는 유일한 방법은 아니지만 적합한 방법 중 하나라고 할 수 있다.


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

변수에 구상 클래스의 레퍼런스를 저장하지 않는다.

new 연산자를 사용하면 구상 클래스의 레퍼런스를 사용하게 된다. 따라서 팩토리를 써서 구상 클래스의 레퍼런스를 변수에 저장하는 일을 미리 방지하자.

구상 클래스에서 유도된 클래스를 만들지 말자.

구상 클래스에서 유도된 클래스를 만들면 특정 구상 클래스에 의존하게 된다. 인터페이스나 추상 클래스처럼 추상화된 것으로부터 클래스를 만들어야 한다.

베이스 클래스에 이미 구현되어 있는 메소드를 오버라이드하지 말자.

이미 구현되어 있는 메소드를 오버라이드한다면 베이스 클래스가 제대로 추상화되지 않는다. 베이스 클래스에서 메소드를 정의할 때는 모든 서브클래스에서 공유할 수 있는 것만 정의해야 한다.


그런데 잘 보면, 이 가이드라인을 다 지키다 보면 영원히 프로그램을 다 못 만들 것 같다.

맞다. 다른 원칙들과 마찬가지로, 이 가이드라인은 항상 지켜야 하는 규칙이 아니라 우리가 지향해야 하는 바를 알려주는 것이다.


6. 원재료 팩토리

지금까지 피자 가게의 코드를 팩토리 메소드를 이용해 수정해보았다.

그런데 여기서 원재료를 생산하는 공장까지 관리하도록 원재료군을 처리하는 방법까지 고려해본다면 어떻게 코드를 수정할 수 있을까?

원재료 팩토리 만들기

이 팩토리에서는 원재료군에 들어있는 각각의 원재로를 생산한다.

우선 모든 원재료를 생산하는 팩토리용 인터페이스를 정의하는 것부터 해보도록 하자.

public interface PizzaIngredientFactory {
 
	public Dough createDough();
	public Sauce createSauce();
	public Cheese createCheese();
	public Veggies[] createVeggies();
	public Pepperoni createPepperoni();
	public Clams createClam();
 
}

인터페이스에 각 재료별 생성 메소드를 정의하였다. 여러 가지 새로운 클래스가 도입되었으므로 재료마다 하나씩 클래스를 만들어야 한다.


뉴욕 원재료 팩토리 만들기

public class NYPizzaIngredientFactory implements PizzaIngredientFactory {
 
	public Dough createDough() {
		return new ThinCrustDough();
	}
 
	public Sauce createSauce() {
		return new MarinaraSauce();
	}
 
	public Cheese createCheese() {
		return new ReggianoCheese();
	}
 
	public Veggies[] createVeggies() {
		Veggies veggies[] = { new Garlic(), new Onion(), new Mushroom(), new RedPepper() };
		return veggies;
	}
 
	public Pepperoni createPepperoni() {
		return new SlicedPepperoni();
	}

	public Clams createClam() {
		return new FreshClams();
	}
}

모든 재료 공장에서 구현해야 하는 인터페이스를 뉴욕 원재료 팩토리에서도 구현한다. 재료군에 들어있는 재료를 뉴욕 지점에 알맞게 만든다.


Pizza 클래스 변경하기

공장을 다 만들었다면 재료를 생산할 준비가 끝난 것이다. Pizza 클래스가 팩토리에서 생산한 원재료를 사용할 수 있도록 코드를 다시 고쳐보자.

우선 Pizza 추상 클래스부터 시작해보자.

public abstract class Pizza {
	String name;

	Dough dough;
	Sauce sauce;
	Veggies veggies[];
	Cheese cheese;
	Pepperoni pepperoni;
	Clams clam;

	// prepare() 메소드를 추사아 메소드로 만듦
	abstract void prepare();

	void bake() {
		System.out.println("Bake for 25 minutes at 350");
	}

	void cut() {
		System.out.println("Cutting the pizza into diagonal slices");
	}

	void box() {
		System.out.println("Place pizza in official PizzaStore box");
	}

	void setName(String name) {
		this.name = name;
	}

	String getName() {
		return name;
	}

	public String toString() {
		// 피자 이름을 출력하는 부분
	}
}

이제 prepare() 메소드를 추상 메소드로 만들었다. 이 부분에서 피자를 만드는 데 필요한 재료들을 가져오게 된다. 물론 모든 원재료는 원재료 팩토리에서 가져온다.


추상 클래스 준비도 끝났으니 각각 피자를 만들어보자. 기존 코드에서 달라진 점은 원재료를 팩토리에서 바로 가져온다는 점 말고는 없다.

치즈 피자 코드를 예시로 보면 아래와 같다.

public class CheesePizza extends Pizza {
	PizzaIngredientFactory ingredientFactory;

	// 피자의 원재료를 제공하는 팩토리가 필요하다.
	public CheesePizza(PizzaIngredientFactory ingredientFactory) {
		this.ingredientFactory = ingredientFactory;
	}
 
 	// prepare() 메소드에서 치즈 피자를 만드는 각 단계를 처리한다.
	void prepare() {
		System.out.println("Preparing " + name);
		dough = ingredientFactory.createDough();
		sauce = ingredientFactory.createSauce();
		cheese = ingredientFactory.createCheese();
	}
}
  • 각 피자 클래스는 생성자로부터 팩토리를 전달받고 그 팩토리를 인스턴스 변수에 저장한다.
  • 재료가 필요할 때마다 팩토리에 있는 메소드를 호출해서 만든다.

sause = ingredientFactory.createSause();

핵심이 되는 부분인 위 코드를 자세히 살펴보자.

  • Pizza에 있는 인스턴스 변수에 이 피자에서 사용할 특정 소스의 레퍼런스를 대입한다.
  • ingredientFactory는 우리가 사용하는 원재료 팩토리다. Pizza 클래스는 원재료 팩토리가 맞기만 하면 어떤 팩토리를 쓰든 상관하지 않는다.
  • createSause() 메소드에서는 해당 지역에서 사용하는 소스를 리턴한다.

재료 공장 사용하기

각 지점을 돌면서 제대로 된 피자를 만드는지 확인해보고 지역별 재료 공장의 레퍼런스를 전달해주면 된다.

public class NYPizzaStore extends PizzaStore {
 
 	// 뉴욕 지점에는 뉴욕 피자 원재료 팩토리를 전달해줘야 한다. 뉴욕 스타일 피자를 만들 때 필요한 재료는 이 팩토리에서 공급한다.
	protected Pizza createPizza(String item) {
		Pizza pizza = null;
		PizzaIngredientFactory ingredientFactory = 
			new NYPizzaIngredientFactory();
 
 		// 피자 형식마다 새로운 Pizza 인스턴스를 만들고 원재료를 공급받는데 필요한 팩토리를 지정해준다.
		if (item.equals("cheese")) {
  
  			// 피자에 맞는 재료를 만드는 팩토리를 피자 객체에 전달해준다.
			pizza = new CheesePizza(ingredientFactory);
			pizza.setName("New York Style Cheese Pizza");
  
		} else if (item.equals("veggie")) {
 
			pizza = new VeggiePizza(ingredientFactory);
			pizza.setName("New York Style Veggie Pizza");
 
		} else if (item.equals("clam")) {
 
			pizza = new ClamPizza(ingredientFactory);
			pizza.setName("New York Style Clam Pizza");
 
		} else if (item.equals("pepperoni")) {

			pizza = new PepperoniPizza(ingredientFactory);
			pizza.setName("New York Style Pepperoni Pizza");
 
		} 
		return pizza;
	}
}

새 피자 주문하기

그럼 이 코드를 바탕으로 새롭게 피자를 주문하는 과정을 알아보자.

  1. 주문 첫 단계는 마찬가지로 피자 가게가 필요하다.
PizzaStore nyPizzaStore = new NYPizzaStore();

NYPizzasStore 인스턴스를 생성한다.


  1. 가게가 준비되었으니 주문을 한다.
nyPizzaStore.orderPizza("cheese");

nyPizzaStore 인스턴스의 orderPizza() 메소드가 호출된다.


  1. orderPizza() 메소드는 일단 createPizza() 메소드를 호출한다.
Pizza pizza = createPizza("cheese");

원재료 팩토리를 사용하므로 여기서부터 달라진다.

  1. createPizza() 메소드가 호출되면 원재료 팩토리가 돌아가기 시작한다.
Pizza pizza = new CheesePizza(nyIngredientFactory);

뉴욕 원재료 팩토리를 사용하는 Pizza 인스턴스를 만든다.
PizzaStore에서 원재료 팩토리를 선택하고 그 인스턴스를 만든다. 원재료 팩토리는 각 피자의 생성자에 전달된다.


  1. prepare() 메소드를 호출하면 팩토리에 원재료 주문이 들어간다.
void prepare() {
	dough = factory.crateDough();
	sauce = factory.crateSause();
	chees = factory.crateCheese();
}

  1. 피자 준비가 끝났으므로 orderPizza() 메소드에서 피자를 굽고, 자르고, 포장한다.

7. 추상 팩토리 패턴

이제 팩토리 패턴을 하나 더 배웠다. 앞서 본 것 처럼 제품군을 만들 때 쓸 수 있는 패턴이다.

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

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

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

클래스 다이어그램은 아래와 같다.


PizzaStore의 시선으로 전체 구조를 보면 아래와 같다.


팩토리 메소드 패턴 vs 추상 팩토리 패턴


9. 오늘의 디자인 원칙

  1. 바뀌는 부분은 캡슐화 한다.
  2. 상속보다는 구성을 활용한다.
  3. 구현보다는 인터페이스에 맞춰서 프로그래밍한다.
  4. 상호작용하는 객체 사이에서는 가능하면 느슨한 결합을 사용해야 한다.
  5. 클래스는 확장에는 열려 있어야 하지만 변경에는 닫혀 있어야 한다. (OCP)
  6. 추상화된 것에 의존하게 만들고 구상 클래스에 의존하지 않게 만든다. << new!!


본 포스팅에 쓰인 이미지와 내용의 모든 출처는 책 '헤드 퍼스트 디자인 패턴' 에 있습니다.

profile
학교 다니는 개발자

0개의 댓글