Java/객체지향

팩토리 메소드 디자인 패턴

재심 2022. 11. 26. 11:46

[팩토리 메소드 디자인 패턴이란]

  • 객체지향 디자인 패턴중 하나.
  • 상위클래스에 알려지지 않은 구체화된 클래스를 생성하는 패턴이며, 하위클래스가 구체화된 클래스를 생성하도록 하는 것.
  • 기존 코드를 수정하지 않고, 새로운 인스턴스를 여러방법으로 생성할 수 있는 "확장에 열려있고 변경에 닫혀있는 객체 지향 원칙"을 지킬 수 있는 패턴.
  • 이러한 원칙을 만족하는 이유는 제품과 제품을 생성하는 팩토리가 느슨한 결합 구조를 가질 수 있기 때문.
  • 추상화와 다형성을 통해 이를 실현한다.
  • 동일한 인터페이스를 통해 구현된 객체들은 다형성을 이용하면 코드의 수정을 최소화할 수 있기 때문에 느슨한 결합을 가져갈 수 있는 것.

[코드 수정이 최소화  될 수 있는 이유]

  • 객체를 생성하는 코드 부분을 분리 시켰기 때문에 객체를 추가
  • 수정이 일어 나더라도 객체를 생성하는 코드만 수정하면 된다.
  • 새로운 객체는 언제든 추가/삭제(메뉴가 후라이드 치킨만 있다가, 양념치킨이 추가될 수 있음)될 수 있는데, 이러한 코드들을 if/else로 작성했다고 가정했을 때 여러 클래스에서 객체를 만들어서사용한다면 새로운 객체가 추가되었을 때 모든 부분을 변경해주어야 한다.
  • 팩토리 메소드 패턴을 통해 객체의 정의부분과 객체를 생성하는 부분을 분리하여 이러한 문제점을 해결하는 방식.

 

[예제 : 피자가게]

PizzaStore

Product: Pizza, Create: PizzaStore

  • Creator: 피자를 만드는 지점 (여러 지점이 있을 수 있고, 지점은 그 지역의 특성에 맞게 피자를 만들 수도 있어야 한다)
  • Product: 피자 (다양한 종류의 피자가 있을 수 있다.)

배경설명

"재심"이라는 피자 스토어는 "재심 피자"와 "재심 페퍼 피자"를 시그니쳐로 판매하고 있다.

아직까지 현재 지점은 1개 밖에 없다..

 

UML 다이어그램

 

Pizza

//기본적인 피자
abstract public class Pizza {
  String name;
  String dough;
  String sauce;

  public Pizza(String name) {
    this.name = name;
  }

  void prepare() {
    System.out.println("피자 준비" + name);
  }

  void bake() {
    System.out.println("피자 굽기");
  }

  void box() {
    System.out.println("피자 포장");
  }

  public String getName() {
    return name;
  }
}

Jaeshim Pizza

//재심피자
public class JaeshimPizza extends Pizza{

  public JaeshimPizza() {
    super("재심 피자");
  }
}
//재심 페퍼피자
public class JaeshimPepperPizza extends Pizza{

  public JaeshimPepperPizza() {
    super("재심 페퍼 피자");
  }
}

PizzaType

public enum PizzaType {
  JAESHIM, JAESHIM_PEPPER
}

SimpleFactory로 구현 + 문제점

PizzaStore

public class PizzaStore {

  private Pizza createPizza(PizzaType pizzaType){
    if(pizzaType.equals(PizzaType.JAESHIM)){
      return new JaeshimPizza();
    }else{
      return new JaeshimPepperPizza();
    }
  }

  public Pizza orderPizza(PizzaType pizzaType){
    Pizza pizza = createPizza(pizzaType);

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

    return pizza;
  }
}

 

피자 주문하기

public class PizzaOrderApplication {
  public static void main(String[] args){

    PizzaStore pizzaStore = new PizzaStore();
    //재심 피자 주문
    pizzaStore.orderPizza(PizzaType.JAESHIM);
    //재심 페퍼 피자 주문
    pizzaStore.orderPizza(PizzaType.JAESHIM_PEPPER);
  }
}

//실행결과
재심 피자준비
피자 굽기
피자 포장
------
재심 페퍼 피자준비
피자 굽기
피자 포장

현재 지점을 운영하는데 아무 이슈가 없다. 주문이 오는대로 피자를 만들어주면 된다.

 

SimpleFactory의 한계

... 시간이 흘러 장사가 잘되서 한국, 일본에서 지점을 내서 하고 싶다는 제의가 왔다.

그리고 각 지역별 특징을 살린 새로운 메뉴도 각각 개발되었다.

 

한국

  • 김치 피자
  • 비빔밥 피자

 

일본

  • 소바 피자
  • 라멘 피자

 

KoreanPizza

//김치 피자
public class KoreanKimchiPizza extends Pizza{

  public KoreanKimchiPizza(){
    super("한국 김치 피자");
  }

  @Override
  void prepare() {
    super.prepare();
    System.out.println("김치를 첨가한다");
  }
}

//비빔밥 피자
public class KoreanBibimbabPizza extends Pizza{

  public KoreanBibimbabPizza() {
    super("한국 비빔밥 피자");
  }

  @Override
  void prepare() {
    super.prepare();
    System.out.println("비빔밥 재료를 투하한다");
  }
}

JapanPizza

//소바 피자
public class JapanSobaPizza extends Pizza{

  public JapanSobaPizza() {
    super("일본 소바 피자");
  }

  @Override
  void prepare() {
    super.prepare();
    System.out.println("소바를 첨가한다");
  }
}

//라멘 피자
public class JapanRamenPizza extends Pizza{

  public JapanRamenPizza() {
    super("일본 라멘 피자");
  }


  @Override
  void prepare() {
    super.prepare();
    System.out.println("라멘을 투하한다");
  }
}

PizzaType

public enum PizzaType {
  JAESHIM, JAESHIM_PEPPER,
  KOREAN_KIMCHI, KOREAN_BIBIMBAB,
  JAPAN_SOBA,JAPAN_RAMEN
}

피자 신메뉴가 개발되었으니 레시피를 각 지점장들에게 전수해주고, 지점은 알아서 세우고 운영하라고 했다.

한국 피자스토어와 일본 피자스토어를 새로 런칭했다.

//한국지점
public class KoreanPizzaStore {

  private Pizza createPizza(PizzaType pizzaType){
    if(pizzaType.equals(PizzaType.JAPAN_SOBA)){
      return new JapanSobaPizza();
    }else if(pizzaType.equals(PizzaType.JAPAN_RAMEN)){
      return new JapanRamenPizza();
    }else if(pizzaType.equals(PizzaType.JAESHIM)){
      return new JaeshimPizza();
    }else{
      return new JaeshimPepperPizza();
    }
  }

  public Pizza orderPizza(PizzaType pizzaType){
    Pizza pizza = createPizza(pizzaType);

    pizza.prepare();
    pizza.bake();

    return pizza;
  }
}
//일본지점
public class JapanPizzaStore {
  private Pizza createPizza(PizzaType pizzaType){
    if(pizzaType.equals(PizzaType.JAPAN_SOBA)){
      return new JapanSobaPizza();
    }else if(pizzaType.equals(PizzaType.JAPAN_RAMEN)){
      return new JapanRamenPizza();
    }else if(pizzaType.equals(PizzaType.JAESHIM)){
      return new JaeshimPizza();
    }else{
      return new JaeshimPepperPizza();
    }
  }

  public Pizza orderPizza(PizzaType pizzaType){
    Pizza pizza = createPizza(pizzaType);

    pizza.bake();
    pizza.box();

    return pizza;
  }
}

그런데.. 런치 후에 고객들이 반응이 좋지않다. 대체 어떻게 된 일일까?

위의 코드를 자세히 보니 지점장들이 피자 주문을 받을 때 하나씩 빼먹고 있었다.

한국은 boxing을 안하고 있었고, 일본은 prepare를 안하고 있었던 것..!

 

그렇다면 어떻게 해야할까?

본점의 주문과정을 그대로 유지할 수 있게 (공통 부분은 유지)하고, 피자 만드는 과정만 각 지점의 특성에 맞도록 한다.

UML 다이어그램

 

PizzaStore

abstract 클래스로 선언하고, createPizza를 알아서 구현하도록 한다.

orderPizza는 공통 프로세스를 타도록 한다.

//PizzaStore
public abstract class PizzaStore {

  protected abstract Pizza createPizza(PizzaType pizzaType);

  public Pizza orderPizza(PizzaType pizzaType){
    Pizza pizza = createPizza(pizzaType);

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

    return pizza;
  }

}

//KoreanPizzaStore
public class KoreanPizzaStore extends PizzaStore{

  @Override
  protected Pizza createPizza(PizzaType pizzaType){
    if(pizzaType.equals(PizzaType.KOREAN_KIMCHI)){
      return new KoreanKimchiPizza();
    }else if(pizzaType.equals(PizzaType.KOREAN_BIBIMBAB)){
      return new KoreanBibimbabPizza();
    }else if(pizzaType.equals(PizzaType.JAESHIM)){
      return new JaeshimPizza();
    }else{
      return new JaeshimPepperPizza();
    }
  }
}


//JapanPizzaStore
public class JapanPizzaStore extends PizzaStore{
  @Override
  protected Pizza createPizza(PizzaType pizzaType) {
      if(pizzaType.equals(PizzaType.JAPAN_SOBA)){
        return new JapanSobaPizza();
      }else if(pizzaType.equals(PizzaType.JAPAN_RAMEN)){
        return new JapanRamenPizza();
      }else if(pizzaType.equals(PizzaType.JAESHIM)){
        return new JaeshimPizza();
      }else{
        return new JaeshimPepperPizza();
      }
  }
}

피자주문

public class PizzaOrderApplication {
  public static void main(String[] args){
    //한국
    PizzaStore kPizzaStore = new KoreanPizzaStore();
    //비빔밥 피자 주문
    kPizzaStore.orderPizza(PizzaType.KOREAN_BIBIMBAB);
    System.out.println("------");
    //김치 피자 주문
    kPizzaStore.orderPizza(PizzaType.KOREAN_KIMCHI);

    System.out.println("===================================");
    //일본
    PizzaStore jPizzaStore = new JapanPizzaStore();
    //소바 피자 주문
    jPizzaStore.orderPizza(PizzaType.JAPAN_SOBA);
    System.out.println("------");
    //라멘 피자 주문
    jPizzaStore.orderPizza(PizzaType.JAPAN_RAMEN);
  }

}

//실행결과
public class PizzaOrderApplication {
  public static void main(String[] args){
    //한국
    PizzaStore kPizzaStore = new KoreanPizzaStore();
    //비빔밥 피자 주문
    kPizzaStore.orderPizza(PizzaType.KOREAN_BIBIMBAB);
    System.out.println("------");
    //김치 피자 주문
    kPizzaStore.orderPizza(PizzaType.KOREAN_KIMCHI);

    System.out.println("===================================");
    //일본
    PizzaStore jPizzaStore = new JapanPizzaStore();
    //소바 피자 주문
    jPizzaStore.orderPizza(PizzaType.JAPAN_SOBA);
    System.out.println("------");
    //라멘 피자 주문
    jPizzaStore.orderPizza(PizzaType.JAPAN_RAMEN);
  }

}

//실행결과
한국 비빔밥 피자준비
비빔밥 재료를 투하한다
피자 굽기
피자 포장
------
한국 김치 피자준비
김치를 첨가한다
피자 굽기
피자 포장
===================================
일본 소바 피자준비
소바를 첨가한다
피자 굽기
피자 포장
------
일본 라멘 피자준비
라멘을 투하한다
피자 굽기
피자 포장

 

[팩토리 메서드 패턴의 특징]

  • 만약 피자를 만드는 곳이 스토어 중심이 아닌 여기저기 흩뿌려져있으면 변경되었을 때 피자 레시피가 변경되었을 때 모두 찾아서 수정해주어야 한다. 팩토리메서드를 이를 해결하고자 했다.
  • 객체를 만드는 "팩토리"를 만듦으로써 객체를 생성하는 코드 부분을 한 곳에서 관리할 수 있게 된다.
  • 동일한 인터페이스를 기반으로 만들어지기 때문에 새로운 개체를 생성할 경우에도 새롭게 하위클래스를 정의해서 구현하기만 하면 된다.
  • 새로운 개체를 만들 필요가 있을 경우 계속해서 하위클래스를 만들어야하고, 중첩되면서 상속은 하지만 확장은 하지않는 등 잘못된 사용케이스가 나오거나 복잡해질 수 있다.

 

[참조]

https://biggwang.github.io/2019/06/28/Design%20Patterns/%5BDesign%20Patterns%5D%20%ED%8C%A9%ED%86%A0%EB%A6%AC%20%ED%8C%A8%ED%84%B4,%20%EB%8F%84%EB%8C%80%EC%B2%B4%20%EC%99%9C%20%EC%93%B0%EB%8A%94%EA%B1%B0%EC%95%BC-%EA%B8%B0%EB%B3%B8%20%EC%9D%B4%EB%A1%A0%ED%8E%B8/

 

biggwang의 개발 블로그

백문이불여일견

biggwang.github.io

https://kangworld.tistory.com/233

 

[객체 생성 패턴] Chapter 2-4. Factory Method Pattern : 장단점

[객체 생성 패턴] Chapter 2-3. Factory Method Pattern : 인터페이스 적용하기 ✍️ 팩토리 메서드 패턴, 장단점 팩토리 메서드 패턴을 사용했을 때의 장점과 단점 팩토리 메서드 패턴의 장점으로는 지금

kangworld.tistory.com

https://ko.wikipedia.org/wiki/%ED%8C%A9%ED%86%A0%EB%A6%AC_%EB%A9%94%EC%84%9C%EB%93%9C_%ED%8C%A8%ED%84%B4

 

팩토리 메서드 패턴 - 위키백과, 우리 모두의 백과사전

위키백과, 우리 모두의 백과사전.

ko.wikipedia.org