헤드퍼스트 디자인패턴 Chapter4

헤드퍼스트 디자인패턴

  • 본 책을 읽고 책의 내용을 간략하게 정리한 글입니다.

Chapter 4. 객체지향 빵 굽기 - 팩토리 패턴

new 연산자가 보이면 “구상”을 떠올려라

  • new 연산자를 사용하면 구상 클래스의 인스턴스가 생성된다. (인터페이스가 아닌 특정 구현을 사용해서)
  • 앞서 구상 클래스를 바탕으로 코딩하면 나중에 코드를 수정해야 할 가능성이 커지고, 유연성이 떨어진다고 배웠다.
  • 일련의 구상 클래스가 있다면 아래와 같은 코드를 만들어야 한다. ```java Duck duck;

// 컴파일하기 전에는 어떤 오리의 인스턴스를 만들어야 하는지 알 수 없다. // 이런 코드는 관리와 갱신이 어려워지고 오류가 생길수도 있다. if (picnic) { duck = new MallardDuck(); } else if (hunting) { duck = new DecoyDuck(); } else if (bathtub) { duck = RubberDuck(); }


#### `new` 연산자의 문제
* `new` 연산자 자체에 문제는 없다. 단 **변화**하는 무언가가 문제를 일으킨다.
* 인터페이스에 맞춰서 코딩하면 시스템에서 일어날 수 있는 여러 변화에 대응할 수 있다.
* **다형성**
  * 인터페이스를 바탕으로 만든 코드는 어떤 클래스던 인터페이스만 구현하면 사용할 수 있다.
* **변경에 닫혀 있는** 코드
  * 구상 클래스를 많이 사용하면 새로운 구상 클래스가 추가될 때마다 코드를 고쳐야 한다.

### 피자 주문 코드 만들기
```java
Pizza orderPizza() {
    Pizza pizza = new Pizza();
    
    pizza.prepare();
    pizza.bake();
    pizza.cut();
    pizza.box();
    return pizza;
}
  • 위 예제 코드에서 피자 종류를 고르고 그에 맞게 피자를 고르는 코드를 추가한다.
    Pizza orderPizza(String type) {
      Pizza pizza = new Pizza();
       
      // 피자 종류를 바탕으로 구상 클래스의 인스턴스를 만든다.
      // 해당 부분이 바뀌는 부분! (신메뉴나 메뉴 변경이 될 때마다 바뀌는 부분이다.)
      if ("cheese".equals(type)) {
          pizza = new CheesePizza();
      } else if ("greek".equals(type)) {
          pizza = new GreekPizza();
      } else if ("pepperoni".equals(type)) {
          pizza = new PepperoniPizza();
      }
        
      pizza.prepare();
      pizza.bake();
      pizza.cut();
      pizza.box();
      return pizza();
    }
    
  • orderPizza() 메서드에서 문제가 되는 부분은 인스턴스를 만드는 구상 클래스를 선택하는 부분이다.
    • 상황이 변하면 코드를 변경해야 하는 부분

객체 생성 부분 캡슐화

  • 우선 객체 생성 코드만 따로 빼서 피자 객체를 만드는 일만 전담하는 객체에 넣는다.
  • 객체 생성을 처리하는 클래스를 팩토리(Factory)라고 부른다.
  • 간단한 피자 팩토리 생성하기
    public class PizzaFactory {
      public Pizza createPizza(String type) {
          Pizza pizza = null;
    
          if ("cheese".equals(type)) {
              pizza = new CheesePizza();
          } else if ("greek".equals(type)) {
              pizza = new GreekPizza();
          } else if ("pepperoni".equals(type)) {
              pizza = new PepperoniPizza();
          } else if ("veggie".equals(type)) {
              pizza = new VeggiePizza();
          }
            
          return pizza;
      }
    }
    
  • 클라이언트 코드에서 팩토리로 피자를 생성하도록 고쳐보기
    public class PizzaStore {
      PizzaFactory factory;
        
      public PizzaStore(PizzaFactory factory) {
          this.factory = factory;
      }
        
      public Pizza orderPizza(String type) {
          Pizza pizza;
            
          pizza = factory.createPizza(type);
            
          pizza.prepare();
          pizza.bake();
          pizza.cut();
          pizza.box();
          return pizza;
      }
        
      // 기타 메서드
    }
    

주의: “인터페이스를 구현한다”

  • 해당 책에서 “인터페이스를 구현한다”라는 표현이 나오는데, 항상 “클래스를 선언하는 부분에 implements 키워드로 어떤 자바 인터페이스를 구현하는 클래스를 만든다”라고 생각하지 말 것!
  • 일반적으로 상위 형식에 있는 구상 클래스는 그 상위 형식의 “인터페이스를 구현”하는 클래스라고 생각하면 된다.

다양한 팩토리 만들기

  • 다양한 스타일의 피자(뉴욕, 시카고, 캘리포니아 스타일)를 만들어야 한다.
  • 방법1. 3가지 서로 다른 팩토리를 만든 다음 PizzaStore에서 사용하도록 하는 방법 ```java // 뉴욕 스타일의 피자를 만드는 팩토리 사용 NYPizzaFactory nyFactory = new NYPizzaFactory(); PizzaStore nyStore = new PizzaStore(nyFactory); nyStore.orderPizza(“veggie”);

// 시카고 스타일의 피자를 만드는 팩토리 사용 ChicagoPizzaFactory chicagoFactory = new ChicagoPizzaFactory(); PizzaStore chicagoStore = new PizzaStore(chicagoFactory); chicagoStore.orderPizza(“veggie”);


* 지점들을 조금 더 제대로 관리할 순 없을까?
* 어떻게 피자 가게와 피자를 만드는 과정을 하나로 묶을 수 있을까?

### 피자 가게 프레임워크 만들기
* 팩토리의 `createPizza()` 메서드를 다시 PizzaStore 클래스에 다시 넣는다. 대신 해당 메서드를 추상 메서드로 선언하고 지역별로 서브클래스를 만든다.
```java
public abstract class PizzaStore {
    public Pizza orderPizza(String type) {
        Pizza pizza;
        
        pizza = createPizza(type);

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

서브클래스에서 결정할 것

  • PizzaStore 서브클래스들은 PizzaStore 프레임워크에 충실하면서도 각 지점마다 피자 스타일이 달라지도록 구현한다.
| PizzaStore    |
| ------------- |
| createPizza() |
| orderPizza()  |
  • 각 서브클래스는 createPizza() 메서드를 오버라이드하지만, orderPizza()는 PizzaStore에서 정의한 내용 그대로 사용한다.
  • 정의한 메서드를 고쳐 쓸 수 없게 하고 싶다면 orderPizza() 메서드를 final로 선언한다.
    | NYStylePizzaStore    |
    | -------------------- |
    | createPizza()        |
    
    public Pizza createPizza(type) {
      if ("cheese".equals(type)) {
          pizza = new NYStyleCheesePizza();
      } else if ("veggie".equals(type)) {
          pizza = nw NYStyleVeggiePizza();
      }
      // 그 외 피자 타입들 ...
    }
    
    | ChicagoStylePizzaStore |
    | ---------------------- |
    | createPizza()          |
    
    public Pizza createPizza(type) {
      if ("cheese".equals(type)) {
          pizza = new ChicagoStyleCheesePizza();
      } else if ("veggie".equals(type)) {
          pizza = nw ChicagoStyleVeggiePizza();
      }
      // 그 외 피자 타입들 ...
    }
    
  • 피자의 종류는 어떤 서브클래스를 선택했느냐에 따라 결정된다.

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

  • 프렌차이즈 형태로 사업하기
    • PizzaStore의 서브클래스를 만들고 지역별 특성에 맞게 createPizza() 메서드만 구현하면 된다.
  • 뉴욕 스타일 피자 서브클래스
    public class NYPizzaStore extends PizzaStore {
        
      // createPizza() 메서드는 PizzaStore에서 추상 메서드로 선언되었으므로 구상 클래스에서 반드시 구현해야 한다.
      Pizza createPizza(String type) {
          if ("cheese".equals(type)) {
              return new NYStyleCheesePizza();
          } else if ("veggie".equals(type)) {
              return new NYStyleVeggiePizza();
          } else return null;
      }
    }
    

팩토리 메서드 선언하기

  • 위 예제 코드에서 PizzaStore 클래스에 추상 메서드로 선언한 createPizza()가 팩토리 메서드
  • 구상 클래스 인스턴스를 만드는 일을 일련의 서브클래스가 처리하는 방식
  • abstract Product factoryMethod(String type)
    • 팩토리 메서드를 추상 메서드로 선언하여 서브클래스가 객체 생성을 책임지도록 한다.
    • 팩토리 메서드는 특정 객체를 반환하며, 그 객체는 보통 슈퍼클래스가 정의한 메서드 내에서 쓰인다.
    • 팩토리 메서드는 클라이언트에서 실제 생성되는 구상 객체가 무엇인지 알 수 없게 만드는 역할도 한다.
    • 팩토리 메서드를 만들 때 매개변수로 만들 객체 종류를 선택할 수도 있다.

Pizza 클래스 만들기

// 이 클래스를 확장하여 구상 클래스로 만들기 위해 추상 클래스로 만든다.
public abstract class Pizza {
    String name;
    String dough;
    String sauce;
    List<String> toppings = new ArrayList<>();
    void prepare() {
        System.out.println("준비 중: " + name);
        System.out.println("도우를 준비하는 중...");
        System.out.println("소스를 뿌리는 중...");
        System.out.println("토핑을 올리는 중...");
        for (String topping : toppings) {
            System.out.println(" " + topping);
        }
    }
    
    void bake() {
        System.out.println("175도에서 25분 간 굽기");
    }
    
    void cut() {
        System.out.println("피자를 6조각으로 자르기");
    }
    
    void box() {
        System.out.println("피자박스에 피자 담기");
    }
    
    public String getName() {
        return name;
    }
    
    // ...
}
  • Pizza 구상 서브클래스 만들기
    public class NYStyleCheesePizza extends Pizza {
      public NYStyleCheesePizza() {
          name = "뉴욕 스타일 치즈 피자";
          dough = "씬 크러스트 도우";
          sauce = "마리나라 소스";
            
          toppings.add("잘게 썬 레지아노 치즈");
      }
    }
    
    public class ChicagoStyleCheesePizza extends Pizza {
      public ChicagoStyleCheesePizza() {
          name = "시카고 스타일 딥 치즈 피자";
          dough = "두꺼운 크러스트 도우";
          sauce = "플럼토마토 소스";
            
          toppings.add("잘게 조각낸 모짜렐라 치즈");
      }
        
      void cut() {
          System.out.println("네모난 모양으로 피자 자르기");
      }
    }
    

피자 만들기 코드 테스트

public class PizzaTest {
	public static void main(String args[]) {
		PizzaStore nyStore = new NYStylePizzaStore();
		PizzaStore chicagoStore = new ChicagoStyleStore();
		
		Pizza pizza = nyStore.orderPizza("cheese");
		System.out.println(pizza.name + "\n");
		
		pizza = chicagoStore.orderPizza("cheese");
		System.out.println(pizza.name + "\n");
    }
}

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

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

생산자(Creator) 클래스

  • 추상 생산자 클래스
  • 서브클래스에서 객체를 생성하려고 구현하는 팩토리 메서드(추상 메서드)를 정의한다.
  • 생산자 자체는 어떤 구상 제품 클래스가 만들어질지 미리 알 수 없다.
    | PizzaStore    |
    | ------------- |
    | createPizza() |
    | orderPizza()  |
    
  • 제품을 생산하는 클래스: 구상 생산자 (concrete creator)
    | NYPizzaStore  |
    | ------------- |
    | createPizza() |
    
    | ChicagoPizzaStore |
    | ----------------- |
    | createPizza()     |
    

제품(Product) 클래스

  • 팩토리에서 생산되는 제품
    | Pizza |
    | ----- |
    

병렬 클래스 계층 구조

  • 구상 생산자별로 수많은 제품을 만들 수 있다.
    • 예로 뉴욕 피자 생산자는 여러 가지 뉴욕 스타일 피자를 만든다.
  • 생상자 클래스와 그에 대응되는 제품 클래스는 병렬 계층구조로 볼 수 있다.

팩토리 메서드 패턴의 정의

  • 팩토리 메서드 패턴에서는 객체를 생성할 때 필요한 인터페이스를 만든다.
  • 어떤 클래스의 인스턴스를 만들지는 서브클래스에서 결정한다.
  • 팩토리 메서드 패턴을 사용하면 클래스 인스턴스 만드는 일을 서브클래스에게 맡기게 된다.
  • 생산자 클래스는 실제 생산될 제품을 전혀 모르는 상태로 만들어진다.
  • 사용하는 서브클래스에 따라 생산되는 객체 인스턴스가 결정된다.

의존성 뒤집기 원칙

디자인 원칙 6

  • 추상화된 것에 의존하게 만들고 구상 클래스에 의존하지 않게 만든다.
    • “구현보다는 인터페이스에 맞춰서 프로그래밍한다”라는 원칙과 비슷하게 보일 수 있다.
    • 해당 원칙에서는 추상화를 더 강조한다.
    • 고수준 구성 요소가 저수준 구성 요소에 의존하면 안 되며, 항상 추상화에 의존하게 만들어야 한다는 뜻
  • 고수준 구성 요소
    • 다른 저수준 구성 요소에 의해 정의되는 행동이 들어있는 구성 요소
    • PizzaStore의 행동은 Pizza에 의해 정의되므로 PizzaStore는 고수준 구성 요소라고 할 수 있다.
    • PizzaStore에서 사용하는 Pizza 객체는 저수준 구성 요소
  • 의존성 뒤집기 원칙에 따르면, 구상 클래스처럼 구체적인 것이 아닌 추상 클래스나 인터페이스와 같이 추상적인 것에 의존하는 코드를 만들어야 한다.

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

  1. 변수에 구상 클래스의 레퍼런스를 저장하지 않는다.
    • new 연산자를 사용하면 구상 클래스의 레퍼런스를 사용하게 된다.
    • 그러니 팩토리를 사용하여 구상 클래스의 레퍼런스를 변수에 저장하는 일을 미리 방지한다.
  2. 구상 클래스에서 유도된 클래스를 만들지 않는다.
    • 구상 클래스에서 유도된 클래스를 만들면 특정 구상 클래스에 의존하게 된다.
    • 인터페이스나 추상 클래스처럼 추상화된 것으로부터 클래스를 만들어야 한다.
  3. 베이스 클래스에 이미 구현되어 있는 메서드를 오버라이드하지 않는다.
    • 이미 구현되어 있는 메서드를 오버라이드한다면 베이스 클래스가 제대로 추상화되지 않는다.
    • 베이스 클래스에서 메서드를 정의할 때는 모든 서브클래스에서 공유할 수 있는 것만 정의해야 한다. * 다른 원칙들과 마찬가지로, 항상 지켜야 하는 규칙이 아니라 우리가 지향해야 할 바를 알려주는 것이다. * 이런 가이드라인을 습득한 상태에서 디자인한다면 원칙을 지키지 않은 부분을 명확하게 파악할 수 있으며, 합리적인 이유로 불가피한 상황에서만 예외를 둘 수 있을 것이다.

피자 원재료군으로 묶기

  • 각 스타일의 피자마다 사용하는 재료는 서로 다르다.
  • 서로 다른 원재료를 제공하려면 원재료군을 처리할 방법을 생각해야 한다.

원재료 팩토리 만들기

  • 각 스타일마다 달라지는 부분을 처리하는 건 뒤에서 생각하고, 우선 원재료를 생산하는 팩토리를 만든다.
    public interface PizzaIngerdientFactory {
      public Dough createDough();
      public Sauce createSauce();
      public Cheese createCheese();
      public Veggies[] createVeggies();
      public Pepperoni createPepperoni();
      public Clams createClams();
    }
    

뉴욕 스타일 원재료 팩토리 만들기

  • 뉴욕 스타일 피자에는 마리나라 소스, 레지아노 치즈, 조개 등이 들어간다고 가정해본다.
    public class NYPizzaIngredientFactory implements PizzaIngredientFactory {
      public Dough createDough() {
          return new ThinCrushDough();
      }
        
      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 SlicePepperoni();
      }
        
      public Clams createClams() {
          return new FreshClams();
      }
    }
    

Pizza 클래스 변경하기

  • Pizza 클래스에서 팩토리로 원재료를 생성하여 사용하도록 수정해본다.
    public abstract class Pizza {
      String name;
        
      Dough dough;
      Sauce sauce;
      Veggies veggies[];
      Cheese cheese;
      Pepperoni pepperoni;
      Clams clams;
        
      // prepare() 메서드를 추상 메서드로 만듬
      // 이 부분에서 필요한 재료들을 가져온다. 원재료는 원재료 팩토리에서 가져온다.
      abstract void prepare();
        
      void bake() {
          System.out.println("175도에서 25분 간 굽기");
      }
        
      void cut() {
          System.out.println("피자를 6조각으로 자르기");
      }
        
      void box() {
          System.out.println("피자박스에 피자 담기");
      }
        
      void setName(String name) {
          this.name = name;
      }
        
      public String getName() {
          return name;
      }
        
      public String toString() {
          // 피자 이름 출력 부분
      }
    }
    
  • 이제 지역 스타일 별 피자를 만들기 위한 NYCheesePizzaChicagoCheesePizza 클래스는 더 이상 사용하지 않아도 된다.
    • 두 지역 스타일 피자를 비교해보면 다른 재료를 사용한다는 것만 빼면 똑같은 형식이다.
  • 즉 피자마다 지역별로 클래스를 따로 만들 필요가 없어졌다.
  • 치즈 피자를 만드는 예시 코드
    public class CheesePizza extends Pizza {
      PizzaIngredientFactory ingredientFactory;
        
      public CheesePizza(PizzaIngredientFactory ingredientFactory) {
          this.ingredientFactory = ingredientFactory;
      }
        
      void prepare() {
          System.out.println("준비 중: " + name);
          // 재료가 필요할 때마다 팩토리에 있는 메서드를 호출해 만든다.
          dough = ingredientFactory.createDough();
          sauce = ingredientFactory.createSauce();
          cheese = ingredientFactory.createCheese();
      }
    }
    
  • 조개 피자를 만드는 예시 코드
    public class ClamPizza extends Pizza {
      PizzaIngredientFactory ingredientFactory;
        
      public ClamPizza(PizzaIngredientFactory ingredientFactory) {
          this.ingredientFactory = ingredientFactory;
      }
        
      void prepare() {
          System.out.println("준비 중: " + name);
          dough = ingredientFactory.createDough();
          sauce = ingredientFactory.createSauce();
          cheese = ingredientFactory.createCheese();
          clam = ingredientFactory.createClam();
      }
    }
    

새로운 팩토리 코드로 피자 주문하기

새롭게 피자가 만들어지는 과정

  • 해당 예제에서는 뉴욕 스타일 피자를 만든다고 가정한다.
  1. 뉴욕 스타일 피자를 주문하기 위해선 우선 뉴욕 피자 가게가 필요하다.
    Pizza nyPizzaStore = new NYPizzaStore();
    
  2. 피자 가게에 주문을 한다.
    nyPizzaStore.orderPizza("cheese");
    
  3. orderPizza() 메서드는 먼저 createPizza() 메서드를 호출한다.
    Pizza pizza = createPizza("cheese");
    
  4. createPizza() 메서드가 호출되면 원재료 팩토리가 돌아간다.
    Pizza pizza = new CheesePizza(nyIngredientFactory);
    
  5. 피자를 준비하기 위해 prepare() 메서드를 호출하면 팩토리에 원재료가 들어간다.
    void prepare() {
     dough = factory.createDough();
     sauce = factory.createSauce();
     cheese = factory.createCheese();
    }
    

추상 팩토리 패턴의 정의

  • 추상 팩토리 패턴: 구상 클래스에 의존하지 않고도 서로 연관되거나 의존적인 객체로 이루어진 제품군을 생산하는 인터페이스를 제공, 구상 클래스는 서브클래스에서 만든다.
  • 추상 팩토리 패턴을 사용하면 클라이언트에서 추상 인터페이스로 일련의 제품을 공급받을 수 있다. 이 때, 실제로 어떤 제품이 생산되는지 전혀 알 필요가 없다.
    • 클라이언트와 팩토리에서 생산되는 제품을 분리할 수 있다.

팩토리 메서드 패턴과 추상 팩토리 패턴의 차이

팩토리 메서드 패턴

  • 객체 생성 방식이 상속을 사용하여 객체를 만든다.
    • 서브클래스로 객체를 만든다.
  • 한 가지 종류의 객체를 생성한다.
  • 단일 메서드를 통해 객체를 생성한다.

추상 팩토리 메서드

  • 객체 생성 방식이 객체 구성을 사용하여 객체를 만든다.
    • 제품군을 만드는 추상 형식을 제공하여, 제품이 생산되는 방법은 해당 형식의 서브클래스에서 정의한다.
  • 다량의 제품군을 생성할 때 사용한다.
  • 여러 메서드를 통해 여러 종류의 객체를 생성한다.