팩토리 패턴 - 추상 팩토리

이주오·2022년 6월 20일
0

디자인 패턴

목록 보기
8/12

헤드 퍼스트 디자인 패턴을 읽고 정리한 글입니다.

  • 느슨한 결합을 이용하는 객체지향 디자인, 객체의 인스턴스를 만드는 작업이 항상 공개되어 있어야 하는 것은 아니며, 오히려 결합과 관련된 문제가 생길 수 있다. 팩토리 패턴을 이용하여 불필요한 의존성을 없애보자

추상 팩토리 패턴에서는 인터페이스를 이용하여 서로 연관된, 또는 의존하는 객체를 구상 클래스를 지정하지 않고도 생성할 수 있다.


원재료군

뉴옥과 시카고에서 사용하는 재료 종류는 서로 다르다. PizzaStore 분점이 점점 생기면서 해당 분점들은 또 다른 자신들만의 재료들을 사용해야 될 것이다.

이렇게 서로 다른 종유릐 재료들을 제공하기 위해 원재료군을 처리할 방법을 생각해야한다.


원재료 공장 만들기

따라서 이제 원재료를 생산하기 위한 공장을 만들어보자.

원재료 공장에서는 원재료군에 들어있는 반죽, 소스, 치즈 같은 각각의 원재료를 생산한다.

우선 모든 원재료를 생산할 팩토리를 위한 인터페이스를 정의해보자

public interface PizzaIngredientFactory {

    Dough createDough();

    Sauce createSauce();

    Cheese createCheese();

    Veggies[] createVeggies();

    Pepperoni createPepperoni();

    Clams createClam();

}
public class NYPizzaIngredientFactory implements PizzaIngredientFactory {

    @Override
    public Dough createDough() {
        return new ThinCrustDough();
    }

    @Override
    public Sauce createSauce() {
        return new MarinaraSauce();
    }

    @Override
    public Cheese createCheese() {
        return new ReggianoCheese();
    }

    ...
}

피자 클래스 변경

원재료 팩토리가 준비되고 Pizza 클래스에서 팩토리에서 생산한 원재료만 사용하도록 코드를 수정한다.

public abstract class Pizza {

    String name;
    Dough dough;
    Sauce sauce;
    Cheese cheese;
		...

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

    public void cut() {
        System.out.println("커팅");
    }

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

    abstract void prepare();
}
  • prepare() 메서드를 제외한 다른 메서드들은 바뀌지 않는다.

public class CheesePizza extends Pizza {

    private final PizzaIngredientFactory pizzaIngredientFactory;

    public CheesePizza(PizzaIngredientFactory pizzaIngredientFactory) { // 생성자를 통해 원재료를 제공하는 팩토리를 주입받는다.
        this.pizzaIngredientFactory = pizzaIngredientFactory;
    }

    @Override
    void prepare() { // 팩토리가 작동하는 부분
        dough = pizzaIngredientFactory.createDough();
        sauce = pizzaIngredientFactory.createSauce();
        cheese = pizzaIngredientFactory.createCheese();
    }
}

앞선 예제에서 NYStyleCheesePizza , ChicagoStyleCheesePizza 클래스를 기억해보자. 그 두 클래스를 살펴보면 지역별로 다른 재료를 사용한다는 것만 빼면 다른 점이 없다.

  • 따라서 피자마다 클래스를 지역별로 따로 만들 필요가 없다. 지역별로 다른 점은 원재료 공장에서 처리하기 때문

이제 피자 코드에서는 팩토리를 이용하여 피자에서 쓰이는 재료를 만든다.

  • 만들어지는 재료는 어떤 팩토리를 쓰는지에 따라 달라지며 피자 클래스에서는 전혀 신경을 쓰지 않는다.
  • 이제 피자 클래스와 지역별 재료가 분리되어 있기 때문에 어떤 지역의 재료 팩토리를 사용하든 피자 클래스는 그대로 재사용할 수 있다.

마찬가지로 피자 가게를 수정해보자

public class NYPizzaStore extends PizzaStore {

    @Override
    protected Pizza createPizza(String type) {
        Pizza pizza = null;
        PizzaIngredientFactory ingredientFactory = new NYPizzaIngredientFactory();

        if (type.equals("cheese")) {
            pizza = new CheesePizza(ingredientFactory);
        } else if (type.equals("greek")) {
            pizza = new GreekPizza(ingredientFactory);
        }
        return pizza;
    }
}
  • 뉴욕 피자 가게에서는 뉴욕 피자 원재료 공장을 주입시켜 준다.

정리

기존 팩토리 패턴에서 추상 팩토리라고 부르는 새로운 형식의 팩토리를 도입해서 서로 다른 피자에서 필요로 하는 원재료군을 생산하기 위한 방법을 구축했다.

  • 추상 팩토리를 통해서 제품군을 생성하기 위한 인터페이스를 제공할 수 있다.
  • 이 인터페이스를 이용하는 코드를 만들면 코드를 제품을 생산하는 실제 팩토리와 분리시킬 수 있다.
  • 이렇게 함으로써 서로 다른 상황별로 적당한 제품을 생산할 수 있는 다양한 팩토리를 구현할 수 있게 된다.

추상 팩토리 패턴 정의

제품군을 만들 때 쓸 수 있는 추상 팩터리 패턴에서는 인터페이스를 이용하여 서로 연관된, 또는 의존하는 객체를 구상 클래스를 지정하지 않고도 생성할 수 있다.

  • 추상 팩토리 패턴을 사용하면 클라이언트에서 추상 인터페이스를 통해서 일련의 제품들을 공급받을 수 있다.
  • 이 때, 실제로 어떤 제이품이 생산되는지 전혀 알 필요가 없다.
  • 따라서 클라이언트와 팩토리에서 생산되는 제품을 분리시킬 수 있다.

클래스 다이어그램



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

추상 팩토리 패턴에 있는 createDough(), createSauce() 같은 메서드는 전부 팩토리 메서드 같이 보인다.

그렇다면 추상 팩토리 패턴 뒤에는 팩토리 메서드 패턴이 숨어져 있는 것일까?

  • 각 메서드는 추상 메서드로 선언되어 있고, 서브 클래스에서 메소드를 오버라이드해서 객체를 만드는 방식이기 때문
  • 추상 팩터리가 일련의 제품들을 생성하는데 쓰일 인터페이스를 정의하기 위해 만들어진 것이므로, 해당 인터페이스에 있는 메서드는 구상 제품을 만드는 일을 맡고 있고, 추상 팩토리의 서브클래스를 만들어서 각 메서드의 구현을 제공한다.
  • 따라서 추상 팩토리 패턴에서 제품을 생성하기 위한 메서드를 구현하는데 있어서 팩토리 메서드를 사용하는것은 자연스러운 일이다.

하지만 팩토리 메서드 패턴은 상속을 통해 객체를 생성하고 추상 팩토리 패턴은 객체 구성을 통해 객체를 생성한다.

또한 추상 팩토리 패턴에서는 제품군에 제품을 추가하는 식으로 관련 제품들을 확대해야 하는 경우에 인터페이스를 수정해야 하지만 팩토리 메서드 패턴에서는 한 가지 제품만 생산하므로 복잡한 인터페이스도 필요하지 않고, 메서드도 하나만 있으면 된다.

  • 추상 팩토리 패턴은 클라이언트에서 서로 연관된 제품군을 만들어야 할 때
  • 팩토레 메소드 패턴은 클라이언트 코드와 인스턴스를 만들어야 할 구상 클래스를 분리시켜야하거나, 어떤 구상 클래스를 필요로 하게 될지 미리 알 수 없는 경우에 매우 유용하다.

핵심 정리

  • 팩토리를 쓰면 객체 생성을 캡슐화할 수 있다.
  • 간단한 팩토리는 엄밀히 디자인 패턴은 아니지만, 클라이언트와 구상 클래스를 분리시키기 위한 간단한 기법으로 활용 가능하다.
  • 팩토리 메소드 패턴에서는 상속을 활용한다. 객체 생성이 서브클래스에게 위임된다. 서브 클래스에서는 팩토리 메소드를 구현하여 객체를 생산한다.
  • 추상 팩토리 패턴에서는 객체 구성을 활용한다. 객체 생성이 팩토리 인터페이스에서 선언한 메소스들에서 구현된다.
  • 모든 팩토리 패턴에서는 애플리케이션의 구상 클래스에 대한 의존성을 줄여줌으로써 느슨한 결합을 도와준다.
  • 추상 팩토리 패턴은 구상 클래스에 직접 의존하지 않고도 서로 관련된 객체들로 이루어진 제품군을 만들기 위한 용도로 쓰인다.
  • DIP에 따르면 구상 형식에 대한 의존을 피하고 추상화를 지향할 수 있다.
  • 팩토리는 구상 클래스가 아닌 추상 클래스, 인터페이스에 맞춰 코딩할 수 있게 해주는 강력한 기법이다.

객체지향 도구 상자

  • 객체지향의 기초(4요소)
    • 캡슐화
    • 상속
    • 추상화
    • 다형성
  • 객체지향 원칙
    • 바뀌는 부분을 캡슐화한다.
    • 상속보다는 구성을 활용한다.
    • 구현이 아닌 인터페이스(super type)에 맞춰서 프로그래밍한다.
    • 서로 상호작용을 하는 객체 사이에서는 가능하면 느슨하게 결합하는 디자인을 사용해야 한다.
    • 클래스는 확장에 대해서는 열려 있지만 변경에 대해서는 닫혀 있어야 한다. (OCP)
    • 추상화된 것에 의존하라. 구상 클래스에 의존하지 않도록 한다.
  • 객체지향 패턴
    • 스트레지티 패턴 : 알고리즘군을 정의하고 각각의 알고리즘을 정의하고 각각을 캡슐화하여 교환해서 사용할 수 있도록 만든다. 전략을 사용하면 알고리즘을 사용하는 클라이언트와는 독립적으로 알고리즘을 변경할 수 있다.
    • 옵저버 패턴 : 한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체들한테 연락이 가고 자동으로 내용이 갱신되는 방식으로 일대다 의존성을 정의한다.
    • 데코레이터 패턴: 객체에 추가 요소를 동적으로 더할 수 있습니다. 데코레이터를 사용하면 서브 클래스를 만드는 경우에 비해 훨씬 유연하게 기능을 확장할 수 있습니다.
    • 추상 팩토리 패턴 : 서로 연관된, 또는 의존적인 객체들로 이루어진 제품군을 생성하기 위한 인터페이스를 제공한다. 구상 클래스는 서브 클래스에 의해 만들어진다.
    • 팩토리 메소드 패턴 : 객체를 생성하기 위한 인터페이스를 만든다. 어떤 클래스의 인스턴스를 만들지는 서브클래스에서 결정하도록 한다. 팩토리 메소드를 이용하면 인스턴스를 만드는 일을 서브클래스로 미룰 수 있다.
profile
동료들이 같이 일하고 싶어하는 백엔드 개발자가 되고자 합니다!

0개의 댓글