Don't give up!

[Design Pattern] 팩토리 패턴 (Factory Pattern) 본문

개발서적/Head First Design Patterns

[Design Pattern] 팩토리 패턴 (Factory Pattern)

Heang Lee 2021. 9. 20. 23:03
이 글은 에릭 프리먼의 'Head First Design Patterns'를 읽고 TIL(Today I Learned)로써 정리한 내용을 기록하는 글입니다.
자세한 내용은 책을 통해 확인하실 수 있습니다.

알라딘: Head First Design Patterns (aladin.co.kr)

 

Head First Design Patterns

볼 거리가 많고 재미있으면서도, 머리 속에 쏙쏙 들어오는 방식으로 구성된 Head First 시리즈. 패턴의 근간이 되는 객체지향 디자인 원칙, 중요한 패턴, 디자인 적용 방법, 쓰지 말아야 하는 이유

www.aladin.co.kr

문제 상황

위 그림과 같이 상속이 적용된 여러 클래스가 있다고 가정하겠습니다.

클래스들을 사용하고자 할 때 new 메소드를 사용하여 구상 클래스의 인스턴스를 만들어 사용하여야 합니다.

특정 서비스에서 사용 또는 반환되어야 할 클래스가 정해져 있지 않은 경우, 주어질 수 있는 조건을 고려하여 코드를 작성해야 합니다.

//주어진 입력 문자열 input을 통해 적절한 클래스 인스턴스를 생성, 사용하는 서비스 메소드
public void doService(String input){
  Duck duck;
  
  if (input.equals("picnic")) {
    duck = new MallardDuck();
  } else if (input.equals("hunting")) {
    duck = new DecoyDuck();
  } else if (input.equals("inBathTub")) {
    duck = new RubberDuck();
  }
  
  //Duck 인스턴스를 사용
  .....
}

위의 코드는 비즈니스 로직을 수행하지만 다음의 문제점을 갖고 있습니다.

  1. 코드가 변경될 때 마다 인스턴스를 생성하는 부분을 찾아 수정해야 한다.
  2. 인스턴스를 사용하여 작업을 수행하는 서비스 메소드에서 '인스턴스를 생성한다'는 책임이 추가된다.
  3. 다른 서비스에서 같은 클래스의 인스턴스를 사용하는 상황이 발생할 경우 코드가 중복될 수 있다.

이러한 문제점을 해결하기 위해 인스턴스 생성에 대한 책임을 별도의 공간(팩토리 클래스)으로 옮기는 디자인 패턴인  팩토리 메소드 패턴이 등장합니다.

팩토리 메소드 패턴 (Factory Method Pattern)

팩토리 메소드 패턴은 인터페이스를 정의하여 인스턴스를 만드는 일을 서브클래스에게 맡기는 디자인 패턴입니다.

추상 클래스 Creator의 operator 메소드에서 추상 메소드 factoryMethod를 호출하고, 이를 구현하는 클래스를 작성함으로써 Product를 생성하는 코드를 캡슐화할 수 있습니다.

인스턴스 생성에 대한 변경이 발생하더라도 팩토리 클래스를 찾아 변경할 수 있고, 인스턴스를 생성할 때 구상 클래스가 아닌 인터페이스만을 필요로 하므로 유연성과 확장성이 뛰어난 코드를 만들 수 있습니다.

//주어진 입력 문자열 input을 팩토리에 넘겨 인스턴스를 받고, 사용하는 서비스 메소드
public void doService(String input){
  DuckFactory duckFactory = new ConcreteDuckFactory();
  Duck duck = duckFactory.createDuck(input);
  
  //Duck 인스턴스를 사용
  //.....
}

//Creator
public abstract class DuckFactory{
  public Duck createDuck(String type){
    Duck duck = makeDuck(type);    
    return duck;
  }
  protected abstract Duck makeDuck(String type);
}

//ConcreteCreator
public class ConcreteDuckFactory extends DuckFactory{
    Duck makeDuck(String type){
       if (input.equals("picnic")) {
         duck = new MallardDuck();
       } else if (input.equals("hunting")) {
         duck = new DecoyDuck();
       } else if (input.equals("inBathTub")) {
         duck = new RubberDuck();
       }
    }
}

문제 상황 2

이번에는 클라이언트가 여러 Product들을 ProductA1-ProductB1과 같은 조합으로 has-a 관계를 맺는다고 하겠습니다.

기존의 팩토리 메소드 패턴은 Product 하나의 인스턴스를 생성하는 책임만을 담당하고 있었습니다.

이러한 케이스에서 팩토리 메소드 패턴을 사용할 경우 팩토리에서 클라이언트 인스턴스를 생성해야 하므로 구상 클라이언트들의 내부 구조에 대해 알아야 합니다. 이는 팩토리가 구상 클라이언트에 의해 팩토리의 코드가 변화할 수 있음을 의미하며 의존관계 역전 원칙, DIP(Dependency Injection Principle)를 위반합니다.

추상 팩토리 패턴은 이러한 상황에 알맞게 적용되며 객체지향원칙을 지킬 수 있도록 합니다.

추상 팩토리 패턴

추상 팩토리 패턴은 인터페이스를 이용하여 서로 연관되고 의존하는 객체를 구상 클래스를 지정하지 않고 생성할 수 있는 디자인패턴입니다.

관련이 있는 객체들을 묶어 팩토리 클래스로 만들고, 팩토리 클래스를 사용하여 구성(Composition)이 완료된 객체를 얻을 수 있어 클라이언트와 실제 구상 제품을 분리할 수 있습니다.

팩토리 자체가 추상화되어 있기 때문에 필요로 하는 객체들을 생성하는 팩토리 클래스를 작성하고, 클라이언트에서 해당 팩토리 인스턴스를 사용하는 것만으로 원하는 객체들을 얻을 수 있습니다.

//추상화된 클라이언트
public abstract class Pizza {
  PizzaIngredientFactory ingredientFactory;
  String name;
  Dough dough;
  Sauce sauce;
  //....
  
  public Pizza(PizzaIngredientFactory ingredientFactory){
    this.ingredientFactory = ingredientFactory;
  }
  
  abstract void prepare();
  
  //구성 객체 set함수
  //....
}

//구상 클라이언트
public class CheesePizza extends Pizza {
  public CheesePizza(PizzaIngredientFactory ingredientFactory){
    super(ingredientFactory);
    //...
  }
  
  void prepare() {
    //CheesePizza의 동작
  }
}

public class ClamPizza extends pizza {
  public ClamPizza(PizzaIngredientFactory ingredientFactory){
    super(ingredientFactory);
    //...
  }
  
  void prepare() {
    //ClamPizza의 동작
  }
}

//Pizza의 Product들을 반환하는 PizzaIngredientFactory
public interface PizzaIngredientFactory {
  public Dough createDough();
  public Sauce createSauce();
  //.....
}

public class NYPizzaIngredientFactory implements PizzaIngredientFactory {
  public Dough createDough(){
    return new ThinCrustDough();
  }
  
  public Sauce createSauce(){
    return new MarinaraSauce();
  }
  //...
}

//서비스 PizzaStore
public abstract class PizzaStore {
  public Pizza orderPizza(String item) {
    Pizza pizza = createPizza(item);
    
    //pizza의 주문에 필요한 코드
    //...
    
    return pizza;
  }

  protected abstract Pizza createPizza(String item);
  //...
}

public class NYPizzaStore extends PizzaStore {
  protected Pizza createPizza(String item){
    Pizza pizza = null;
    PizzaIngredientFactory ingredientFactory = new NYPizzaIngredientFactory();
    
    if(item.equals("cheese")) {
      pizza = new CheesePizza(ingredientFactory);
      //CheesePizza set함수 호출
    }else if (item.euqals("clam")) {
      pizza = new ClamPizza(ingredientFactory);
      //ClamPizza set함수 호출
    }
    
    return pizza;
  }
}

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

두 패턴은 팩토리 클래스를 사용하고, 인스턴스 생성에 대한 부분을 캡슐화한다는 공통점을 가지고 있지만 다음의 차이점을 보이고 있습니다.

1. 인스턴스를 생성하는 방식

팩토리 메소드 패턴은 상속을 통해 인스턴스를 생성합니다. Creator 클래스를 확장하고, factoryMethod를 오버라이드함으로써 클라이언트는 자신이 사용할 추상 형식만을 알아도 되도록 합니다.

추상 팩토리 패턴은 객체 구성을 통해 인스턴스를 생성합니다. 구상 팩토리 인스턴스를 만들고 추상 형식으로 팩토리를 사용하는 코드에 전달함으로써 클라이언트와 실제 구상 제품을 분리합니다. 

2. 팩토리가 생성하는 인스턴스의 종류

팩토리 메소드 패턴은 하나의 create 메소드만을 사용합니다. 팩토리가 Product를 반환해야 하므로 Product에 대한 초기 설정은 팩토리의 create 메소드 내부에서 결정됩니다.

추상 팩토리 패턴은 클라이언트가 추상화된 팩토리를 소유하고 팩토리로부터 Product들을 받아 사용하므로 여러 create 메소드를 사용할 수 있습니다. 또한 클라이언트에 대한 초기 설정은 팩토리가 아닌 클라이언트에서 결정됩니다.

3. 팩토리의 목적

팩토리 메소드 패턴은 클라이언트 코드와 인스턴스를 만들어야 할 구상 클래스를 분리시켜야 할 때 사용합니다. Creator의 서브 클래스를 만들고 팩토리 메소드를 구현하기만 하면 사용할 수 있으므로 Creator 코드를 작성하는 과정에서 어떤 구상 클래스를 필요로 하게 될지 알 수 없는 경우 유용합니다.

추상 팩토리 패턴은 클라이언트에서 서로 연관된 일련의 제품들(제품군)을 만들어야 할 때 사용합니다. 클라이언트의 구현 세부 사항을 노출하지 않고 라이브러리를 제공하고자 할 때 유용합니다.