Factory Pattern

3 minute read

팩토리 패턴

아래 예시에 사용된 코드는 
https://github.com/vsh123/springboot-test/tree/feat/factory-pattern
에서 확인 가능합니다.

느슨한 결함을 이용하는 객체지향 패턴

비즈니스 로직에 new를 사용하여 객체를 만드는 것은 좋은 것이 아니다

    Duck duck = new MallarDuck();
    /*
    인터페이스를 사용해서 코드를 유연하게 만들었지만,
    구상 클래스의 인스턴스를 만드는 new를 사용했기 때문에
    나중에 코드를 수정해야할 가능성이 높아지며
    유연성이 떨어진다
    */
  • 이를 해결하기 위해 비즈니스 로직은 인터페이스에 맞춰 코딩을 하고, 변하는 부분(구상 클래스는) 외부에서 주입해주는 형식으로 변경을 할 수 있다.

Example) 피자 주문 메소드

  • 아래와 같이 피자를 주문하면 만들어서 리턴해주는 메소드가 있습니다.
    //Pizza는 인터페이스
    private static Pizza orderPizza(String type) {
        Pizza pizza = null;
        if (type.equals("cheese")) {
            pizza = new CheesePizza();
        }
    		//만약 type이 늘어난다면 위 코드가 else-if가 계속해서 붙겠죠?
        pizza.prepare();
        pizza.bake();
        pizza.cut();
        pizza.box();
            
        return pizza;
    }
  • 위 코드에서 type이 추가되면 if/else-if 구문이 계속해서 늘어나는 문제점이 발생할 가능성이 존재합니다.

  • 이를 해결하기 위해 타입을 입력하면 리턴해주는 간단한 PizzaFactory클래스를 작성할 수 있습니다.

    //아래와 같이 객체의 생성을 담당해주는 클래스를 Factory 클래스라고 합니다.
    public class PizzaFactory {
        public static Pizza createPizza(String type) {
            if (type.equals("cheese")) {
                return new CheesePizza();
            }
            return null;
        }
    }
  • 변경된 orderPizza 메소드
    private static Pizza orderPizza(String type) {
        Pizza pizza = PizzaFactory.createPizza(type);
    
        pizza.prepare();
        pizza.bake();
        pizza.cut();
        pizza.box();
    
        return pizza;
    }

Q. 이와 같이 PizzaFactory를 만들었을 때의 장점?

  • Pizza가 늘어나도 orderPizza의 메소드는 변하지 않는 구조가 됩니다!
  • 해당 팩토리를 다른 메소드에서도 사용이 가능합니다!

Example) 지점 별로 만들어지는 피자가 다른 case

  • PizzaStore마다 같은 type을 입력하더라도 스타일이 다른 경우에 어떻게 구현하면 좋을까?

    → 기존 PizzaFactory의 createPizza()를 지점별로 구현해주면 될 것 같다!

  • 다음과 같이 PizzaStore 추상 클래스를 선언해 줍니다.

    public abstract class PizzaStore {
        public Pizza orderPizza(String type) {
            //PizzaFactory가 아닌 하위 Store에서 구현해주는 createPizza()를 사용
            Pizza pizza = createPizza(type);
    
            pizza.prepare();
            pizza.bake();
            pizza.cut();
            pizza.box();
    
            return pizza;
        }
    
        //하위 구상 클래스마다 만들어지는 방법이 조금씩 다르다
    		//해당 메소드는 팩토리 메소드라 할 수 있다.
        protected abstract Pizza createPizza(String type);
    }
  • 그리고 PizzaStore를 상속받아 구현한 SeoulPizzaStore, PusanPizzaStore를 다음과 같이 구현해준다.
    public class SeoulPizzaStore extends PizzaStore {
        @Override
        protected Pizza createPizza(String type) {
            if (type.equals("cheese")) {
                //서울식 치즈 피자
                return new SeoulCheesePizza();
            }
            return null;
        }
    }
    
    --------------------------------------------
    public class PusanPizzaStore extends PizzaStore {
    		@Override
        protected Pizza createPizza(String type) {
            if (type.equals("cheese")) {
                //부산식 치즈 피자
                return new PusanCheesePizza();
            }
            return null;
        }
    }

Q. 위와 같이 구성하였을 때의 장점?

  • 객체를 생성하는 작업을 서브클래스에서 정의함으로 인해 캡슐화 시킬 수 있습니다. 이로 인해 비즈니스 로직 시에는 슈퍼클래스에 있는 oderPizza()에만 신경쓰면 되는 이점이 있습니다.
  • 객체 생성 방식(위에서는 PizzaStore가 됩니다!)이 추가되더라도 PizzaStore의 변경 없이 하위 서브클래스만 추가하면 되기 때문에 OCP(개방-폐쇄 원칙)을 만족시킨다고 할 수 있습니다!

팩토리 메소드 패턴 : 위와 같이 서브클래스에서 어떤 클래스를 만들지를 결정하여 객체 생성을 캡슐화 하는 것

위와 같이 구현했을 때의 객체 의존성

connect

  • 위 보시는 사진과 같이 PizzaExample은 슈퍼클래스인PizzaStor와 Pizza에만 의존하고 있습니다. 이렇게 추상화된 클래스에 의존하는 것을 DIP(의존성 역전 원칙) 이라고 합니다.

의존성 역전 원칙 : 추상화된 것에 의존하도록 만들어라. 구상 클래스에 의존하도록 만들지 않도록 한다.

Q. 어떤 걸 역전시킨다는 것인가요?

  • 위의 다이어그램을 보면 저수준 구성요소(Seoul, PusanPizzaStore)가 고수준 추상클래스인 PizzaStore에 의존하고 있습니다. 이처럼 고수준이 저수준을 의존하는 것이 아닌 저수준이 고수준을 의존하게 만든 것을 의존성을 역전시켰다고 합니다.

DIP를 지키기 위한 가이드라인

  • 어떤 변수에도 구상 클래스에 대한 참조를 저장하지 마라(생성자를 이용한 생성을 하지말라)
  • 구상 클래스에서 유도된 클래스를 만들지 말라. 이는 곳 구상클래스를 의존하게 된다.
  • 베이스 클래스에 이미 구현되어 있던 메소드를 오버라이드 하지 마라 이는 곧 베이스 클래스가 제대로 추상화 되어있지 않다는 것이다.(상속보다 구성을 사용해라)

추상 팩토리 패턴

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

connect

Q. 추상 팩토리 vs 팩토리 메소드 패턴

  • 팩토리 메소드는 상속을 통해 객체를 생성, 추상 팩토리 패턴은 구성을 통해 생성
    //팩토리 메소드 패턴, 하위 구상클래스가 상속 받아 객체를 생성하는 메소드를 만든다
    public abstract class PizzaStore {
        public Pizza orderPizza(String type) {
            //피자 팩토리가 아닌 하위 Store에서 구현해주는 createPizza()를 사용
            Pizza pizza = createPizza(type);
    
            pizza.prepare();
            pizza.bake();
            pizza.cut();
            pizza.box();
    
            return pizza;
        }
    
        //하위 구상 클래스마다 만들어지는 방법이 조금씩 다르다
        protected abstract Pizza createPizza(String type);
    }
    //추상 팩토리 패턴 PizzaExample은 PizzaStore를 구성하고 있으며
    //pizza.orderPizza()를 통해 Pizza객체를 생성한다.
    public class PizzaExample {
        private PizzaStore pizzaStore;
    
        public PizzaExample(PizzaStore pizzaStore) {
            this.pizzaStore = pizzaStore;
        }
    
        public Pizza orderPizza(String type) {
            return pizzaStore.orderPizza(type);
        }
    }

Q. 그래서 둘의 사용 용도는..?

  • 추상 팩토리 패턴은 클라이언트에서 서로 연관된 일련의 제품을 만들어야 할 때(예를 들어 서울치즈피자에 들어가는 재료들) 사용하면 좋다
  • 팩토리 메소드 패턴은 클라이언트 코드와 인스턴스를 만들어야 할 구상 클래스를 분리시켜야 할 때 사용

Updated: