본문 바로가기
Book

[독서 기록] 헤드퍼스트 디자인 패턴 4장 | 팩토리 패턴 - 객체지향 빵 굽기

by Renechoi 2022. 11. 14.
 
헤드 퍼스트 디자인 패턴
이유 1. 흥미로운 이야기와 재치 넘치는 구성이 담긴 〈헤드 퍼스트〉 시리즈! 하나의 패턴에 하나의 이야기를 담았습니다. 틀에 박히지 않아 지루할 틈이 없는 구성과 친구와 이야기하듯 편안한 대화체로 이야기를 풀어냅니다. 이야기 속에 다양한 방법으로 해결할 수 있는 질문과 90개 이상의 연습문제를 담았습니다. 마치 게임 퀘스트를 해결하듯 문제를 하나하나 해결하다 보면 학습한 내용이 머릿속에 강렬하게 남습니다. 이유 2. 원스톱으로 배우는 14가지 GoF 핵심 디자인 패턴과 9가지 객체지향 디자인 원칙! 현장에서 자주 사용되는 옵저버, 어댑터, MVC 패턴 등 14가지 GoF 객체지향 패턴을 중점으로 패턴의 정의, 사용 시기, 사용처, 사용 이유, 즉시 디자인에 적용하는 방법을 알려줍니다. 이와 더불어 객체지향 프로그래밍에 광범위하게 적용할 수 있는 OCP, 할리우드 원칙 등 9가지 객체지향 디자인 원칙과 패턴으로 생각하는 방법도 알려줍니다. 이유 3. 시대의 변화에 맞춘 개정과 한국 독자만을 위한 특별판! 자바 8과 자바 16 이상에서 무리 없이 동작할 수 있도록 예제 코드를 수정했으며, 부가적인 설명과 Q&A 질문을 추가했습니다. 또한 16여 년 만의 개정을 기념해 오직 한국 독자만을 위한 새로운 삽화를 사용하고 한글 친화적인 구성했습니다. 원서를 읽을 때보다 더욱 편안하게 디자인 패턴을 학습할 수 있습니다. ▶ 이 책을 읽어야 하는 당신! ● 소프트웨어 출시는 완벽 그 자체! “어?~ 코드 수정하려고 다시 보니까 난리…” → 유지보수만 생각하면 그저 눈물인 주니어 (자바) 개발자 ● 코딩 실력은 장판파의 장비! “어?~ 팩토리 메소드 패턴을 이렇게 적용했던가?” → 디자인 패턴을 다시 한번 살펴보고 싶은 시니어 (자바) 개발자 ● 혼자 공부해서 다진 프로그래밍 언어 실력! “어?~ 근데 패턴이 뭐야?” → 개발 현장의 소프트웨어 디자인 방법이 궁금한 개발자 지망생
저자
에릭 프리먼, 엘리자베스 롭슨, 케이시 시에라, 버트 베이츠
출판
한빛미디어
출판일
2022.03.16

 

헤드퍼스트 디자인 패턴 4장 | 팩토리 패턴 - 객체지향 빵 굽기

(에릭 프리먼 외 4인, 서환수 옮김, 한빛미디어)

 


 

 

'new' 연산자가 눈에 띈다면 '구상'이라는 용어를 떠올려 주세요.

- 144p 

 

진짜 말썽을 일으키는 녀석을 바로 '변화'입니다. 변화하는 무언가 때문에 new를 조심해서 사용해야 합니다.

- 145p 

 

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

- 145p 

 

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 인스턴스 변수에 그 인스턴스를 대입합니다.
    => 여기에 있는 모든 피자 클래스는 pizaa 인터페이스를 구현합니다. 
    
    => 이부분이 바뀌는 부분입니다. 피자 종류가 바뀔 때마다 코드를 계속 고쳐야 합니다. 
    
    
    pizza.prepare();
    pizza.bake();
    pizza.cut();
    pizza.box();
    
    return pizza; 
    
    => 이 부분은 바뀌지 않습니다. 피자를 준비하고, 굽고, 자르고 포장하는 일은 피자를 판매할 때
    당연히 해야 하는 일입니다. 따라서 이 코드는 고칠 일이 없습니다.

 

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

이 부분 때문에 상황이 변하면 코드를 변경해야 합니다.

이제 어떤 부분이 바뀌고 어떤 부분이 바뀌지 않는지를 파악했으니 캡슐화를 할 차례군요. 

 

- 147 ~ 148p 

 

 

객체 생성 코드를 orderPizza 메소드에서 빼냅니다. 

 

이 코드는 피자를 만드는 일만 처리하는 객체에 넣습니다. 다른 객체에서 피자를 만들어야 할 일이 있으면 이 객체로 와서 부탁하면 되죠.

 

새로 만들 객체를 팩토리라고 부르겠습니다. 

 

- 148p 

 

 

 

객체 생성 팩토리 만들기

 

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 PepperoniPizzza();
        } 
        ...
        
        return pizza;
   }
}

- 149p 

 

 

클라이언트 코드 수정하기

 

Pizza orderPizza(String type) {
	SimplePizzaFactory factory;	=> pizzaStore에 simplepizzaFactory의 레퍼런스를 저장합니다. 
    
    public PizzaStore(SimplePizzaFactory factory){
    	this.factory = factory;  => pizzaStore의 생성자에 팩토리 객체가 전달됩니다. 
    } 
    
    public Pizza orderPizza(String type) {
    	Pizza pizza;
        
        pizza = factory.createPizza(type); => orderPizza() 메소드는 팩토리로 피자 객체를 만듭니다. 주문받은 형식을 그냥 이쪽으로 전달하기만 하면 되죠.
		=> new 연산자 대신 팩토리 객체에 있는 create 메소드를 썼습니다. 이제 더 이상 구상 클래스의 인스턴스를 만들 필요가 없습니다.         
        ...

 

-  150p 

 

 

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

 

public class NYPizzaStore extends PizzaStore {	=> NYPizzaStore는 pizzaStore를 확장하기에 orderPizza() 메소드도 자동으로 상속 받습니다. 

	Pizza createPizza(String item) {
    	if (item.equals("cheese")) {   => createPizza()는 pizzaStore에서 추상메소드로 선언되었으므로 구상 클래스에서 바드시 구현해야 합니다.
        	return new NYStyleCheesePizza();
        } else if (item.equals("veggie")) {
        	return new NYStyleVeggiePizza(); => 구상 클래스의 객체를 생성. 피자 종류에 해당하는 뉴욕 스타일 피자를 생성하여 리턴.
        } ...

 

- 157p 

 

abstract Product factoryMethod(String type) 

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

매개 변수로 만들 객체 종류를 선택할 수도 있습니다. 

 

- 159p 

 

 

피자가 만들어지기까지 

PizzaStore nyPizzaStore = new NYPizzaStore();

NYPizzaStore 인스턴스 생성 

 

피자가게가 확보됐으니 이제 주문을 받을 수 있습니다. 

 

nyPizzaStore.orderPizza("cheese);

 

nyPizzaStore 인스턴스의 orderPizza() 메소드가 호출됩니다. 그러면 pizzaStore에 정의된 메소드가 호출되겠죠.

 

orderPizza() 메소드에서 createPizza() 메소드를 호출합니다. 

 

Pizza pizza = createPizza("cheese");

 

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

 

pizza.preparer();
pizza.bake();
pizza.cut();

이 메소드들은 모두 createPizza() 팩토리 메소드에서 정의한 특정 피자 객체 내에 정의되어 있습니다. 그리고 createPizza() 메소드는 NYPizzaStore에 정의되어 있죠. 

 

- 161p 

 

 

 

Pizza 클래스 만들기 

 


abstract public class Pizza {
	String name;
	String dough;
	String sauce;
	List<String> toppings = new ArrayList<String>();

	public String getName() {
		return name;
	}

	public void prepare() {
		System.out.println("Preparing " + name);
	}

	public void bake() {
		System.out.println("Baking " + name);
	}

	public void cut() {
		System.out.println("Cutting " + name);
	}

	public void box() {
		System.out.println("Boxing " + name);
	}

	public String toString() {
		// code to display pizza name and ingredients
		StringBuffer display = new StringBuffer();
		display.append("---- " + name + " ----\n");
		display.append(dough + "\n");
		display.append(sauce + "\n");
		for (String topping : toppings) {
			display.append(topping + "\n");
		}
		return display.toString();
	}
}

 

 

이제 구상 서브 클래스를 만들어야 합니다. 

 

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

 

 

최점단 피자 코드 테스트 

 

public class PizzaTestDrive {
 
	public static void main(String[] args) {
		PizzaStore nyStore = new NYPizzaStore();
		PizzaStore chicagoStore = new ChicagoPizzaStore();
 
		Pizza pizza = nyStore.orderPizza("cheese");
		System.out.println("Ethan ordered a " + pizza.getName() + "\n");

=> 우선 2가지 피자 가게를 만듭니다.

 

=> 그리고 nyStore를 써서 에단이 주문한 피자를 만듭니다. 

 

슈퍼클래스는 자질구레한 내용을 전혀 몰라도 됩니다. 서브 클래스에서 올바른 피자 인스턴스를 만들어서 필요한 작업을 전부 알아서 처리해주니까요.

 

 

- 161 ~ 164p 

 

 

 

- 165p

 

 

 

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

- 168p 

 

 

여기에 팩토리를 사용하지 않는  심하게 의존적인 PizzaStore 클래스가 있습니다.  ...

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

 

이 코드에서는 모든 피자 객체를 팩토리에 맡겨서 만들지 않고 PizzaStore 클래스 내에서 직접 만들었습니다. 

 

=> 모든 피자 객체를 직접 생성해야 하므로 이 pizzaStore는 모든 피자 객체에 직접 의존하게 됩니다.

=> 여기 있는 피자 클래스들의 구현이 변경되면 pizzaStore까지 고쳐야 할 수도 있습니다.

=> 피자 구상 클래스가 변경되면 pizzaStore까지도 바꿔야 할 수 있으므로, pizzastore는 피자 클래스 구현에 의존한다라고 말할 수 있습니다. 

=> 피자 종류를 새로 추가하면 pizzastore는 더 많은 피자 객체에 의존하게 됩니다.

 

- 172p 

 

 

 

의존성 뒤집기 원칙 

 

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

- 173p 

 

 

PizzaStore는 고수준 구성 요소라고 할 수 있고, 피자 클래스는 저수준 구성 요소라고 할 수 있습니다. Pizzastore 클래스는 구상 피자 클래스에 의존하고 있다는 사실을 확실하게 알 수 있습니다. 

- 173p 

 

팩토리 메소드 패턴을 적용하면 고수준 구성 요소인 PizzaStore와 저수준 구성 요소인 피자 객체 모두가 추상 클래스인 Pizza에 의존한다는 사실을 알 수 있습니다. 팩토리 메소드 패턴이 의존성 뒤집기 원칙을 준수하는 유일한 방법은 아닙니다. 하지만 적합한 방법 중 하나라고 할 수 있습니다.

- 174p 

 

 

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

 

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

- 변수에 구상 클래스의 레퍼런스를 저장하지 맙시다.

=>new 연산자를 사용하면 구상 클래스의 레퍼런스를 사용하게 됩니다. 그러니 팩토리를 써서 구상 클래스의 레퍼런스를 변수에 저장하는 일을 미리 방지합시다. 

 

- 구상 클래스에서 유도된 클래스를 만들지 맙시다.

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

 

- 베이스 클래스에 이미 구현되어 있는 메소드를 오버라이드하지 맙시다. 

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

 

- 177p 

 

 

원재료 팩토리 만들기 

 

public interface PizzaIngredientFactoy {

	public Dough createDough();
    public sauce createSauce();
    public cheese createCheese();

 

=> 인터페이스에 각 재료별 생성 메소드를 정의합니다.

=> 여러가지 새로운 클래스가 도입되었습니다. 재료마다 하나씩 클래스를 만들어야 합니다. 

 

뉴욕 원재료 팩토리를 다음과 같이 구현했습니다. 이 팩토리에서는 마리나라 소스, 레지아노 치즈, 신선한 조개 등을 전문적으로 생산합니다. 

 

public class NYPizzaIngredientFactory implements PizzaIngredientFactory {
	=> 모든 재료 공장에서 구현해야 하는 인터페이스를 뉴욕 원재료 팩토리에서도 구현합니다.
    
    public Dough createDough() {
    	return ne ThinCrustDought();
    }
    
    public Sauce createSauce() {
    	return new MarinaraSauce();
    }

- 180 ~ 181p

 

 

Pizza 클래스 변경하기 

 

public abstract class Pizza {
	String name;
    
    Dough dough;
    Sauce sauce;
    Veggies veggies[];
    Cheese cheese;
    Pepperoni pepperoni;
    Clams clam;
    => 피자마다 준비 과정에서 사용하는 원재료들이 있습니다. 
    
    abstract void prepare();
    => 이제 prepare() 메소드를 추상 메소드로 만들었습니다. 
    이 부분에서 피자를 만드는데 필요한 재료들을 가져옵니다.
    물론 모든 원재료는 원재료 팩토리에서 가져옵니다. 
    
    void bake() {
    	System.out.println("굽기");
    }
    ...

 

 

치즈 피자 코드는 다음과 같이 만들 수 있습니다. 

 

public class CheesePizza extends Pizza {
	PizzaIngreiendtFactory ingredientFactory;
    
    public CheesePizza(PizzaIngredientFactory ingredientFactory) {
    	this.ingredientFactory = ingredientFactory;
    }
    => 피자의 원재료를 제공하는 팩토리가 필요합니다.
    각 피자 클래스는 생성자로부터 팩토리를 전달받고 그 팩토리를 인스턴스 변수에 저장합니다. 
    
    void prepare() {
    	System.out.println("준비중:" + name);
        dough = ingredientFactory.createDough();
        sauce = ingredientFacttory.createDough();
        cheese = ingredientFactory.createCheese();
        => 팩토리의 마법이 일어나는 부분
        => prepare() 메소드에서 치즈 피자를 만드는 각 단계를 처리합니다. 재료가 필요할 때마다
        팩토리에 있는 메소드를 호출해서 만듭니다.

 

피자 코드에서는 팩토리로 피자 재료를 만듭니다. 만들어지는 재료는 어떤 팩토리를 쓰는지에 따라 달라집니다. 피자 클래스는 어떤 재료가 배달되는지 전혀 신경쓰지 않습니다. 피자를 만드는 방법만 알고 있으면 되니까요. 이제 피자 클래스와 지역별 재료가 분리되어 있어서 로키산맥 지역, 북서부 지역 등 모든 지역에서 어떤 팩토리를 사용하든 클래스는 그대로 재사용할 수 있습니다. 

 

sauce = ingredientFactory.createSauce();

=> pizza에 있는 인스턴스 변수에 이 피자에서 사용할 특정 소스의 레퍼런스를 대입합니다.

=> 우리가 사용하는 원재료 팩토리. pizza 클래스는 원재료 팩토리가 맞기만 하면 어떤 팩토리를 쓰든 상관하지 않습니다.

=> createSauce() 메소드에서는 해당 지역에서 사용하는 소스를 리턴합니다. 

 

- 182 ~ 185p

 

 

 

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

 - 190p 

 

public class NYPizzaStore extends PizzaStore {

	protected Pizza createPizza(String itme) {
    	Pizza pizza = null;
        PizzaIngredientFactory ingredientFacotry = new NyPizzaIngredientFactory();
        
        => 뉴욕 지점에서는 뉴욕 피자 원재료 팩토리를 전달해 줘야 합니다. 뉴욕 스타일 피자를 만들때 필요한 재료는 이 팩토리에서 공급.
        
        if (item.equals("cheese")) {
        	pizza = new CheesePizza(ingredientFactory);
            pizza.setName("뉴욕 스타일 치즈 피자");
            
            => 이제 피자에 맞는 재료를 만드는 팩토리를 피자 객체에 전달.

- 186p 

 

 

반응형