4. 팩토리 매소드 패턴 (Factory Method Pattern)

Kim Dong Kyun·2023년 6월 25일
4

Design Pattern

목록 보기
4/5
post-thumbnail

썸네일 이미지 링크

모든 코드는 깃허브에 있습니다.


들어가기 전에!

의존성 역전 (Dependency Inversion)?

"추상화된 것에 의존하게 만들고, 구상 클래스에 의존하지 않게 한다"

그 방법은?

  1. 변수에 구상 클래스의 레퍼런스를 저장하지 않는다.
  • new 연산자를 사용하면 구상 클래스의 레퍼런스를 사용하게 된다. 그러니 팩토리를 써서 구상 클래스의 레퍼런스를 변수에 저장하는 일을 방지해주자
  1. 구상 클래스에서 유도된 클래스를 만들지 말자
  • 즉, "특정 구상 클래스"(구현체)에 유도된(의존하는) 클래스를 만들지 말자.
    구상 -> 구상 이아닌
    추상 -> 구상 이어야 한다.
  1. 상위 클래스에 이미 구현된 매소드는 오버라이드 하지 말자
  • 미완인 매소드(추상 매서드) 말고, 완성된 매서드는 오버라이드 하지 말자.

-> 가이드 일 뿐이지만, 지키라고 하는 이유를 다시 한번 생각해보자.


예전에 한 것 같은데?

이전에 내가 올렸던 팩토리 패턴은 자바/스프링을 통해서 "간단한 팩토리"를 구현했다.

오늘은 좀 더 나아가서, 간단한 팩토리를 다시 한번 정리하고 좀 더 거대한 녀석으로 만들어 보자.

팩토리 패턴은, 객체의 생성 부분을 캡슐화해서 상위 추상이 아닌 구상 클래스 그 자체가 생성을 수행하는 패턴이다.
(이 때, 상위 추상은 어떤 구현체로 생성되는 지 "몰라"도 된다는 점이 포인트.)


피자 주문 시스템 만들기!

피자 가게를 운영해봅시다!

  • 최첨단 피자가게, 코드로 운영합니다!
public class OrderPizza {
    Pizza orderPizza(){
        Pizza pizza = new Pizza();
        
        pizza.prepare();
        pizza.bake();
        pizza.cut();
        pizza.box();
        
        return pizza;
    }
}
  • 그런데, 피자 종류는 하나가 아니다! 따라서 좀 더 다양하게 만들어 보자.

  • if / else 문을 사용해서 분기를 타면 좋겠네!

public class OrderPizza {
    Pizza orderPizza(String type){
        Pizza pizza = null;
        
        if (type.equals("cheese")){
            pizza = new CheesePizza();
        } else if(type.equals("peperoni")){
            pizza = new PeperoniPizza();
        } else {
            throw new NullPointerException("그런 피자는 없어요!");
        }

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

        return pizza;
    }
}
  • 오! 이제 피자를 CheesePizza, PeperoniPizza 등 여러 타입으로 생성 가능하구나!

  • 추상 클래스인 pizza, 그리고 그 구현체들인 CheesePizza, PeperoniPizza 를 각각 처리 할 수 있다!


맞은 편 가게에는 파인애플 피자도 있던데, 여긴 안파나요?

  • 당연히 추가해야겠다고 생각한 당신, 코드는 다음과 같이 변경되었다.
	...(이전 코드들)
    
    Pizza orderPizza(String type){
        Pizza pizza = null;

        if (type.equals("cheese")){
            pizza = new CheesePizza();
        } else if(type.equals("peperoni")){
            pizza = new PeperoniPizza();
        } else if (type.equals("pineapple")) { // 새로 추가됨!
            pizza = new PineapplePizza();
        } else {
            throw new NullPointerException("그런 피자는 없어요!");
        }

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

        return pizza;
    }
  • 어? 그런데 이렇게하면 OrderPizza 클래스의 매서드에 매번 피자가 새로 생길때마다 코드의 변경이 생긴다!!!

  • 오더피자의 코드는 변경하지 않고, 피자를 새로 만들 수 있는 방법은 없을까?


    일단, 책임을 분리해보자!

  • 분기문을 타는 부분(if/else)이 아무래도 마음에 걸린다. "생성"의 역할을 떼서 다른 곳으로 빼버리면 좋겠다!

  • 그럼, 피자를 선별해주는 공장 클래스를 만들어서 처리해보자!

1. 공장 클래스(Factory)

public class SimplePizzaFactory {
    public Pizza createPizza(String type){
        Pizza pizza = switch (type) {
            case "cheese" -> new CheesePizza();
            case "peperoni" -> new PeperoniPizza();
            case "pineapple" -> new PineapplePizza();
            default -> throw new NoSuchElementException("그런 피자는 없어요!");
        };
        
        return pizza;
    }
}
  • 간지나게 switch문을 통해서 처리했다.
  • 객체의 생성은 여기에서 분기문을 통해서 이루어진다.
  • 이전 코드에서의 "객체의 생성" 역할을 팩토리 클래스가 위임받은 식.

2. orderPizza 매서드의 변화

public class PizzaStore {
    SimplePizzaFactory simplePizzaFactory;
    public PizzaStore(SimplePizzaFactory simplePizzaFactory) {
        this.simplePizzaFactory = simplePizzaFactory;
        // 생성자에 아규먼트로 받아서 생성 매서드가 이루어진다.
    }
    
    public Pizza orderPizza(String type){
        Pizza pizza;
        pizza = simplePizzaFactory.createPizza(type);

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

        return pizza;
    }
}
  • 이제 가게는 orderPizza매서드를 위와 같이 바꿀 수 있다.

위 다이어그램에서는 Pizza 의 구현체들만 화살표로 나와있지만, 참조 관계는

    1. PizzaStore -> SimplePizzaFactory : 피자 만들어줘
    1. SimplePizzaFactory -> Pizza : 실제 어느 피자인지 분기문을 통해 파악
  • 핵심은, 객체의 생성을 Factory 클래스에 위임한것.

위 작업이 바로 "간단한 팩토리" 라고 명명된 방식이다.


그리고...

간단한 팩토리를 통해서 똑똑하게 피자를 생산한 결과 큰 돈을 벌어서, 프랜차이즈 사업을 시작하기로 했다.

  • 뉴욕 피자 팩토리, 시카고 피자 팩토리, 캘리포니아 피자 팩토리를 나눠서 세 가지 스타일 피자를 판매해보자

  • PizzaStore 에서 적당한 팩토리를 사용하면 될 것 같다!

NYPizzaFactory nyFac = new NYPizzaFactory(); // 뉴욕피자 팩토리
PizzaStore nyStore = new PizzaStore(nyFac); // pizzaStore 생성을 위해 팩토리 주입
nyStore.orderPizza("vegie"); // 아규먼트로 type 전달

오! 이런식으로 계속 뭔가를 추가 할 수 있을 거 같은데요?

  • 그런데, 지점에서 우리가 만든 팩토리로 피자를 굽긴 하는데, 완전히 지네 맘대로 하고 있다!(피자를 자르지 않거나, 포장하지 않거나, 굽지 않은 피자를 리턴하는 등)

  • 그냥 PizzaStore 에서 피자를 생산하는 일은 다 하고, 지점의 스타일(세부사항)을 살릴 수 있는 방법은 없을까?

  • 즉, 핵심 로직은 공유하면서 유연한 확장이 가능할 순 없을까?


Pizza 클래스의 생성을 유연하게 조정해보기

1. PizzaStore -> 추상으로 돌리기

public abstract class PizzaStore {
    abstract Pizza createPizza(String type);
    // 추상 매서드로 createPizza 선언! 이부분이 지점마다의 차이를 만들 것이다.

    public Pizza orderPizza(String type){
        Pizza pizza;
        pizza = createPizza(type);

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

        return pizza;
    }
}
  • 추상 매서드 createPizza()를 사용해서, 하위 팩토리들은 모두 createPizza() 매서드를 구현하도록 강제한다.

  • 이를 통해서 하위 팩토리들은 자신만의 필드,매서드를 가지면서도, "Pizza"라는 객체를 리턴하는 매서드를 구현 할 수 있다. 즉, 유연하게 생성이 가능하다

2. 서브클래스 - NYPizza, CHPizza

public class NYPizzaStore extends PizzaStore{
    @Override
    Pizza createPizza(String type) {
        Pizza pizza;
        if (type.equals("NYCheese")){
            pizza = new NYCheesePizza();
        } else if (type.equals("NYPeperoni")){
            pizza = new NYPeperoniPizza();
        } else {
            throw new NoSuchElementException("그런 건 없어요!");
        }
        return pizza;
    }
}
public class ChicagoPizzaStore extends PizzaStore{
    @Override
    Pizza createPizza(String type) {
        Pizza pizza;
        if (type.equals("CHCheese")){
            pizza = new NYCheesePizza();
        } else if (type.equals("CHPeperoni")){
            pizza = new NYPeperoniPizza();
        } else {
            throw new NoSuchElementException("그런 건 없어요!");
        }
        return pizza;
    }
}
  • 위와 같이 뉴욕, 시카고 피자 스토어를 선언한다.

  • 각각 적절한 타입이 들어왔을 때 어떤 객체를 리턴 할 지 구성 가능하다.

스토어에 입장에서 흐름 정리

public abstract class PizzaStore {
    abstract Pizza createPizza(String type);
    // 피자 인스턴스 만드는 일은 팩토리 매소드에서 알아서 처리함
    public Pizza orderPizza(String type){
        Pizza pizza;
        pizza = createPizza(type);
		// pizza 가 어떤 타입인진 몰라도 됨
        // 이전 코드 : Pizza pizza = new NYStylePizza();
        pizza.prepare();
        pizza.bake();
        pizza.cut();
        pizza.box();

        return pizza;
    }
}
  • createPizza() 부분에 적절한 아규먼트만 넣어준다면(type), PizzaStore 입장에서는 "어떤 클래스를 사용해 만든 피자객체"인지 알 필요가 없다.

  • 이제 피자 스토어는 구현체의 인스턴스를 만들기 위한 코드의 변경이 없이, 심지어 어떤 타입인지 알지 못하더라도 상관 없이 새 인스턴스를 반환해 줄 수 있다.

그럼, 같이 테스트 해보자


테스트 해보기

추상 클래스 "Pizza"

public abstract class Pizza {
    String name;
    String dough;
    String sauce;
    List<String> toppings = new ArrayList<>();
    public void prepare(){
        System.out.println("준비 중 : " + name);
        System.out.println("도우를 돌리는 중...");
        System.out.println("소스 뿌리는 중");
        System.out.println("토핑을 올리는 중: ");
        for (String topping : toppings) {
            System.out.println("토핑 : " + topping);
        }
    }

    public void bake(){
        System.out.println("175도에서 25분간 굽기");
    }

    public void cut(){
        System.out.println("피자를 사선으로 자르기");
    }

    public void box(){
        System.out.println("상자에 피자 담기");
    }
    
    public String getName(){
        return this.name;
    }
}
  • 여기서 추상 매서드로 선언한 녀석들이 이 추상의 핵심 로직이라고 할 수 있음.

2. 구현체 (구상 클래스)

public class NYCheesePizza extends Pizza{
    public NYCheesePizza() {
        name = "뉴욕 스타일 소스와 치즈 피자";
        dough = "씬 크러스트 도우";
        sauce = "마리나라 소스";
        
        toppings.add("잘게 썬 파마산 치즈");
    }
}
public class ChicagoCheesePizza extends Pizza{
    public ChicagoCheesePizza() {
        name = "시카고 딥 디쉬 피자";
        dough = "아주 두꺼운 크러스트 도우";
        sauce = "플럼 토마토 소스";
        
        toppings.add("잘게 조각낸 모짜렐라 치즈");
    }
    
    @Override
    public void cut() {
        System.out.println();
    }
}
  • 위와 같이 필드에 대해서 각자 구현한 구상 클래스들이다.

  • 추상의 핵심 매서드인 cut 매서드도 오버라이드해서 사용이 가능하다 -> 유연성!

3. 테스트 코드 및 결과


정리

1. 팩토리 패턴?

어떤 클래스의 인스턴스를 만들지 서브클래스에서 결정하게 하는 것.

2. 장점?

  1. 유연성
  • 클라이언트 측에서는 객체 생성 방법에 신경쓰지 않고, 생성에 필요한 방법만 (위예시의 "type") 알면 된다.
  1. 확장성
  • 새 객체를 추가하기 위해서 기존 코드를 수정하지 않아도 된다.
  1. 추상화
  • 생성에 관련된 복잡한 로직을 캡슐화 가능하다

즉, 변경되는 부분을 분리해서 캡슐화 + 유연하게 확장 가능하게 한다

3. 단점?

복잡성 : 추가적 클래스, 팩토리 매서드 패턴에서 제공하는 새 "방법"(=type 같은)들이 복잡성을 증가시킬 가능성이 있음.

++ 제품 객체의 갯수마다 공장 서브 클래스를 모두 구현해야되서 클래스 폭발이 일어날수 있다는 점


해당 내용은 "헤드퍼스트 디자인 패턴"의 실습 내용입니다.

2개의 댓글

comment-user-thumbnail
2023년 6월 28일

+_+이제 패턴 공부까지!! ㅎㅎ 조만간 요거 참고해서 공부 할게요 ㅎㅎ 정리 너무 잘했어영

1개의 답글