새소식

design-pattern

[디자인패턴] 팩토리 메서드(Factory Method)와 추상 팩토리(Abstract Factory) 패턴

  • -

팩토리 메서드(Factory Method)와 추상 팩토리(Abstract Factory) 패턴

팩토리(Factory) 패턴을 사용하면 객체 생성을 캡슐화 할 수 있다.
팩토리 패턴에는 팩토리 메서드(Factory Method) 패턴과 추상 팩토리(Abstract Factory) 패턴이 있다.

팩토리 메서드 패턴

개념

팩토리 메서드 패턴은 인스턴스 생성 방법을 상위 클래스에서 결정하되, 생성될 인스턴스가 구체적으로 어떤 클래스의 인스턴스인지는 하위 클래스에서 결정하도록 하는 디자인 패턴을 말한다.

팩토리 메서드 패턴을 사용하면 인스턴스 생성을 위한 프레임워크실제 인스턴스를 생성하는 클래스를 나누어 생각할 수 있다.

아래는 팩토리 메서드 패턴을 클래스 다이어그램으로 표현한 것이다.

팩토리 메서드 클래스 다이어그램

Creator 추상 클래스로 인스턴스를 생성하는 뼈대(프레임워크) 역할을 한다.
Creator 클래스의 factoryMethod() 메서드는 추상 메서드로, Creator 클래스를 구체화한 하위 클래스인 ConcreteCreator에서 이를 구현한다.
factoryMethod() 메서드는 Product 인터페이스를 반환하며, ConcreteCreatorfactoryMethod()를 구현하여 ConcreteProduct를 생성하여 반환하는 역할을 한다.
ConcreateProductProduct 인터페이스의 구현체이다.

이렇게 되면 다른 제품을 추가하거나 제품 구성을 변경하더라도 Creator 클래스가 ConcreteProduct와 느슨하게 결합되어 있기 때문에 Creator는 건드릴 필요가 없다.
구체적인 ConcreteProduct를 생성하는 ConcreteCreator만을 수정하면 된다.

아래 예시 코드를 통해 팩토리 메서드 패턴을 어떻게 구현할 수 있는지 확인해보자.

예시

피자를 만드는 PizzaStore를 예시로 들겠다.
PizzaStore는 여러 지점이 있을 수 있다.
각 지점에서 생성하는 Pizza는 전체적으로는 같지만, 토핑이나 피자 도우같은 일부는 지점별로 스타일이 다르다고 하자.
예를 들자면 시카고 피자랑 뉴욕 피자의 관계와 같다.

이 때, 팩토리 메서드 패턴을 이용할 수 있다.

먼저 Creator를 통해 생산될 Product 인터페이스를 정의하자.
위 예시에서 PizzaProduct 역할을 하게 된다.

생성할 Product 인터페이스 작성

먼저, 생산될 제품인 Product 인터페이스를 정의하자.

// Product
public interface Pizza {
    void bake();
    void complete();
}

Creator 추상 클래스, 팩토리 메서드 작성

이제 이를 생산할 Creator를 작성하자.
여기서는 PizzaStoreCreator의 역할을 한다.

// Creator
public abstract class PizzaStore {

    public final Pizza order(String type) {
        Pizza pizza = create(type);

        pizza.bake();
        pizza.complete();

        return pizza;
    }

    protected abstract Pizza create(String type);
}

order() 메서드는 type을 받아서 팩토리 메서드인 create() 메서드에 넘겨주고 Pizza를 받는다.
Pizza 객체에 적절한 처리(비즈니스 로직 호출)를 가한 후, 이를 반환한다.

create() 메서드는 Pizza를 반환한다.
이는 추상 메서드로 PizzaStore를 상속한 하위 클래스에서 이를 구현해야 한다.
여기서 create() 메서드는 팩토리 메서드Pizza를 생성하는 역할을 하는데, 실제 어떤 Pizza의 구현체가 생성될지는 하위 클래스만 알고있다.
PizzaStore는 상세한 내용(어떤 구상 클래스가 생성되는지에 대한)은 모르고, 인터페이스만을 통해 작업을 처리한다.

ConcreteProduct를 생성하는 ConcreteCreator 작성

이제 PizzaStore를 상속받아서 실제 Pizza의 구현체를 생성해보자.

public class ChicagoPizzaStore extends PizzaStore {
    @Override
    protected Pizza create(String type) {
        Pizza pizza;

        switch (type) {
            case "Custom":
                pizza = new ChicagoCustomPizza();
                break;
            case "Simple":
                pizza = new ChicagoSimplePizza();
                break;
            default:
                throw new RuntimeException();
        }

        return pizza;
    }
}

public class NewYorkPizzaStore extends PizzaStore {
    @Override
    protected Pizza create(String type) {
        Pizza pizza;

        switch (type) {
            case "Custom":
                pizza = new NyCustomPizza();
                break;
            case "Simple":
                pizza = new NySimplePizza();
                break;
            default:
                throw new RuntimeException();
        }

        return pizza;
    }
}

ConcreteCreator 역할을 하는 ChicagoPizzaStoreNewYorkPizzaStore를 작성했다.
XxxPizzaStore들은 각자에게 맞는 ConcreteProductChicagoXxxPizzaNyXxxPizza를 생성한다.

public class ChicagoSimplePizza implements Pizza {
    @Override
    public void bake() {
        System.out.println("[ChicagoSimplePizza] pizza is baking...");
    }

    @Override
    public void complete() {
        System.out.println("[ChicagoSimplePizza] pizza is completed!!!");
    }
}

public class ChicagoCustomPizza implements Pizza {
    @Override
    public void bake() {
        System.out.println("[ChicagoCustomPizza] pizza is baking...");
    }

    @Override
    public void complete() {
        System.out.println("[ChicagoCustomPizza] pizza is completed!!!");
    }
}

public class NySimplePizza implements Pizza {
    @Override
    public void bake() {
        System.out.println("[NySimplePizza] pizza is baking...");
    }

    @Override
    public void complete() {
        System.out.println("[NySimplePizza] pizza is completed!!!");
    }
}

public class NyCustomPizza implements Pizza {
    @Override
    public void bake() {
        System.out.println("[NyCustomPizza] pizza is baking...");
    }

    @Override
    public void complete() {
        System.out.println("[NyCustomPizza] pizza is completed!!!");
    }
}

ConcreteProduct를 작성했다.
Product 역할을 하는 Pizza 인터페이스를 구현하였다.

작성한 내용을 사용해보자.

public class Main {

    public static void main(String[] args) {
        System.out.println("First : ChicagoPizza");
        PizzaStore pizzaStore = new ChicagoPizzaStore();

        pizzaStore.order("Custom");

        System.out.println("=================");
        System.out.println("Second : NewYorkPizza");
        pizzaStore = new NewYorkPizzaStore();

        pizzaStore.order("Simple");
    }
}

처음 PizzaStore를 생성할 때 ChicagoPizzaStore로 생성하고 order() 메서드를 통해 ChicagoCustomPizza를 생성했다.
이후 PizzaStoreNewYorkPizzaStore로 변경하고 이를 통해 NySimplePizza를 생성했다.

이처럼 팩토리 메서드 패턴을 이용하면 인터페이스를 바탕으로 프로그래밍 할 수 있어 유연한 코드를 작성할 수 있다.

또한 ConcreteCreator의 역할을 하는 XxxPizzaStore 부분을 보자.
객체를 생성하는 부분이 XxxPizzaStore에만 들어있기 때문에 변경이 발생한다면 이 부분만 확인하면 된다.

정리

팩토리 메서드 패턴은 다음과 같은 장점들이 있다.

  • 객체 생성 코드를 전부 한 객체 또는 메서드에 넣어서 코드 중복을 제거할 수 있다.
  • 관리할 때에도 한 곳만 신경쓰면 되기 때문에 편리하다.
  • 객체 인스턴스를 만들 때 인터페이스만 있으면 되기 때문에 인터페이스를 바탕으로 프로그래밍 할 수 있어 유연하고 확장성이 뛰어난 시스템을 만들 수 있다.

자바를 사용할 때, 객체를 생성하지 않고 프로그래밍 할 수는 없다.
하지만 팩토리 메서드 패턴과 같이 객체 생성 코드를 한 곳에 모아놓고 체계적으로 관리할 수 있는 디자인을 만들 수는 있다.
이렇게 한 곳에 모아두면 객체 인스턴스를 만드는 코드를 보호하고 관리하기 편해진다.

팩토리 메서드 패턴의 단점은 관리할 클래스가 많아진다는 것이다.
하나의 제품 생성에 여러 클래스들을 작성해야 하고, 결과적으로 코드량이 증가하게 된다.

구상 생산자 클래스가 하나밖에 없더라도 팩토리 메서드 패턴이 유용할까?
충분히 유용하다고 생각한다. 제품을 생산하는 부분과 사용하는 부분을 분리할 수 있기 때문이다.
이렇게 되면 다른 제품을 추가하거나 제품 구성을 변경하더라도 Creator 클래스가 ConcreteProduct와 느슨하게 결합되어 있기 때문에 Creator는 건드릴 필요가 없다.

추상 팩토리 패턴

개념

추상 팩토리 패턴은 구상 클래스에 의존하지 않고도 서로 연관되거나 의존적인 객체로 이루어진 제품군을 생산하는 인터페이스를 제공하는 디자인 패턴을 말한다.

추상 팩토리 패턴을 사용하면 클라이언트는 실제로 어떤 제품이 생산되는지 전혀 알 필요가 없다.
어떤 AbstractFactory의 구상 클래스 인스턴스(ConcreteFactory)를 사용했느냐에 따라 AbstractFactory가 알아서 AbstractProduct를 생성한다. 물론 생성된 AbstractProductConcreteProduct 클래스의 인스턴스이다.
이를 통해 팩토리를 사용하는 클라이언트는 생성될 인스턴스가 구체적으로 어떤 클래스인지 몰라도 된다.
따라서 낮은 결합도를 유지할 수 있다.

아래는 추상 팩토리 패턴을 클래스 다이어그램으로 표현한 것이다.

추상 팩토리 클래스 다이어그램

AbstractFactory에는 AbstractProduct의 인스턴스를 만들기 위한 인터페이스가 존재한다. 이 인터페이스를 사용하여 AbstractProduct의 인스턴스를 생성한다.
AbstractProductAbstractFactory를 통해 만들어지는 추상적인 부품이나 제품의 인터페이스를 결정한다. 생성될 일련의 제품군의 역할을 한다.
ConcreteFactoryAbstractFactory를 구현한 클래스로, AbstractProduct의 구상 클래스 인스턴스를 생성하는 역할을 한다.
ConcreteProductAbstractProduct 인터페이스를 구현하여 생성될 부품이나 제품의 역할을 한다.

아래 예시 코드를 통해 추상 팩토리 패턴을 어떻게 구현할 수 있는지 확인해보자.

예시

위에서 팩토리 메서드 패턴의 예시로 든 PizzaStore를 활용하여 추상 팩토리 패턴의 예시를 들겠다.

Pizza에 소스와 토핑을 추가해야 한다고 가정한다.
Pizzaprepare() 인터페이스를 추가하여 해당 인터페이스를 통해 Pizza에 들어갈 재료를 선택하도록 한다.
이때, 각 PizzaStore 지점마다, 혹은 피자마다 사용하는 재료가 다르도록 해야한다면?

이런 상황에 추상 팩토리 패턴을 이용하면 변경에 유연한 코드를 작성할 수 있다.

먼저 재료인 SourceTopping 인터페이스를 작성하고, 지점마다 사용할 구현체들도 작성한다.

// Source 인터페이스와 구현체들
public interface Source { String getSource(); }

public class NySource implements Source {
    @Override
    public String getSource() { return "source : ny-source"; }
}

public class ChicagoSource implements Source {
    @Override
    public String getSource() { return "source : chicago-source"; }
}

// Topping 인터페이스와 구현체들
public interface Topping { String getTopping(); }

public class NyTopping implements Topping {
    @Override
    public String getTopping() { return "topping : ny-topping"; }
}

public class ChicagoTopping implements Topping {
    @Override
    public String getTopping() { return "topping : chicago-topping"; }
}

이제 위 재료들을 생성할 팩토리 인터페이스를 작성하고, 각 지점의 재료들을 생성하는 팩토리의 구현체를 작성하자.

public abstract class IngredientFactory {
    public abstract Source createSource();
    public abstract Topping createTopping();
}

public class NyPizzaIngredientFactory extends IngredientFactory {
    @Override
    public Source createSource() { return new NySource(); }

    @Override
    public Topping createTopping() { return new NyTopping(); }
}

public class ChicagoPizzaIngredientFactory extends IngredientFactory {
    @Override
    public Source createSource() { return new ChicagoSource(); }

    @Override
    public Topping createTopping() { return new ChicagoTopping(); }
}

추상 팩토리를 정의하고, 팩토리의 구현이 완료되었다. 이제 팩토리를 사용하여 제품을 생성할 차례이다.

일단 기존의 Pizza 인터페이스를 추상 클래스로 변경하여 재료인 SourceTopping을 인스턴스 변수로 가질 수 있도록 하였다.
또한 재료를 준비하는 메서드인 prepare() 메서드를 추가하였다.

// 기존의 interface에서 abstract class로 변경
// Pizza 객체가 공통적으로 Source와 Topping을 갖도록 하기 위함.
public abstract class Pizza {
    protected Source source;
    protected Topping topping;

    public abstract void prepare();

    public abstract void bake();

    public abstract void complete();
}

이제 Pizza 클래스가 재료 팩토리를 사용하여 재료를 생성할 수 있도록 작성하자.
예시는 수정된 Pizza 클래스에 맞게 기존 코드를 수정하고, 재료 팩토리를 주입받아 사용하도록 하였다.

public class NySimplePizza extends Pizza {
    private IngredientFactory ingredientFactory;

    public NySimplePizza(IngredientFactory ingredientFactory) {
        this.ingredientFactory = ingredientFactory;
    }

    @Override
    public void prepare() {
        this.source = ingredientFactory.createSource();
        this.topping = ingredientFactory.createTopping();
    }

    @Override
    public void bake() {
        System.out.println("[NySimplePizza] pizza is baking...");
    }

    @Override
    public void complete() {
        System.out.println("[NySimplePizza] " + source.getSource() + ", " + topping.getTopping() + " pizza is completed!!!");
    }
}

public class ChicagoCustomPizza extends Pizza {
    private IngredientFactory ingredientFactory;

    public ChicagoCustomPizza(IngredientFactory ingredientFactory) {
        this.ingredientFactory = ingredientFactory;
    }

    @Override
    public void prepare() {
        this.source = ingredientFactory.createSource();
        this.topping = ingredientFactory.createTopping();
    }

    @Override
    public void bake() {
        System.out.println("[ChicagoCustomPizza] pizza is baking...");
    }

    @Override
    public void complete() {
        System.out.println("[ChicagoCustomPizza] " + source.getSource() + ", " + topping.getTopping() + " pizza is completed!!!");
    }
}

여기서는 간단하게 NySimplePizzaChicagoCustomPizza만 작성했다.

이제 추상 팩토리 패턴으로 작성된, 재료가 포함된 피자를 생성하도록 해보자.

public class Main {
    public static void main(String[] args) {
        System.out.println("First : ChicagoPizza");
        PizzaStore pizzaStore = new ChicagoPizzaStore();

        // order() 메서드 호출 시점에 IngredientFactory의 구현체를 주입시킨다.
        pizzaStore.order(new ChicagoPizzaIngredientFactory(), "Custom");

        System.out.println("=================");
        System.out.println("Second : NewYorkPizza");
        pizzaStore = new NewYorkPizzaStore();

        // order() 메서드 호출 시점에 IngredientFactory의 구현체를 주입시킨다.
        pizzaStore.order(new NyPizzaIngredientFactory(), "Simple");
    }
}
# 결과 출력
First : ChicagoPizza
[ChicagoCustomPizza] pizza is baking...
[ChicagoCustomPizza] source : chicago-source, topping : chicago-topping pizza is completed!!!
=================
Second : NewYorkPizza
[NySimplePizza] pizza is baking...
[NySimplePizza] source : ny-source, topping : ny-topping pizza is completed!!!

객체 구성을 활용했기 때문에 런타임에도 얼마든지 다른 재료 팩토리로 교체할 수 있다.
PizzaStore.order() 메서드에 필요한 재료를 생성하는 팩토리만 주입시켜주면 된다.

정리

추상 팩토리 패턴도 팩토리 메서드 패턴의 장점과 유사한 장점을 가진다.
팩토리는 구상 클래스가 아닌 추상 클래스와 인터페이스에 맞춰서 코딩할 수 있게 해주는 기법이기 때문이다.
추상 팩토리 패턴의 장점은 다음과 같다.

  • 클라이언트 코드를 특정 구현으로부터 분리하여 결합도를 낮출 수 있다.
  • 객체 구성을 활용하여 인스턴스를 생성하기 때문에, 생성되는 제품군을 쉽게 변경할 수 있다.

추상 팩토리 패턴의 단점 역시 존재한다.
만약 새로운 종류의 제품을 제공해야 한다면, 추상 팩토리의 인터페이스를 변경해야 한다.
추상 팩토리의 인터페이스가 변경되면 이미 존재하는 모든 ConcreteFactory들에 변경된 인터페이스에 대한 코드를 작성해야 한다.

추상 팩토리 패턴은 구상 클래스에 직접 의존하지 않고도 서로 관련된 객체로 이루어진 제품군을 만드는 용도로 쓰인다.
여러 제품군 중 하나를 선택해서 시스템을 설정하고, 한 번 구성한 제품을 다른 것으로 대체해야 한다면 추상 팩토리 패턴 사용을 고려해보자.

팩토리 메서드와 추상 팩토리 정리

공통점

추상 팩토리 패턴과 팩토리 메서드 패턴은 방법은 다르지만 둘 다 애플리케이션을 특정 구현으로부터 분리하는 일을 한다.
애플리케이션의 구상 클래스 의존성을 줄여줌으로서 낮은 결합도를 유지하도록 돕는다.

차이점

팩토리 메서드 패턴상속을 활용하여 객체 생성을 하위 클래스에 맡긴다.
하위 클래스는 팩토리 메서드를 구현하여 객체를 생성한다.
하지만 팩토리 메서드 패턴은 제품 생성에 필요한 여러 클래스들을 작성하게 되어 코드량이 증가하고 관리할 클래스가 많아진다.

추상 팩토리 패턴객체 구성을 활용하여 객체를 생성한다.
추상 팩토리를 구현한 구상 팩토리를 주입받고 팩토리 인터페이스에서 선언한 메서드를 통해 객체를 생성한다.
구상 팩토리를 주입받을 때 상위 타입으로 받을 수 있도록 하여, 쉽게 팩토리를 교체할 수 있다.
추상 팩토리 패턴은 제품군에 새로운 제품을 추가하게 되면 팩토리 클래스의 인터페이스를 추가해야 하고, 기존의 모든 팩토리 구현체에 이에 대한 코드를 작성해야 한다.

[디자인패턴] 팩토리 메서드(Factory Method)와 추상 팩토리(Abstract Factory) 패턴

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.