[헤드퍼스트 디자인패턴] Chapter 04 - 팩토리 패턴

뚱이·2023년 5월 8일
0
post-thumbnail

이번 챕터에서는 3가지 패턴 (정확히는 2개의 패턴 + 자주 쓰이는 관용구)에 대해서 알아보겠다.

0. 문제 상황

'new' 연산자가 눈에 띈다면 '구상'이라는 용어를 떠올려 주세요

new 연산자는 인스턴스(정확히는 구상 클래스의 인스턴스)를 생성하는 연산자이다.

다음과 같은 코드가 있다.

Duck duck;
if (picnic) {
	duck = new MallardDuck();
} else if (hunting) {
	duck = new DecoyDuck();
} else if (inBathTub) {
	duck = new RubberDuck();
}

위 코드를 보면,
구상 클래스의 인스턴스가 여러 개 있고,
그 인스턴스의 형식은 실행 시에 조건에 따라 결정된다 는 사실을 알 수 있다.

이런 식으로 코드를 짜면 관리와 갱신이 어렵고, 오류 발생 가능성도 높다.


과연 'new'가 문제일까?

new 연산자 자체에 문제가 있는 건 아니다. 당연히 최소 한 번은 사용한다.

우리가 주의해야 할 건 new가 아니라 변화 이다.
변화하는 무언가 때문에 new를 조심해서 사용해야 한다.

이전 챕터에서 본 OCP 이론 을 떠올려보자.
이 이론대로 new를 사용할 때에도 확장에는 열려있고, 변경에는 닫혀 있는 코드를 작성해야 한다.



1. 기존 코드

이번 챕터의 테마는 '피자'다 🍕
책에서는 피자 가게와 그 체인점들을 기준으로 설명했지만,
나에겐 조금 헷갈리는 경향이 없잖아 있어 여기선 여러 피자 브랜드를 기준으로 설명하겠다.

우리 동네 도미노 피자 가 있다.
도미노 를 운영하는 코드는 다음과 같다.

Pizza orderPizza(String type) {
	Pizza pizza;
    
    if (type.equals("cheese")) {
    	pizza = new CheesePizza();
    } else if (type.equals("potato") {
    	pizza = new PotatoPizza();
    } else if (type.equals("pepperoni") {
    	pizza = new PepperoniPizza();
    }
    
    pizza.prepare();
    pizza.bake();
    pizza.cut();
    pizza.box();
    return pizza;
}

위 코드에서 가장 문제가 되는 부분은 인스턴스를 만드는 구상 클래스를 선택하는 부분 이다.
만약 인기 없는 피자가 없어지거나 신메뉴가 출시되면 코드를 직접 수정해야 한다.

🤔 엥 몇 개 없어서 직접 수정해도 될 거 같은데요 ?

위 코드는 간략한 코드이기 때문에 지금으로선 직접 수정해도 크게 상관이 없겠지만,
만약 기존 메뉴가 100개에 단종된 메뉴가 20개, 새롭게 추가된 메뉴가 50개면 ?
지금 보는 메소드는 orderPizza() 뿐이지만 modifyPizza() , cancelPizza() 등 여러 메소드에서도 일일이 수정해야 하면 그래도 괜찮을까 ?

당근 아니다.

변하는 부분과 변하지 않는 부분을 합쳐서 몰아넣었기 때문에 이제부터 캡슐화를 진행해 볼 거다.

❔ 먼저 변하는 부분은?
⭕ 피자 종류를 선택하고 그 인스턴스를 생성하는 부분이다.

변하지 않는 부분은?
❌ 피자를 준비하고, 굽고, 커팅하고, 포장하는 과정은 모든 피자에 필요한 것이므로 변하지 않는다.



2. 간단한 팩토리 (Simple Factory)

'팩토리(Factory)'란?

먼저 팩토리(Factory)란 객체 생성을 처리하는 클래스를 의미한다.

말그대로 공장에 주문을 하면, 공장은 그 주문 받은 제품을 만들어 돌려주는 것이다.

그렇다면 우리는 피자 인스턴스를 팩토리를 활용해 생성할 수 있겠다 !


'간단한 팩토리(Simple Factory)'

간단한 팩토리는 정확히 말하자면 프로그래밍에서 자주 쓰이는 관용구에 가깝다.
디자인 패턴이 아니라는 얘기다 🙅‍♀️

'간단한 팩토리'의 클래스 다이어그램을 먼저 살펴보자.

SimplePizzaFactory 는 피자를 만드는 공장이다. 그렇기 때문에 여러 인스턴스를 생성할 수 있다.
다양한 피자 객체들은 Pizza 클래스를 구현함으로써 정의된다.
그리고 PizzaStore 는 공장에 주문을 하는 것처럼, SimplePizzaFactory를 사용해 구체적인 피자 객체를 구현한다.


코드로 살펴보기

팩토리를 적용해 기존 코드를 수정해보자.

[팩토리 코드]

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

[클라이언트 코드]

public class PizzaStore {
	SimplePizzaFactory factory;
    
    public PizzaStore(SimplePizzaFacotory factory) {
    	this.factory = factory;
    }
    
    pubic Pizza orderPizza(String type) {
    	Pizza pizza;
        
        pizza = factory.createPizza(type);
        
        pizza.prepare();
        pizza.bake();
        pizza.cut();
        pizza.box();
        
        return pizza;
    }
    
    // 기타 메소드
    
}

다양한 팩토리 만들기

원래 우리 동네에는 도미노 피자 만 있었는데, 이번에 피자헛 이랑 피자스쿨 이 새로 들어왔다.

당연히 각 브랜드는 자신만의 피자 만드는 방법이 있을 거고, 그에 따라 공장도 각기 다를 것이다. 그렇다면 이번엔 3개의 공장을 만들어야 하는 것인가?

한 번 만들어보자.

DominoPizzaFactory dominoFactory = new DominoPizzaFactory();
PizzaStore dominoStore = new PizzaStore(dominoFactory);
dominoStore.orderPizza("Potato");

PizzahutPizzaFactory pizzahutFactory = new PizzahutPizzaFactory();
PizzaStore pizzahutStore = new PizzaStore(pizzahutFactory);
pizzahutStore.orderPizza("Potato");

흠.

코드를 이렇게 짜니까 뭔가 맞는 거 같으면서도 잘못 한 거 같다.

이번에는 팩토리 메소드 패턴 에 대해 알아보자.



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

'팩토리 메소드 패턴'이란?

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

객체에서 생성할 때 필요한 인터페이스를 만든다.
어떤 클래스의 인스턴스를 만들지는 서브클래스에서 결정한다.
이 패턴을 사용하면 클래스 인스턴스 만드는 일을 서브클래스에게 맡긴다.

새로운 패턴이 등장했다 !

여기서 핵심은 서브 클래스 이다.

추상 클래스를 정의하고, 이 추상 클래스를 구현한 구상 클래스(서브 클래스)에서 객체를 생성한다는 것이다.

이 때 주의해야 할 건,
실행 중에 서브클래스에서 어떤 클래스의 인스턴스를 만들지를 결정한다는 게 아니다. 🙅‍♀️
사용하는 서브클래스에 따라 생산되는 객체 인스턴스가 결정되는 것이다. 🙆‍♀️


[피자 가게] 클래스 다이어그램

팩토리 메소드 패턴 에서 중요했던 건 서브 클래스 이다.

위 클래스 다이어그램을 살펴보면,
추상 Creator 클래스인 PizzaStore 에서는 어떤 종류의 피자가 만들어지는지 미리 알 수 없다.
그리고 추상 Creator 클래스의 서브클래스인 DominoPizzaStorePizzahutPizzaStore 에서 각자 고유의 피자를 만들 수 있다.
( NYPizzaStore -> DomoniPizzaStore, ChicagoPizzaStore -> PizzahutPizzaStore )

그리고 createPizza()orderPizza() 를 분리했다.

피자를 주문하면, 피자 종류를 결정하고 / 준비하고 / 굽고 / 자르고 / 포장한다.
이 때 준비하고 / 굽고 / 자르고 / 포장하는 단계는 공통이고,
피자 종류를 결정하는 것만 달라진다.
그리고 피자 종류를 결정하는 이 부분은 createPizza() 함수로 따로 구현했다.


[피자 가게] 코드로 살펴보기

[팩토리 메소드]

public abstact class PizzaStore {
	
    public Pizza orderPizza(String type) {
    	Pizza pizza;
        
        pizza = createPizza(type);
        
        pizza.prepare();
        pizza.bake();
        pizza.cut();
        pizza.box();
        
        return pizza;
    }
    
    protected abstract Pizza createPizza(String type);
    
    // 기타 메소드
    
}

---

public class DominoPizzaStore extends PizzaStore {
	
    Pizza createPizza(String item) {
    	if (item.equals("cheese")) {
    		return new DominoCheesePizza();
    	} else if (item.equals("potato") {
    		return new DominoPotatoPizza();
    	} else if (item.equals("pepperoni") {
    		return new DominoPepperoniPizza();
    	} else return null;
    } 
    
}

[Pizza 클래스]

public abstract class Pizza {
	String name;
    String dough;
    String sauce;
    List<String> toppings = new ArrayList<String>();
    
    void prepare() {
    	System.out.println("피자 준비 중");
        // 피자 이름 출력
        // 도우 종류 출력
        // 소스 종류 출력
        // 토핑들 출력
    }
    void bake() {
    	System.out.println("피자 굽는 중");
    }
    void cut() {
    	System.out.println("피자 사선으로 자르는 중");
    }
    void box() {
    	System.out.println("피자 포장하는 중");
    }
    public String getName() {
    	return name;
    }
}

---

public class DominoPotatoPizza extends Pizza {
	
    public DominoPotatoPizza() {
    	name = "도미노 포테이토 피자";
        dough = "씬 크로스트 도우";
        sauce = "기본 토마토 소스";
        
        toppings.add("베이컨", "감자", "치즈");
    }
    
}

[테스트 코드]

public class PizzaTestDrive {
	
    public static void main(String[] args) {
    	PizzaStore dominoStore = new DominoPizzaStore();
        PizzaStore pizzahutStore = new PizzahutPizzaStore();
        
        Pizza pizza = dominoStore.orderPizza("potato");
        // 주문 정보 출력
        
        pizza = pizzahutStore.orderPizza("potato");
        // 주문 정보 출력
    }
    
}

[피자 가게] 정리: 피자가 만들어지기까지

01 어디서 주문할지 가게 선택

PizzaStore dominoStore = new DominoPizzaStore();

02 피자 주문

dominoStore.orderPizza("potato");

03 orderPizza() 메소드에서 createPizza() 메소드 호출

Pizza pizza = createPizza("potato");

04 이제 피자 완성

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


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

의존성 뒤집기 원칙

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

추상화된 것에 의존하게 만들고, 구상클래스에 의존하지 않게 만든다.

언뜻 보면 "구현보다는 인터페이스에 맞춰서 프로그래밍한다" 와 비슷해보인다.

그러나 의존성 뒤집기 원칙 에는
1. 고수준 구성 요소저수준 구성 요소에 의존하면 안 되고
2. 항상 추상화에 의존하게 만들어야 한다
는 뜻이 담겨 있다.

앞서 보았던 피자 상황에서는,
피자에 의해 행동이 정의되는 PizzaStore고수준 구성 요소이고,
Pizza 클래스는 저수준 구성 요소이다.

다이어그램으로 살펴보면 다음과 같다.

PizzaStore가 모든 피자 객체에 직접 의존하는 상황을,
의존성 뒤집기 원칙을 적용하면

PizzaStore추상 클래스Pizza 클래스에만 의존하는 상황으로 바뀐다.
그리고 모든 구상 피자 클래스들도 Pizza 클래스에 의존한다.


'추상 팩토리 패턴'이란?

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

구상 클래스에 의존하지 않고도 서로 연관되거나 의존적인 객체로 이루어진 제품군을 생산하는 인터페이스를 제공한다.
구상 클래스는 서브클래스에서 만든다.

이 패턴의 핵심은 제품군 이다.


[피자 가게] 정리: 피자가 만들어지기까지

01 어디서 주문할지 가게 선택

PizzaStore dominoStore = new DominoPizzaStore();

02 피자 주문

dominoStore.orderPizza("potato");

03 orderPizza() 메소드에서 createPizza() 메소드 호출

Pizza pizza = createPizza("potato");

04 createPizza()로 인해 원재료 팩토리 실행

Pizza pizza = new PotatoPizza(dominoIngredientFactory);

05 피자 준비

void prepare() {
	dough = factory.createDough();
    sauce = factory.createSauce();
    cheese = factory.createCheese();
}


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

팩토리 메소드 패턴


추상 팩토리 패턴



6. 정리

객체지향 기초

  • 추상화
  • 캡슐화
  • 다형성
  • 상속

객체지향 원칙

  • 바뀌는 부분은 캡슐화 한다.
  • 상속보다는 구성을 활용한다.
  • 구현보다는 인터페이스에 맞춰서 프로그램이한다.
  • 상호작용하는 객체 사이에서는 가능하면 느슨한 결합을 사용해야 한다.
  • OCP: 클래스는 확장에는 열려 있어야 하지만 변경에는 닫혀 있어야 한다.
  • 추상화다ㅚㄴ 것에 의존하게 만들고 구상 클래스에 의존하지 않게 만든다.

객체지향 패턴

전략 패턴

전략 패턴은 알고리즘군을 정의하고 캡슐화해서 각각의 알고리즘군을 수정해서 쓸 수 있게 한다.
전략패턴을 사용하면 클라이언트로부터 알고리즘을 분리해서 독립적으로 변경할 수 있다.

옵저버 패턴

한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체에게 연락이 가고 자동으로 내용이 갱신되는 방식으로, 일대다 (one-to-many) 의존성을 정의한다.

데코레이터 패턴

객체에 추가 요소를 동적으로 더할 수 있다.
데코레이터를 사용하면 서브 클래스를 만들 때보다 훨씬 유연하게 기능을 확장할 수 있다.

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

객체에서 생성할 때 필요한 인터페이스를 만든다.
어떤 클래스의 인스턴스를 만들지는 서브클래스에서 결정한다.
이 패턴을 사용하면 클래스 인스턴스 만드는 일을 서브클래스에게 맡긴다.

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

구상 클래스에 의존하지 않고도 서로 연관되거나 의존적인 객체로 이루어진 제품군을 생산하는 인터페이스를 제공한다.
구상 클래스는 서브클래스에서 만든다.

0개의 댓글