컴퓨터과학/0 + 소프트웨어 아키텍처(디자인 패턴)

[소프트웨어 아키텍처] 8. 팩토리 메소드 패턴, 추상 팩토리 패턴(Factory Method Pattern, Abstract Factory Pattern -java)

힘들면힘을내는쿼카 2022. 11. 29. 16:06
728x90
반응형

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

 

 

구상 형식 인스턴스를 만드는 작업을 캡슐화 합니다.

객체의 인스턴스를 만드는 작업이 항상 공개되어야 하는 것은 아닙니다.
오히려 공개했을 경우 결합 문제가 발생할 수도 있습니다.
이러한 불필요한 의존성을 없애서 결합 문제를 해결하는 방법이 바로 팩토리 패턴 입니다.

  1. 객체를 생성할 때 필요한 인터페이스를 만듭니다.
  2. 어떤 클래스의 인스턴스를 만들지는 서브클래스에서 결정합니다.

팩토리 메소드 패턴을 사용하면 클래스 인스턴스를 만드는 작업을 서브클래스에게 맡기게 됩니다.
사용하는 서브클래스에 따라 생산되는 객체 인스턴스가 결정됩니다.

Factory factory = new SeoulFactory();

new를 사용하면 구상 클래스의 인스턴스가 만들어 집니다.

당연히 인터페이스가 아닌 특정 구현을 사용하는 방법 입니다.

하지만 우리는 특정 구현(구상 클래스)을 바탕으로 코딩하면 나중에 코드를 수정해야 할 가능성이 커지고, 유연성이 떨어진다고 배웠습니다.!

이를 어떻게 해결하면 좋을까요?

구상 클래스??
new 키워드를 사용하여 인스턴스를 만드는 클래스를 구상 클래스(Concrete Class)라고 합니다.

 

인터페이스에 맞춰서 코딩하면 시스템에서 일어날 수 잇는 여러 변화에 대응 할 수 있습니다.!
인터페이스를 의존하는 코드는 어떤 클래스든 특정 인터페이스만 구현하면 사용할 수 있기 때문입니다.
이러한 원리를 다형성 이라고 부릅니다.

반대로 구상 클래스를 많이 사용하면 새로운 구상 클래스가 추가될 때마다 코드를 고쳐야 하기 때문에 문제가 발생합니다.
즉, 변경에는 닫혀 있는 코드가 되는 것 입니다. OCP를 위배하게 되는 거죠.

 

의존성 뒤집기 원칙(DIP : Dependency Inversion Principle)

구상 클래스 의존성을 줄이면 좋다는 것은 이제 알게 되었습니다.

추상화된 것에 의존하게 만들고, 구상 클래스에 의존하지 않게 만든다.
쉽게 이야기하면, 인터페이스(추상화)에 의존해라 입니다.

 

고수준 구성 요소(Creator)가 저수준 구성 요소(Product)에 의존하면 안되며, 항상 추상화에 의존하게 만들어야 한다는 의미 입니다.

DIP에 따르면, 구상 클래스처럼 구체적인 것이 아닌 추상 클래스나 인터페이스와 같이 추상적인 것에 의존하는 코드를 만들어야 합니다.

의존성 뒤집기 원칙을 지키를 방법

    • 변수에 구상 클래스의 레퍼런스를 저장하지 말자.
      • new 연산자를 사용하면 구상 클래스의 레퍼런스를 사용하게 된다.
      • 그러므로 팩토리를 사용하여 구상 클래스의 레퍼런스를 변수에 저장하는 일을 방지 하자.
    • 구상 클래스에서 유도된 클래스를 만들지 말자.
      • 구상 클래스에서 유도된 클래스를 만들면 특정 구상 클래스에 의존하게 됩니다.
      • 인터페이스나 추상 클래스처럼 추강화된 것으로부터 클래스를 만들자.
    • 베이스 클래스에 이미 구현되어 있는 메소드를 오버라이드 하지 말자.
      • 이미 구현되어 있는 메소드를 오버라이드한다면 베이스 클래스가 제대로 추상화 되지 않는다.
      • 베이스 클래스에서 메소드를 정의할 때는 모든 서브클래스에서 공유할 수 있는 것만 정의하자.

팩토리 메소드 패턴 구조

  • Product
    • 제품 역할부
  • ConcreteProduct
    • 제품 클래스의 구현체
  • Creator
    • 제품으로 원하는 작업을 할 때 필요한 모든 메소드가 구현
    • 제품의 인스턴스를 생성하는 메소드는 추상메소드로 정의
  • ConcreteCreator
    • 인스턴스를 생성하는 factoryMethod()를 구현

피자 가게 애플리케이션 만들기

피자를 좋아하는 신영이는 팩토리 메소드 패턴을 이용하여 피자 가게 애플리케이션을 만들려고 한다.!

Pizza

public abstract class Pizza {
    private String storeName;
    private String name;

    public void initPizza(String storeName, String name) {
        this.storeName = storeName;
        this.name = name;
    }

    public void hello() {
        System.out.println(storeName+" 입니다.");
    }

    public void prepare() {
        System.out.println(name+" 상품을 준비 합니다.");
    }

    public void finish() {
        System.out.println("끝!");
    }
}

PizzaHutCheese

public class PizzaHutCheese extends Pizza {
    public PizzaHutCheese() {
        initPizza("피자헛", "치즈");
    }
}

PizzaHutPotato

public class PizzaHutPotato extends Pizza {
    public PizzaHutPotato() {
        initPizza("피자헛", "포테이토");
    }
}

PizzaSchoolCheese

public class PizzaSchoolCheesePizza extends Pizza {
    public PizzaSchoolCheesePizza() {
        initPizza("피자스쿨", "치즈");
    }
}

PizzaSchoolPotato

public class PizzaSchoolPotatoPizza extends Pizza {
    public PizzaSchoolPotatoPizza() {
        initPizza("피자스쿨", "포테이토");
    }
}

PizzaStore

public abstract class PizzaStore {

    public void orderPizza(String type) {
        Pizza pizza = createPizza(type);
        pizza.hello();
        pizza.prepare();
        pizza.finish();
    }

      //피자 브랜드마다 피자를 만드는 방법 캡슐화
    public abstract Pizza createPizza(String type);
}

PizzaHut

public class PizzaHut extends PizzaStore{
    @Override
    public Pizza createPizza(String type) {
        if(type.equals("포테이토")) return new PizzaHutPotato();
        else if(type.equals("치크")) return new PizzaHutCheese();
        return new PizzaHutCheese();
    }
}

PizzaSchool

public class PizzaSchool extends PizzaStore{
    @Override
    public Pizza createPizza(String type) {
        if(type.equals("포테이토")) return new PizzaSchoolPotatoPizza();
        else if(type.equals("치즈")) return new PizzaSchoolCheesePizza();
        return new PizzaSchoolCheesePizza();
    }
}

Client

public class Client {
    public static void main(String[] args) {
        System.out.println("신영이: 아 피자헛 포테이토피자 먹고 싶다!");
        PizzaStore pizzaStore = new PizzaHut();
        pizzaStore.orderPizza("포테이토");

        System.out.println("--------------------------------");

        System.out.println("신영이: 아 피자스쿨 치즈피자 먹고 싶다!");
        pizzaStore = new PizzaSchool();
        pizzaStore.orderPizza("치즈");
    }
}

결과

신영이: 아 피자헛 포테이토피자 먹고 싶다!
피자헛 입니다.
포테이토 상품을 준비 합니다.
끝!
--------------------------------
신영이: 아 피자스쿨 치즈피자 먹고 싶다!
피자스쿨 입니다.
치즈 상품을 준비 합니다.
끝!

 

728x90

 

추상 팩토리 패턴(Abstract Factory Pattern)

두 가지 피자를 맛본 신영이는 사실 엄청난 부자라서 두 피자가게를 인수 했습니다.
그런데 피자 가게를 운영하면서 문제가 생깁니다.
같은 종류의 피자라고 할지라도 PizzaSchoolPizzaHut에 들어가는 재료는 다르겠죠?
신영이는 이러한 문제를 해결하기위해 어떻게 해야할까요? 🤔

추상 팩토리 패턴은 제품군을 만들 때 사용할 수 있는 패턴 입니다.
구상 클래스에 의존하지 않고도 서로 연관되거나 의존적인 객체로 이루어진 제품군을 생산하는 인터페이스를 제공합니다.
구상 클래스는 서브클래스에서 만듭니다.

추상 팩토리 패턴을 사용하면 클라이언트에서 추상 인터페이스로 일련의 제품을 공급받을 수가 있습니다.
이때, 실제로 어떤 제품이 생산되는지는 알 필요가 없습니다.
따라서 클라이언트와 팩토리에서 생산되는 제품을 분리할 수 있습니다…!

추상 팩토리 패턴 구조

  • AbstractFactory
    • 모든 구상 팩토리에서 구현해야하는 인터페이스
    • 제품을 생산할 때 일련의 메소드가 정의됨
  • ConcreteFactory1, 2
    • 구상 팩토리는 서로 다른 제품군을 구현
    • 클라이언트에서 제품이 필요하면 팩토리 가운데 적당한 걸 골라서 사용하면 되기에 제품 객체의 인스턴스를 직접 만들 필요가 없음
  • AbstractProductA, B
    • 제품군, 각 구상 팩토리에서 필요한 제품군의 인터페이스
  • ProductA, B
    • 제품군의 구현체

인수한 피자가게 재료공급 애플리케이션

아까 언급한 것 처럼 신영이가 인수한 피자가게는 브랜드마다 같은 종류의 피자일지라도 들어가는 재료가 다릅니다.
이를 해결해봅시다.!

PizzaIngredientFactory

public interface PizzaIngredientFactory {
    Dough createDough();
    Cheese createCheese();
}

PizzaHutIngredientFactory

public class PizzaHutIngredientFactory implements PizzaIngredientFactory {

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

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

PizzaSchoolIngredientFactory

public class PizzaSchoolIngredientFactory implements PizzaIngredientFactory {

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

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

Cheese

public interface Cheese {
    void cheeseName();
}

CamembertCheese

public class CamembertCheese implements Cheese {

    public CamembertCheese() {
        cheeseName();
    }

    @Override
    public void cheeseName() {
        System.out.println("까망베르 치즈");
    }
}

MozzarellaCheese

public class MozzarellaCheese implements Cheese {

    public MozzarellaCheese() {
        cheeseName();
    }

    @Override
    public void cheeseName() {
        System.out.println("모짜렐라 치즈");
    }
}

Dough

public interface Dough {
    void doughName();
}

KoreaDough

public class KoreaDough implements Dough {

    public KoreaDough() {
        doughName();
    }

    @Override
    public void doughName() {
        System.out.println("한국 도우");
    }
}

USADough

public class USADough implements Dough {

    public USADough() {
        doughName();
    }

    @Override
    public void doughName() {
        System.out.println("미국 도우");
    }
}

Pizza

import lombok.Getter;
import lombok.Setter;

@Getter @Setter //롬복 사용
public abstract class Pizza {
    Cheese cheese;
    Dough dough;

    public abstract void prepare();
}

CheesePizza

public class CheesePizza extends Pizza {

    private PizzaIngredientFactory ingredientFactory;

    public CheesePizza(PizzaIngredientFactory ingredientFactory) {
        this.ingredientFactory = ingredientFactory;
    }

    @Override
    public void prepare() {
        System.out.println("피자 재료 선택!");
        setCheese(ingredientFactory.createCheese());
        setDough(ingredientFactory.createDough());
    }
}

PizzaStore

public abstract class PizzaStore {

    public void orderPizza(String type) {
        Pizza pizza = createPizza(type);
        pizza.prepare();
    }

    public abstract Pizza createPizza(String type);
}

PizzaHutStore

public class PizzaHutStore extends PizzaStore {
    @Override
    public Pizza createPizza(String type) {
        System.out.println("안녕하세요. 피자헛 입니다.");
        PizzaIngredientFactory ingredientFactory = new PizzaHutIngredientFactory();

        if(type.equals("cheese")) {
            return new CheesePizza(ingredientFactory);
        }

        return null;
    }
}

PizzaSchoolStore

public class PizzaSchoolStore extends PizzaStore {
    @Override
    public Pizza createPizza(String type) {
        System.out.println("안녕하세요. 피자스쿨 입니다.");
        PizzaIngredientFactory ingredientFactory = new PizzaSchoolIngredientFactory();

        if(type.equals("cheese")) {
            return new CheesePizza(ingredientFactory);
        }

        return null;
    }
}

Client

public class Client {
    public static void main(String[] args) {
        PizzaStore pizzaStore1 = new PizzaHutStore();
        pizzaStore1.orderPizza("cheese");

        System.out.println("============================");

        PizzaStore pizzaStore2 = new PizzaSchoolStore();
        pizzaStore2.orderPizza("cheese");
    }
}

결과

안녕하세요. 피자헛 입니다.
피자 재료 선택!
까망베르 치즈
한국 도우
============================
안녕하세요. 피자스쿨 입니다.
피자 재료 선택!
모짜렐라 치즈
미국 도우

 

반응형

 

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

위 코드를 보면 추상 팩토리에 있는 메소드를 팩토리 메소드(createDough(), createCheese())와 같다는 것을 알 수 있습니다.
그렇다면 메소드는 추상 메소드로 선언되어 있고 서브클래스에서 오바라이드해서 객체를 만드는데, 그러면 그냥 팩토리 메소드 패턴이 아닌가?!

추상 팩토리 패턴 뒤에 팩토리 메소드 패턴이 숨겨져 있는건가?

추상 팩토리 메소드가 팩토리 메소드로 구현되는 경우가 종종 있습니다.
추상 팩토리가 원래 일련의 제품을 만드는데 사용하는 인터페이스를 정의하려고 만들어진 패턴이기 때문입니다.
인터페이스(PizzaIngredientFactory)에 있는 각 메소드는 구상 제품(createDough(), createCheese())을 생산하는 일을 맡고, 추상 팩토리 패턴에서 제품을 생산하는 메소드를 구현하는데 있어서 팩토리 메소드를 사용하는 것은 자연스러운 작업 입니다.

 

팩토리 메소드 패턴

팩토리 메소드 패턴을 사용하면 클래스 인스턴스를 만드는 작업을 서브클래스에게 맡기게 됩니다.
사용하는 서브클래스에 따라 생산되는 객체 인스턴스가 결정됩니다.

 

추상 팩토리 패턴

추상 팩토리 패턴은 제품군을 만들 때 사용할 수 있는 패턴 입니다.
구상 클래스에 의존하지 않고도 서로 연관되거나 의존적인 객체로 이루어진 제품군을 생산하는 인터페이스를 제공합니다.
구상 클래스는 서브클래스에서 만듭니다.

 

두 패턴 모두 특정 구현으로부터 분리하는 패턴 입니다. 둘다 구상 클래스는 서브클래스에서 구현하기 때문이죠!!
객체 생성을 캡슐화 해서 코드와 구상 형식을 분리 합니다.

클라이언트에서 서로 연관된 일련의 제품을 만들어야 할때, 즉 제품군을 만들어야 할 때는 추상 팩토리 패턴 사용
클라이언트 코드와 인스턴스를 만들어야 할 구상 클래스를 분리시켜야 할 때, 팩토리 메소드 패턴 사용

 

 

 

728x90
반응형