본문 바로가기

디자인 패턴

1. Factory Pattern

개발을 진행하다보면 다음과 같이 일련의 코드를 만들어야 하는 경우가 생긴다.

Pizza pizza;
if (type.equals("cheeze")) pizza = new CheezePizza();
else if (type.equals("ham")) pizza = new HamPizza();
else if (type.equals("bacon")) pizza = new BaconPizza();

이러한 코드가 존재한다는 것은 유연성이나 확장성에서 크나큰 문제를 야기할수 있다.

왜냐하면, 확장하거나 축소해야 할때, 코드를 다시 추가하거나 제거해야 함을 의미하기 때문이다.

 

인터페이스에 맞춰서 코딩을 하면 시스템에서 일어날 수 있는 여러 변화를 이겨낼 수 있다. 왜냐하면..

다형성 덕분에 어떤 클래스든 특정 인터페이스만 구현하면 사용할수 있기 때문이다.

반대로. 구상 클래스를 많이 사용하면 새로운 구상 클래스가 추가될 때마다 코드를 고쳐야 하기때문에 많은 문제가 생길수 있다.

즉 변화에 대해 닫혀 있는 코드가 되어버리는 것이다.

 

디자인 원칙으로 봤을때 구상 클래스를 바탕으로 코딩을 하면 나중에 코드를 수정해야 할 가능성이 높아지고, 유연성이 떨어진다는걸 다시한번 확인했는데 그렇다면 회피할수 있는 방법은 무엇일까?

- 바뀔 수 있는 부분을 찾아내서 바뀌지 않는 부분하고 분리시켜야 한다는 원칙이다.

 

Pizza orderPizza(String type) { 
    Pizza pizza;
    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;
}

그렇다면 어떤 방식을 사용할수 있을까 ? 바뀌는 영역인 type을 통해 객체를 생성하는 영역을 따로 관리하는 곳에 두어 사용하면 되지 않을까?

 

class SimplePizzaFactory {
    public Pizza createPizza(String type) {
        if(type.equals("cheese")) pizza = new CheesePizza();
        else if(type.equals("greek")) pizza = new GreekPizza();
        else if(type.equals("pepperoni")) pizza = new PepperoniPizza();
    }
}

class KoreaPizzaFactory {
    public Pizza createPizza(String type) {
        if(type.equals("kimchi")) pizza = new KimchiPizza();
    }
}

class EnglandPizzaFactory {
    public Pizza createPizza(String type) {
        if(type.equals("beef")) pizza = new BeefPizza();
    }
}

class AmericaPizzaFactory {
    public Pizza createPizza(String type) {
        if(type.equals("steak")) pizza = new SteakPizza();
    }
}


public class PizzaStore {
    SimplePizzaFactory simplePizzaFactory;

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


    public Pizza orderPizza(String type) {
        Pizza pizza;

        pizza = simplePizzaFactory.createPizza(type);

        pizza.prepare();
        pizza.bake();
        pizza.cut();
        pizza.box();

        return pizza;
    }
}

Factory라는 공간을 만들어 피자를 만드는 일을 위임하였다.

하지마 이 케이스도 역시 문제는 존재한다. 문제는 다름아니라, cheeze만드는 방식에 있어서도 차이가 발생할수 있다는것이다.

 

한국에서는 치즈를 2개만 넣을수도 있고, 미국은 치즈를 5개 넣을수가 있다. 하지만 그럴 경우 코드는 다음과 같은 형태를 띄게 된다.

PizzaStore pizzaStore = new PizzaStore(new KoreaPizzaFactory());
pizzaStore.orderPizza("cheeze;");

PizzaStore pizzaStore = new PizzaStore(new AmericaPizzaFactory());
pizzaStore.orderPizza("cheeze;");

그렇다면 지점이 전세계 국가만큼 생성된다면? 국가에서도 지역마다 다르다면 ? 내부적으로 무한이 늘어날수 있고 지금같은 상황에서 확장성은 극도로 떨어지게 된다.

 

해결하기 위해서 무엇을 해야할까?

일단 동적으로 변화하는 부분을 먼저 체크해보자. 동작으로 변화하는 부분은 createPizza 즉 피자를 만드는 부분이다.

해당 부분을 실제로 실현하는 구현체가 가져가서 구현을 하면 어떨까 ?

그렇다면 생각할수 있는 부분은 2가지이다. 이미 변화되지 않는 부분인 orderPizza는 가져가고 변화되는 부분인 createPizza는 interface처럼 메소드만 가져가는 것이다. class + interface 먼저 떠오르지 않는가? 추상 클래스이다.

public abstract class PizzaStore {
    public Pizza orderPizza(String type) {
        Pizza pizza;

        pizza = simplePizzaFactory.createPizza(type);

        pizza.prepare();
        pizza.bake();
        pizza.cut();
        pizza.box();

        return pizza;
    }

    abstract Pizza createPizza(String type);
}

추상클래스를 활용하여 변화할수 있는 부분인 createPizza는 메소드만 선언하는 것이다.

 

public class KoreaPizzaStore extends PizzaStore {
    @Override
    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;
    }
}

한국 피자점이다. PizzaStore를 상속하여 기존에 사용하던 양식인 orderPizza 메소드를 사용하고 변화된 형식에 맞게 createPizza 메소드는 수정한다.

 

결국 각 피자의 스타일은 PizzaStore를 상속하는 서브클래스에서 결정하게 된다.

이러한 방식을 팩토리 메소드 패턴 이라고 칭한다

또한 모든 팩토리 패턴에서는 객체 생성을 캡슐화 한다.

팩토리 메소드 패턴에서는 서브 클래스에서 어떤 클래스를 만들지를 결정하게 함으로써 객체 생성을 캡슐화 한다.

 

위와 같은 방식이 궁극적으로 추구하는 것은 구상클래스에 대한 의존성을 줄이는 것이다.

그리고 구상 클래스에 대한 의존성을 줄이는 것을 가능케 한 요소는 바로 다형성이다.

 

다형성이란 서브클래스가 상위 클래스로 선언될수 있는 것을 의미한다. 그리고 이는 위와 같은 사례처럼 프로그램에 큰 유연성과 확장성을 부여한다.

위와 같인 다형성과 추상클래스의 활용을 통해 구상 클래스에 대한 의존성을 줄이는 것이 좋다는 것은 이제 확실해졌다.

이런 내용 정리해 놓은 객체지향 디자인 원칙이 바로 의존성 뒤집기 원칙이다.

 

디자인 원칙

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

 

왜 의존성 뒤집기 원칙이냐면...

 

PizzaStore -> NYStyleCheesePizza

PizzaStore -> ChicagoStypeCheesePizza

PizzaStore -> NYStyleVeggiePizza

 

이런식으로 의존이 되던 좋지않은 디자인이

 

PizzaStore -> Pizza

Pizza <- NYStyleCheesePizza

Pizza <- ChicagoStyleCheesePizza

Pizza <- NYStyleVeggiePizza

 

팩토리 메소드 패턴을 적용하고 나면 고수준 구성요소(PizzaStore)와 저수준 구성요소(NYStyleCheesePizza, ChicagoStylePizza, ..) 들이 모두 추상 클래스인 Pizza에 의존하게됨. (고수준 모듈과 저수준 모듈이 둘다 하나의 추상 클래스에 의존)

팩토리 메소드 패턴이 의존성 뒤집기 원칙을 준수하기 위해 쓸 수 있는 유일한 기법은 아니지만 가장 적합한 벙법 가운데 하나이다.

 

의존성 뒤집기 원칙에 위배되는 객체지향 디자인을 피하는데 도움이 되는 가이드이다.

 1. 어떤 변수에도 구상 클래스에 대한 레퍼런스를 지정하지 않는다.

     - new 연산자를 사용하면 레퍼런스를 사용하게 되는 것.

 

 2. 구상 클래스에서 유도된 클래스를 만들지 않는다.

     - 구상클래스에서 유도된 클래스를 만들면 특정 구상 클래스에 의존하게됨, 추상화 된것을 사용해야 함.

 

 3. 베이스 클래스에 이미 구현되어 있던 메소드를 오버라이드 하지 않는다.

     - 이미 구현되어 있는 메소드를 오버라이드 한다는 것은 애초부터 베이스 클래스가 제대로 추상화 된것이 아니었다고

        볼수있다. 베이스 클래스에서 메소드를 정의할 때는 모든 서브 클래스에서 공유할 수 있는 것만 정의해야함.

위의 가이드 라인은 다른 원칙들과 마찬가지로 항상 지켜야 하는 규칙이 아니라. 지향해야하는 바를 밝히는 것.

String 인스턴스는 사실 별생각 없이 쓰는데 엄밀히 말하자면 이것도 원칙에 위배되는 것이지만 별문제가 되지않는다. 왜냐하면 String 클래스가 바뀌는 일의 거의 없을테니까. 하지만 자신이 만들고 있는 클래스가 바뀔 가능성이 있다면 팩토리 메소드 패턴 같은 기법을 써서 변경될 수 있는 부분을 캡슐화 하여야 한다.

 

이렇게 PizzaStore 디자인이 모양새를 갖췄다. 유연한 프레임워크도 만들어 졌고, 디자인 원칙도 충실하게 지켰다.

각각 체인점들이 미리 정해놓은 절차를 잘 따르고 있지만 몇몇 체인점들이 자잘한 재료를 더 싼 재료로 바꿔서 원가를 절감해 마진을 남기고 있다. 원재료의 품질까지 관리하는 방법이 있을까??

     - 원재료 군을 만들어 파악하자.

       제품에 들어가는 재료군(반죽, 소스, 치즈, 야채, 고기)은 같지만, 지역마다 재료의 구체적인 내용이 조금씩 다르다.

 

원재료 공장을 만들어보자.

 

1. 지역별로 팩토리를 만들어 각 생성 메소드를 구현하는 PizzaingredientFactory 클래스를 만들어야 함.

2. ReggianoCheese, RedPeppers, ThickCrustDough와 같이 팩토리에서 사용할 원재료 클래스들을 구현한다.

3. 만든 원재료 공장을 PizzaStore 코드에서 사용하도록 함으로써 모든 것을 하나로 묶어준다. 

 public interface PizzaIngredientFactory {
    public Dough createDough();
    public Sauce createSauce();
    public Cheese createCheese();
    public Veggies[] createVeggies();
    public Pepperoni createPepperoni();
    public Clams createClams(); 
}
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();   
    }  
    
    @Override  
    public Veggies[] createVeggies() {
        Veggies veggies[] = { 
                new Farlic(), new Onion(), new Mushroom(), new RedPepper() 
        };    
        return veggies;    
    }  
    
    @Override  
    public Pepperoni createPepperoni() {   
        return new SlicedPepperoni();  
    }  
    
    @Override  
    public Clams createClams() {
        return new Freshclams();   
    } 
}
public class CheesePizza extends Pizza{ 
    PizzaIngredientFactory ingredientFactory;  
    
    public CheesePizza(PizzaIngredientFactory ingredientFactory) {
        this.ingredientFactory = ingredientFactory;    
    }
    
    @Override  public void prepare() {    
        this.dough = ingredientFactory.createDough();
        this.sauce = ingredientFactory.createSauce();
        this.cheese = ingredientFactory.createCheese();    
    } 
}

이제 전체적인 흐름은.

 

1. 뉴욕 피자가게를 만든다.

     - PizzaStore nyPizzaStore = new NYPizzaStore();

 

2. 주문을 한다.

     - nyPizzaStore.orderPizza("cheese");

 

3. orderPizza 메소드에서는 우선 createPizza() 메소드를 호출한다

     - Pizza pizza = createPizza("cheese");

 

4. createPizza() 메소드가 호출되면 원재료 공장이 돌아가기 시작한다.

     - Pizza pizza = new CheesePizza(nyIngredientFactory);

 

5. 피자를 준비하는 prepare()메소드가 호출되면 팩토리에 원재료 주문이 들어간다.

     - void prepare(){

            dough = nyIngredientFactory.createDough();

            sauce = nyIngredientFactory.createSauce();

            cheese = nyIngredientFactory.createCheese();

       }

 

6. 준비단계가 끝나고 orderPizza() 메소드에서는 피자를 굽고, 자르고, 포장한다.

 

다시 한번 정리를 해보면..

 추상 팩토리 패턴 : 제품군을 생성하기 위한 인터페이스를 생성 그 인터페이스를 구성하여 사용할수 있게끔 하는것.

 추상 메소드 패턴 : 하나의 추상클래스에서 추상 메소드를 만들고 서브클래스들이

                          그 추상메소드를 구현하여 인스턴스를 만들게끔 하는것이다.

'디자인 패턴' 카테고리의 다른 글

Worker Thread Pattern  (0) 2022.08.24
데코레이터(Decorator) 패턴  (0) 2022.07.31
4. 전략 패턴  (0) 2022.07.14
3. 이터레이터 패턴  (0) 2022.07.12
2. Adapter Pattern  (0) 2022.07.08