팩토리 패턴 - 팩토리 메서드

이주오·2022년 5월 28일
3

디자인 패턴

목록 보기
7/12

팩토리 패턴

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

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

팩토리 메소드 패턴에서는 객체를 생성하기 위한 인터페이스를 정의하는데, 어떤 클래스의 인스턴스를 만들지는 서브클래스에서 결정하게 만든다. 팩토리 메소드 패턴을 이용하면 클래스의 인스턴스를 만드는 일을 서브클래스에게 맡기는 것


“new”는 “구상 객체”를 뜻한다.

new를 사용하는 것은 구상 클래스의 인스턴스를 만드는 것이다. 당연히 인터페이스가 아닌 특정 구현을 사용하는 것, 앞에서 디자인 패턴을 공부하면서 구상 클래스를 바탕으로 코딩을 하게 되면 코드를 수정해야 할 가능성이 높아지고, 유연성이 떨어지는 것을 볼 수 있었다.

Duck duck = new MallardDuck();

일련의 구상 클래스들이 존재하는 경우, 다음과 같은 코드를 만들어야 하는 경우가 있다.

Duck duck;

// 컴파일시에는 어떤 것의 인스턴스를 만들어야 할지 알 수 없다.
if(picnic) {
		duck = new MallardDuck();
} else if() {
		duck = new DecoyDuck();
} else if() {
		duck = new RubberDuck();
}

이런 코드가 있다는 것은, 변경하거나 확장해야 할 때 코드를 다시 확인하고 추가 또는 제거해야 한다는 것을 뜻한다. 따라서 코드를 이런식으로 만들면 관리 및 갱신하기가 어려워지고, 오류가 생길 가능성이 높아지게 된다.

사실 “new” 자체에 문제가 있는 것은 아니다. 가장 문제가 되는 점은 “변경”이다. 뭔가 변경되는 것 때문에 new를

사용하는데 있어서 조심해야 하는 것이다.

그렇기 때문에 인터페이스에 맞춰서 코딩을 하면 다형성 덕분에 어떤 클래스든 특정 인터페이스만 구현하면 사용할 수 있기 때문에 여러 변경에 대해 유연함을 가질 수 있는 것이다. 반대로 구상 클래스를 많이 사용하면 새로운 구상 클래스가 추가될 때마다 코드를 고쳐야 하기 때문에 많은 문제가 생길 수 있다. 즉 OCP 원칙

💡 어떻게 하면 애플리케이션에서 구상 클래스의 인스턴스를 만드는 부분을 전부 찾아내서 애플리케이션의 나머지 부분으로부터 분리 및 캡슐화 시킬 수 있을까?

바뀌는 부분을 찾아보자

피자 가게를 운영하고 있다고 생각해보자.

피자의 종류가 다양할 것이나, 새로운 피자 신메뉴를 출시하거나 메뉴가 사라질 수 있을 것이다.

따라서 oderPizza() 메소드에서 가장 문제가 되는 부분은 바로 인스턴스를 만들 구상 클래스를 선택하는 부분이다. 해당 부분 때문에 변화에 따라 코드를 변경할 수 밖에 없다. 이제 바뀌는 부분과 바뀌지 않는 부분을 파악했으니 캡슐화할 차례이다.

Pizza orderPizza(String type) {
        Pizza pizza = null; // 인터페이스

				// 바뀌는 부분
        if (type.equals("cheese")) {
            pizza = new CheesePizza();
        } else if (type.equals("greek")) {
            pizza = new GreekPizza();
        } else if (type.equals("pepperoni")) {
            pizza = new PepperoniPizza();
        }

				// 바뀌지 않는 부분
        pizza.prepare();
        pizza.bake();
        pizza.cut();
        pizza.box();

        return pizza;
    }

객체 생성 부분을 캡슐화해보자

이제 객체를 생성하는 부분을 메소드에서 뽑아내어 피자 객체를 만드는 일만 전담하는 다른 객체에 집어넣어 보자.

피자를 만드는 일만 처리하는 객체를 집어 넣어 다른 객체에서 피자를 만들어야 하는 일이 있으면 해당 객체에게 부탁하는 객체 추가해보자. 새로 만들 객체에는 팩토리라는 이름을 붙이기로 하자. SimplePizzaFactory를 만들고 나면 orderPizza() 메소드는 새로 만든 객체의 클라이언트가 된다. 즉 새로 만든 객체를 호출하는 것. 피자 공장에 피자 하나 만들어 달라고 부탁한다고 생각하면 쉽다.


SimplePizzaFactory를 추가하자

피자 객체 생성을 전달한 클래스를 정의한다.

// 해당 클래스에서 하는 일은 클라이언트를 위해 피자를 만들어 주는 일 뿐이다.
public class SimplePizzaFactory {

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

        if (type.equals("cheese")) {
            pizza = new CheesePizza();
        } else if (type.equals("greek")) {
            pizza = new GreekPizza();
        } else if (type.equals("pepperoni")) {
            pizza = new PepperoniPizza();
        }
        return pizza;
    }

}

Q) 이렇게 하면 어떤 장점이 있는 것일까?? 얼핏 보면 아까 문제를 다른 객체로 넘겨 버린 것 처럼 보일 수 있어 보인다.

  • SimplePizzaFactory를 사용하는 클라이언트가 매우 많을 수 있는 점을 생각 하자
  • 피자 객체를 받아서 가격, 설명 등을 찾아서 활용하는 클래스 또는 피자 주문을 처리하는 클래스에서도 이 팩토리를 사용할 수 있을 것이다.
  • 따라서 피자리를 생성하는 작업을 한 클래스에 캡슐화시켜 놓으면 구현을 변경해야 하는 경우에 여기저기 다 들어가서 고칠 필요 없이 팩토리 클래스 하나만 고치면 된다.
  • 추후 클라이언트 코드에서 구상 클래스의 인스턴스를 만드는 코드를 없애는 작업 진행

Q) 비슷한 식으로 메소드를 정적 메소드를 선언한 디자인(정적 팩터리 메소드)과 차이점은 무엇일까?

  • 정적 팩토리 메서드를 사용하면 객체를 생성하기 위한 메소드를 실행시키기 위해서 객체의 인스턴스를 만들지 않아도 되기 때문에 간단한 팩토리를 정적 메소드를 정의하는 기법도 일반적으로 많이 쓰인다.
  • 하지만 서브클래스를 만들어서 객체 생성 메소드의 행동을 변경시킬 수 없다는 단점이 존재한다.

간단한 팩토리를 이용한 PizzaStore 수정

public class PizzaStore {

    SimplePizzaFactory simplePizzaFactory;

    public PizzaStore(SimplePizzaFactory simplePizzaFactory) {
        this.simplePizzaFactory = simplePizzaFactory;
    }

    Pizza orderPizza(String type) {
        Pizza pizza = simplePizzaFactory.createPizza(type);

        // 바뀌지 않는 부분
        pizza.prepare();
        pizza.bake();
        pizza.cut();
        pizza.box();

        return pizza;
    }

}

PizzaStore

  • 팩토리를 사용하는 클라이언트
  • 팩토리를 통해 피자 인스턴스를 받게 된다.

SimplePizzaFactory

  • 피자 객체를 만드는 팩토리
  • 애플리케이션에서 유일하게 구상 Pizza 클래스를 직접 참조한다.
💡 이렇게 간단한 팩토리를 이용해 보았다. 간단한 팩토리는 디자인 패턴이라고 할 수 없고, 관용구에 가깝다. 이제 팩토리에 해당하는 두 가지 강력한 패턴을 알아보자.

팩토리 메서드

피자 가게가 큰 성공을 거두어 여러 지점을 가지게 되었고, 지역별로 조금씩 다른 차이점이 존재했고, 각각 지역의 특성을 반영하여 피자를 만들어야 했다. 이런 차이점을 어떤 식으로 적용해야 할까??

간단하게 생각하면 SimplePizzaFactory를 빼고 지역별 피자 팩토리(NYPizzaFactory, ChicagoPizzaFactory)를 만든 다음, PizzaStore에서 해당하는 팩토리를 사용하도록 하면 될 것이다.

NYPizzaFactory nyFactory = new NYPizzaFactory();
PizzaStore nyStore = new PizzaStore(nyFactory);
nyStore.orderPizza("Veggie");

ChicagoPizzaFactory chicagoFactory = new ChicagoPizzaFactory();
PizzaStore chicagoStore = new PizzaStore(chicagoFactory);
chicagoStore.orderPizza("Veggie");
💡 해당 팩토리를 사용하여 피자를 만들었지만 지역별로 피자를 만들 때, 독자적인 방법들을 사용하기 시작했다. 이런 문제를 해결하기 위해 피자 가게와 피자 제작 과정 전체를 하나로 묶어주는 프레임워크의 필요성과 어떻게 하면 유연성을 잃지 않게 묶을 수 있을까?? **바로 서브클래스에서 결정하게 하는 것!!**

피자 가게 프레임워크 - 팩토리 메소드 선언

피자를 만드는 활동 자체를 전부 PizzaStore 클래스에 국한시키면서도 분점마다 고유의 스타일을 살릴 수 있도록 하기 위해서 createPizza() 메소드를 다시 PizzaStore에 집어 넣고 추상 메서드로 선언하고, 각 지역마다 고유의 스타일에 맞게 PizzaStore의 분점을 나타내는 서브클래스를 만들도록 할 것이다. 즉 피자의 스타일은 각 서브클래스에서 결정하는 것

public abstract class PizzaStore {

    Pizza orderPizza(String type) {
        
        Pizza pizza = createPizza(type); // 팩토리 객체가 아닌 메소드 호출

        // 바뀌지 않는 부분
        pizza.prepare();
        pizza.bake();
        pizza.cut();
        pizza.box();

        return pizza;
    }

    // 팩토리 객체 대신 해당 "메소드" 사용
    // "팩토리 메소드"가 추상 메소드로 바뀌었다.
    abstract Pizza createPizza(String type);

}

서브클래스에서 결정되는 것

각 서브클래스에서 달라질 수 있는 것은 피자의 만들 때의 스타일 뿐이다. 이렇게 달라지는 점들을 createPizza() 메소드에 집어넣고 그 메소드에서 해당 스타일의 피자를 만드는 것을 모두 책임진다. PizzaStore의 서브클래스에서 createPizza() 메소드를 구현하도록 하면 PizzaStore 프레임워크에 충실하면서도 각각의 스타일을 제대로 구현할 수 있는 PizzaStore 서브클래스들을 구비할 수 있다.

  • PizzaStore에서 정의한 메서드를 서브클래스에서 고쳐서 쓸 수 없게 하고 싶다면 메서드를 final로 선언할 수도 있다.
  • ex) final Pizza orderPizza(String type)
public class NYPizzaStore extends PizzaStore {

    @Override
    Pizza createPizza(String type) {
        Pizza pizza = null;

        if (type.equals("cheese")) {
            pizza = new NYStyleCheesePizza();
        } else if (type.equals("greek")) {
            pizza = new NYStyleGreekPizza();
        }
        return pizza;
    }
}

public class ChicagoPizzaStore extends PizzaStore {

    @Override
    Pizza createPizza(String type) {
        Pizza pizza = null;

        if (type.equals("cheese")) {
            pizza = new ChicagoStyleCheesePizza();
        } else if (type.equals("greek")) {
            pizza = new ChicagoStyleGreekPizza();
        }
        return pizza;
    }
}
  • 슈퍼클래스에 있는 orderPizza() 메소드에서는 Pizza 객체를 가지고 여러 작업을 하긴 하지만, Pizza는 추상 클래스기 때문에 어떤 구상 클래스에서 작업이 처리되고 있는지 전혀 알 수없다.
    • 즉 PizzaStore와 Pizza는 서로 완전히 분리되어 있다.
    • 그렇다면 피자 종류를 결정하는 것은 누구일까??
  • orderPizza() 입장에서 볼 때는 PizzaStore 서브클래스에서 피자 종류를 결정한다고 할 수 있을 것이다.
    • 따라서 서브 클래스에서 실제로 뭔가를 “결정”하는 것이 아니라, 우리가 선택하는 PizzaStore의 서브클래스 종류에 따라 결정되는 것이지만, 만들어지는 피자의 종류를 해당 PizzaStore 서브클래스에서 결정한다고 할 수 있다.

팩토리 메소드

abstract Product factoryMethod(String type);

팩토리 메소드는 객체 생성을 처리하며, 팩토리 메소드를 이용하면 객체를 생성하는 작업을 서브클래스에 캡슐화시킬 수 있다. 이렇게 하면 슈퍼 클래스에 있는 클라이언트 코드와 서브클래스에 있는 객체 생성 코드를 분리시킬 수 있다.

  • abstract
    • 팩토리 메소드는 추상 메소드로 선언하여 서브클래스에서 객체 생성을 책임지도록 한다.
  • Product
    • 팩토리 메소드에서는 특정 객체를 리턴하며, 그 객체는 보통 수퍼 클래스에서 정의한 메소드 내에서 쓰이게 된다.
  • factoryMethod
    • 팩토리 메소드는 클라이언트 (ex : orderPizza method)에서 실제로 생성되는 구상 객체가 무엇인지 알 수 없게 만드는 역할도 한다.
  • type
    • 팩토리 메소드를 만들 때 매개변수를 써서 만들어낼 객체 종류를 선택할 수도 있다.

사용 code

public class Main {

    public static void main(String[] args) {
        PizzaStore nyStore = new NYPizzaStore();
        PizzaStore chicagoStore = new ChicagoPizzaStore();

        Pizza pizza = nyStore.orderPizza("cheese");
        pizza = chicagoStore.orderPizza("cheese");

    }

}

팩토리 메소드 패턴

팩토리 메소드 패턴에서는 객체를 생성하기 위한 인터페이스를 정의하는데, 어떤 클래스의 인스턴스를 만들지는 서브클래스에서 결정하게 만든다. 팩토리 메소드 패턴을 이용하면 클래스의 인스턴스를 만드는 일을 서브클래스에게 맡기는 것

  • 모든 팩토리 패턴에서는 객체 생성을 캡슐화한다.
  • 팩토리 메소드 패턴에서는 서브 클래스에서 어떤 클래스를 만들지를 결정하게 함으로써 객체 생성을 캡슐화한다.
  • 즉 팩토리 메서드 패턴에서는 객체를 생성하기 위한 인터페이스를 정의하는데, 어떤 클래스의 인스턴스를 만들지는 서브클래스에서 결정하게 만든다.
  • 팩토리 메소드 패턴을 이용하면 클래스의 인스턴스를 만드는 일을 서브클래스에게 맡기는 것
💡 여기에서 결정한다 라는 표현을 쓰는 이유는, 서브클래스에서 실행중에 어떤 클래스의 인스턴스를 만들지를 결정하기 때문이 아니라, 생산자 클래스 자체가 실제 생산될 제품에 대한 사전 지식이 전혀 없이 만들어지기 때문이다. 즉 사용하는 서브클래스에 따라 생산되는 객체 인스턴스가 결정되는 것이다.

클래스 다이어그램을 살펴보자

Creator : 생산자

  • ex) PizzaStore Class
  • 제품을 가지고 원하는 일을 하기 위한 모든 메소드들이 구현되어 있다.
  • 하지만 제품을 만들어 주는 팩토리 메소드는 추상 메소드로 정의되어 있을 뿐, 구현되어 있진 않다.
  • Creator의 모든 서브 클래스에서 factoryMethod() 추상 메소드를 구현해야 한다.

ConreteCreator : 구상 생산자

  • 실제로 제품을 생산하는 factoryMethod()를 구현한다.

Product / ConreteProduct

  • 제품 클래스에서는 모두 똑같은 인터페이스 구현
  • 그래야 제품을 사용할 클래스(클라이언트)에서 구상 클래스가 아닌 인터페이스에 대한 레퍼런스를 써서 객체를 참조할 수 있기 때문

ConreteCreator → ConreteProduct

  • 구상 클래스 인스턴스를 만들어내는 일은 ConreteCreator가 책임진다.
  • 실제 제품인 ConreteProduct 객체를 만들어내는 방법을 알고 있는 클래스는 ConreteCreator 클래스 뿐

병렬 클래스 계층 구조

  • Product, Creator 클래스 둘 다 추상 클래스로 시작하고, 그 클래스를 확장하는 구상 클래스들을 가지고 있다.
  • 구체적인 구현은 구상 클래스들이 책임지고 있다.
  • 구상 생산자 클래스에는 특정 구상 제품군에 대한 모든 지식이 캡슐화 되어 있으며 팩토리 메소는 이러한 지식을 캡슐화 시키는데 있어서 가장 핵심적인 역할을 맡고 있다.

Simple Factory vs Factory Method Pattern

  • 뉴욕과 시카고 분점을 만들 때 간단한 팩토리를 사용했다고 할 수 있지 않을까??
    • 비슷하긴 하지만 방법이 조금 다르다.
    • 구상 클래스를 만들 때 createPizza() 추상 메소드가 정의되어 있는 추상 클래스를 확장해서 만들었다는 점이 중요한 차이
      • createPizza() 메소드에서 어떤 일을 할지는 각 분점에서 결정한다.
    • 간단한 팩토리를 사용할 때는 팩토리가 PizzaStore 안에 포함되는 별개의 객체였다는 큰 차이점이 존재한다.
  • simple factory
    • 일회용 처방에 불과하다.
    • 객체 생성을 캡슐화하는 방법을 사용하긴 하지만 생성하는 제품을 마음대로 변경할 수 없기 때문에 강력한 유연성을 제공하진 못한다.
  • factory method pattern
    • 어떤 구현을 사용할지를 서브클래스에서 결정하는 프레임워크를 만들 수 있다.
    • 강력한 유연성을 제공한다.

의존적인 PizzaStore

  • 팩토리를 사용하지 않는 PizzaStore 클래스는 피자 객체가 의존하고 있는 구상 피자 객체의 개수만큼 의존하게 된다.
  • 왜냐하면 객체 인스턴스를 직접 만들어 구상 클래스에 의존하기 때문이다.

https://msyu1207.tistory.com/entry/4장-헤드퍼스트-디자인-패턴-팩토리-패턴

  • 적용 전

https://msyu1207.tistory.com/entry/4장-헤드퍼스트-디자인-패턴-팩토리-패턴

  • 팩토리 메소드 패턴을 적용한 다이어 그램

DIP : 의존성 역전 원칙

  • 구상 클래스에 대한 의존성을 줄이는 것이 좋다는 내용을 정리해 놓은 객체지향 디자인 원칙
  • 이 원칙은 다음과 같이 일반화 시킬 수 있다.
    • 추상화된 것에 의존하도록 만들어라
    • 구상 클래스에 의존하도록 만들지 않아야 한다.
  • 이 원칙하고 “특정 구현이 아닌 인터페이스에 맞춰서 프로그래밍한다”는 원칙이 똑같다고 생각할 수 있다.
    • 비슷하긴 하지만 dip에서는 추상화를 더 많이 강조한다.
    • 해당 원칙에는 고수준 구성요소가 저수준 구성요소에 의존하면 안된다는 것이 내포되어 있다.
    • 항상 추상화에 의존하도록 만들어야 한다.
  • 그럼 고수준, 저수준은 어떤 의미일까??
    • 고수준 구성요소
      • 고수준 구성요소는 다른 저수준 구성요소에 의해 정의되는 행동이 들어있는 구성요소를 뜻한다.
      • ex) PizzaStore
      • PizzaStore의 행동은 피자에 의해 정의되기 때문에 고수준 구성요소라고 할 수 있다.
    • 저수준 구성요소
      • 이 때 PizzaStore에서 사용하는 피자 객체들은 저수준 구성요소라고 할 수 있다.
  • 팩토리 패턴을 사용하지 않은 기존의 PizzaStore 클래스는 구상 피자 클래스들에 의존하고 있다.
  • dip 원칙에 의하면, 구상 클래스처럼 구체적인 것이 아닌 추상 클래스나 인터페이스와 같이 추상적인 것에 의존하는 코드를 만들어야 한다.
    • 이 원칙은 고수준 모듈과 저수준 모듈에 모두 적용될 수 있다.
  • dip 원칙을 지키는데 도움이 될만한 가이드라인
    1. 어떤 변수에도 구상 클래스에 대한 레퍼런스를 저장하지 않는다.
    2. 구상 클래스에서 유도된 클래스를 만들지 않는다.
    3. 베이스 클래스에 이미 구현되어 있던 메소드를 오버라이드 하지 않는다.
    • 해당 가이드들은 항상 지켜야 하는 규칙이 아니라 지향하는 바를 나타낸 것
    • 이 가이드라인을 완벽하게 따를 순 없다.
profile
동료들이 같이 일하고 싶어하는 백엔드 개발자가 되고자 합니다!

1개의 댓글

comment-user-thumbnail
2023년 11월 4일

깔끔하게 예시와 함께 정리되어있어 이해에 도움이 많이 되었습니다! 감사합니다.

답글 달기