04. 팩토리 패턴 - 객체지향 빵 굽기

솔트커피·2022년 7월 11일
0

느슨한 결합으로 객체지향 디자인을 만들어 봅시다.

new 연산자를 사용한다고 해서 새로운 객체가 만들어지는 것은 아닙니다.

객체의 인스턴스를 만드는 작업이 항상 공개되어야 하는 것은 아니며, 오히려 모든 것을 공개했다가는 결합 문제가 생길 수 있다는 사실을 배웁니다.

팩토리 패턴으로 불필요한 의존성을 없애서 결합 문제를 해결하는 방법을 알아봅시다.

질문

특정 구현을 바탕으로 프로그래밍 하지 않아야 한다는 원칙을 배웠는데, new를 쓸 때마다 결국은 특정 구현을 사용하게 되는 것 아닌가요?

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

new를 사용하면 구상 클래스의 인스턴스가 만들어집니다. 당연히 인터페이스가 아닌 특정 구현을 사용하는 거죠.

정말 좋은 질문입니다. 앞에서 구상 클래스를 바탕으로 코딩하면 나중에 코드를 수정해야 할 가능성이 커지고, 유연성이 떨어진다고 배웠었죠.

일련의 구상 클래스가 있다면 어쩔 수 없이 다음과 같은 코드를 만들어야 합니다.

// 오리를 나타내는 클래스는 여러가지 있지만 컴파일 하기 전까지는
// 어떤 것의 인스턴스를 만들어야 하는지 알 수 없습니다.

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

이 코드를 보면 구성 클래스의 인스턴스가 여러 개 있으며, 그 인스턴스의 형식은 실행 시에 주어진 조건에 따라 결정된다는 사실을 알 수 있습니다.

이런 코드를 변경하거나 확장해야 할 때는 코드를 다시 확인하고 새로운 코드를 추가하거나 기존 코드를 제거해야 합니다.

따라서 코드를 이런 식으로 만들면 관리와 갱신이 어려워지고 오류가 생길 가능성도 커집니다.

'new'에는 어떤 문제가 있는 걸까요? (p145)

사실 new는 문제가 없습니다. 진짜 말썽을 일으키는 녀석은 바로 '변화'입니다.

변화하는 무언가 때문에 new를 조심해서 사용해야 합니다.

인터페이스에 맞춰서 코딩하면 시스템에서 일어날 수 있는 여러 변환에 대응할 수 있습니다. 왜 그럴까요?

인터페이스를 바탕으로 만들어진 코드들은 어떤 클래스든 특정 인터페이스만 구현하면 사용할 수 있기 때문이죠. 이게 다 다형성 때문입니다.

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

변경에 닫혀 있는 코드가 되는 거죠. 새로운 구상 형식을 써서 확장해야 할 때는 어떻게서든 다시 열 수 있게 만들어야 합니다.

우리가 배운 첫 번째 디자인 원칙을 떠올려 보세요. 바뀌는 부분을 찾아내서 바뀌지 않는 부분과 분리해야 한다는 원칙, 기억나죠?

피자 코드 추가하기(p147)

orderPizza() 메소드에서 가장 문제가 되는 부분은 인스턴스를 만드는 구상 클래스를 선택하는 부분입니다.

이 부분 때문에 상황이 변하면 코드를 변경해야하니 캡슐화를 할 차례군요

객체 생성 부분 캡슐화하기(p148)

새로 만들 객체를 팩토리라고 부르겠습니다.
'객체 생성을 처리하는 클래스를 팩토리(Factory)라고 부릅니다.

일단 SimplePizzaFactory 클래스를 만들고 나면 orderPizza() 메소드는 새로 만든 객체의 클라이언트가 됩니다. 즉 새로 만든 객체를 호출하는 거죠.

피자가 필요할 때마다 피자 공장에 피자 하나 만들어 달라고 부탁한다고 생각하면 됩니다. 이제 더 이상 orderPizza() 메소드에서 어떤 피자를 만들지 고민하지 않아도 됩니다.

orderPizza() 메소드는 Pizza 인터페이스를 구현하는 피자를 받아서 그 인터페이스에서 정의했던 prepare(), bake(), cut(), box() 메소드를 호출하기만 하면 되죠.

하지만 아직 몇 가지 자질구레한 사항을 해결해야 합니다. 예를 들어 orderPizza() 메소드에서 객체를 생성하는 부분에 썼던 코드 대신 들어갈 코드 같은 것 말이죠.

피자 가게에서 사용할 만한 간단한 팩토리 클래스를 구현한 후 어떻게 할지 생각해 봅시다.

객체 생성 팩토리 만들기(p149)

Q1. 이렇게 캡슐화하면 무슨 장점이 있는 건가요? 아까문제를 그냥 다른 객체로 넘겨버렸을 뿐인 것 같은데요?

SimplePizzaFactory를 사용하는 클라이언트가 매우 많을 수도 있습니다.

여기에는 orderPizza() 메소드만 있지만, 피자 객체를 받아서 피자를 설명하거나 가격을 알려주는 PizzaShopMenu 클래스와

PizzaStore 클래스와는 조금 다른 방식으로 피자 주문을 처리하는 HomeDelivery 클래스에도 이 팩토리를 사용할 수 있습니다.

그런 상황에서 피자 객체 생성 작업을 팩토리 클래스로 캡슐화해 놓으면 구현을 변경할 때 여기저기 고칠 필요 없이 팩토리 클래스 하나만 고치면 되겠죠.

그리고 조금 있으면 클라이언트 코드에서 구상 클래스의 인스턴스를 만드는 코드를 전부 없애 버릴 겁니다.

Q2. 팩토리 정적 메소드로 선언한 디자인을 본 적 있어요. 그 디자인이랑 어떻게 다른거죠?

간단한 팩토리를 정적 메소드로 정의하는 기법도 많이 쓰입니다. 정적 팩토리(static factory)라고 부르기도 하죠.

왜 정적 메소드를 쓰는지 궁금할 텐데, 객체 생성 메소드를 실행하려고 객체의 인스턴스를 만들지 않아도 되기 때문입니다.

하지만 서브 클래스를 만들어서 객체 생성 메소드의 행동을 변경할 수 없다는 단점이 있다는 점도 꼭 기억해두세요.

'간단한 팩토리'의 정의 (151p)

간단한 팩토리(Simple Factory)는 디자인 패턴이라기 보다는 프로그래밍에서 자주 쓰이는 관용구에 가깝습니다.

엄연히 말하면 패턴은 아니지만 중요하지 않은 건 아닙니다. 새로 만든 피자 가계의 클래스 다이어그램을 살펴보면서 간단한 팩토리를 왜 쓰는지 알아봅시다.

간단한 팩토리는 일종의 워밍업이라고 생각합니다.

주의
디자인 패턴을 얘기할 때, "인터페이스를 구현한다"라는 표현이 나온다고 해서 항상

"클래스를 선언하는 부분에 implements 키워드를 써서 어떤 자바 인터페이스를 구현하는 클래스를 만든다" 라고 생각하면 안됩니다.

어떤 상위 형식(클래스와 인터페이스)에 있는 구상 클래스는 그 상위 형식의 '인터페이스를 구현하는' 클래스 라고 생각하면 됩니다.

다양한 팩토리 만들기(p152)

이미 1가지 방법을 앞에서 배웠습니다.
SimplePizzaFactory를 삭제하고, 3가지 서로 다른 팩토리(NYPizzaFactory, ChicagoPizzaFactory, CalifornizaPizzaFactory)를 만든 다음, PizzaStore에서 적당한 팩토리를 사용하도록 하는 방법입니다.

하지만 지점들을 조금 더 제대로 관리해야 합니다.
지점에서 우리가 만든 팩토리로 피자를 만들긴 하는데, 굽는 방식이 달라진다거나 종종 피자를 자르는 것을 까먹는 일이 생기기 시작했습니다.

심지어 이상하게 생긴 피자 상자를 쓰는 일도 있었습니다.

이 문제를 해결하려면 PizzaStore와 피자 제작 코드 전체를 하나로 묶어주는 프레임워크를 만들어야 합니다. 물론 그렇게 만들면서도 유연성은 잃어버리면 안되겠죠.

SimplePizzaFactory를 만들기 전에 썼던 코드에는 피자를 만드는 코드가 PizzaStore와 직접 연결되어 있긴 했지만, 유연성이 전혀 없었습니다.

어떻게 해야 피자 가게와 피자 만드는 과정을 하나로 묶을 수 있을까요?

피자 가게 프레임워크 만들기(p154)

피자 만드는 일 자체는 전부 PizzaStore 클래스에서 진행하면서도 지점의 스타일을 살릴 수 있는 방법이 있습니ㅏ다.

이제 createPizza() 메소드를 PizzaStore에 다시 넣겠습니다. 하지만 이번에는 그 메소드를 추상 메소드로 선언하고, 지역별 스타일에 맞게 PizzaStore의 서브클래스를 만들겠습니다.

이제 각 지점에 맞는 서브 클래스를 만들어야 합니다. 피자의 스타일은 각 서브클래스에서 결정합니다.

서브클래스가 결정하는 것 알아보기(p155)

달라지는 점은 createPizzaa() 메소드에 넣고 그 메소드에서 해당 스타일의 피자를 만들도록 할 계획입니다.

그러니 PizzaStore의 서브 클래스에서 createPizza() 메소드를 구현하겠습니다. 

이제 PizzaStore 프레임워크에 충실하면서도 각각의 스타일을 제대로 구현할 수 있는 orderPizza() 메소드를 PizzaStore 서브클래스에 구비할 수 있습니다.

팩토리 메소드 선언하기

public abstract class PizzaStore {
 
	abstract Pizza createPizza(String item); // Pizza 인스턴스를 만드는 일은 이제 팩토리 메소드에서 맡아서 처리합니다.
 
	public Pizza orderPizza(String type) { // 피자 객체 인스턴스를 만드는 일은 PizzaStore의 서브클래스(NYStylePizzaStore, ChicagoStylePizzaStore 등등)에 있는 createPizza() 메소드에서 처리합니다.
		Pizza pizza = createPizza(type);
		System.out.println("--- Making a " + pizza.getName() + " ---");
		pizza.prepare();
		pizza.bake();
		pizza.cut();
		pizza.box();
		return pizza;
	}
}

만들어지기까지(p161)

// 1. 에단이 주문한 내역을 따라가 봅시다. 우선 NYPizzaStore가 필요합니다.
PizzaStore nyPizzaStore = new NYPizzaStore();

// 2. 피자 가게가 확보됐으니 이제 주문을 받을 수 있습니다.
nyPizzaStore.orderPizza("cheese");

// 3. orderPizza() 메소드에서 createPizza() 메소드를 호출합니다.
Pizza pizza = createPizza("cheese");

// 4. 아직 준비되지 않은 피자를 받았습니다. 이제 피자를 만드는 작업을 마무리 해야겠죠
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();

Pizza 클래스 만들기(P162)

public abstract class Pizza {
	String name;
	String dough;
	String sauce;
	ArrayList<String> toppings = new ArrayList<String>();
 
	void prepare() {
		System.out.println("Prepare " + name);
		System.out.println("Tossing dough...");
		System.out.println("Adding sauce...");
		System.out.println("Adding toppings: ");
		for (String topping : toppings) {
			System.out.println("   " + topping);
		}
	}
  
	void bake() {
		System.out.println("Bake for 25 minutes at 350");
	}
 
	void cut() {
		System.out.println("Cut the pizza into diagonal slices");
	}
  
	void box() {
		System.out.println("Place pizza in official PizzaStore box");
	}
 
	public String getName() {
		return name;
	}

	public String toString() {
		StringBuffer display = new StringBuffer();
		display.append("---- " + name + " ----\n");
		display.append(dough + "\n");
		display.append(sauce + "\n");
		for (String topping : toppings) {
			display.append(topping + "\n");
		}
		return display.toString();
	}
}

구상 서브 클래스

public class NYStyleCheesePizza extends Pizza {

	public NYStyleCheesePizza() { 
		name = "NY Style Sauce and Cheese Pizza";
		dough = "Thin Crust Dough";
		sauce = "Marinara Sauce";
 
		toppings.add("Grated Reggiano Cheese");
	}
}

최첨단 피자 코드 테스트(p164)

public class PizzaTestDrive {
 
	public static void main(String[] args) {
		PizzaStore nyStore = new NYPizzaStore();
		PizzaStore chicagoStore = new ChicagoPizzaStore();
 
		Pizza pizza = nyStore.orderPizza("cheese");
		System.out.println("Ethan ordered a " + pizza.getName() + "\n");
 
		pizza = chicagoStore.orderPizza("cheese");
		System.out.println("Joel ordered a " + pizza.getName() + "\n");

		pizza = nyStore.orderPizza("clam");
		System.out.println("Ethan ordered a " + pizza.getName() + "\n");
 
		pizza = chicagoStore.orderPizza("clam");
		System.out.println("Joel ordered a " + pizza.getName() + "\n");

		pizza = nyStore.orderPizza("pepperoni");
		System.out.println("Ethan ordered a " + pizza.getName() + "\n");
 
		pizza = chicagoStore.orderPizza("pepperoni");
		System.out.println("Joel ordered a " + pizza.getName() + "\n");

		pizza = nyStore.orderPizza("veggie");
		System.out.println("Ethan ordered a " + pizza.getName() + "\n");
 
		pizza = chicagoStore.orderPizza("veggie");
		System.out.println("Joel ordered a " + pizza.getName() + "\n");
	}
}

팩토리 메소드 패턴 살펴보기(p165)

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

생산자(Creator) 클래스

  • 추상 생산자 클래스는 나중에 서브클래스에서 제품(객체)을 생산하려고 구현하는 팩토리 메소드(추상 메소드)를 정의합니다.
  • 생산자 클래스에 추상 제품 클래스에 의존하는 코드가 들어있을 때도 있습니다.
    이 제품 클래스의 객체는 클래스의 서브클래스에 의해 만들어지죠. 생산자 자체는 어떤 구상 제품 클래스가 만들어질지 미리 알 수 없습니다.
  • 여기에서는 createPizza() 메소드가 팩토리 메소드입니다. 이 메소드에서 제품(객체)을 생산하죠.
  • 각 분점마다 PizzaStore의 서브클래스가 따로 있으므로 createPizza() 메소드 구현을 활용해서 그 가게 고유의 피자를 마음대로 만들 수 있습니다.
  • 제품을 생산하는 클래스는 구상 생산자(concrete creator)라고 부릅니다.

제품(Product) 클래스

  • 팩토리는 제품을 생산합니다. PizzaStore는 Pizza를 만들죠.
  • NYStyleCheesePizza, NYStylePerpperoniPizza ... 등은 피자 가게에서 만들어지는 피자 구상 클래스입니다.

병렬 클래스 계층 구조 알아보기(p166)

  • Pizza 제품 클래스와 PizzaStore 생산자 클래스는 둘다 추상 클래스로 시작하고, 그 클래스를 확장하는 구상 클래스들을 가지고 있습니다.
    그리고 뉴욕 지점과 시카고 지점의 구체적인 구현은 구상 클래스들이 책임지고 있죠.
  • NYPizzaStore에는 뉴욕 스타일 피자를 만드는 모든 방법이 캡슐화 되어 있습니다.
  • 팩토리 메소드는 이러한 방법을 캡슐화하는 데 있어서 가장 핵심적인 역할을 맡고 있습니다.

팩토리 메소드 패턴의 정의(p168)

팩토리 메소드 패턴(Factory Method Pattern)에서는 객체를 생성할 때 필요한 인터페이스를 만듭니다.

어떤 클래스의 인스턴스를 만들지는 서브클래스에서 결정합니다. 팩토리 메소드 패턴을 사용하면 클래스 인스턴스 만드는 일을 서브클래스에게 맡기게 됩니다.

Creator 추상 클래스에서 객체를 만드는 메소드, 즉 팩토리 메소드용 인터페이스를 제공한다는 사실을 알 수 있습니다.

Creator 추상 클래스에 구현되어 있는 다른 메소드는 팩토리 메소드에 의해 생산된 제품으로 필요한 작업을 처리합니다.

하지만 실제 팩토리 메소드를 구현하고 제품(객체 인스턴스)를 만드는 일은 서브 클래스에서만 할 수 있습니다.

"팩토리 메소드 패턴에서는 어떤 클래스의 인스턴스를 만들지를 서브클래스에서 결정한다"라고 하는 얘기를 종종 듣게 될 것입니다. 여기에서 '결정한다'라는 표현을 쓰는 이유는

실행 중에 서브클래스에서 어떤 클래스의 인스턴스를 만들지를 결정해서가 아니라, 생산자 클래스가 실제 생산될 제품을 전혀 모르는 상태로 만들어지기 때문입니다.

사실 더 정확하게 말하면, 사용하는 서브클래스에 따라 생산되는 객체 인스턴스가 결정됩니다.

  • Creator에는 제품으로 원하는 일을 할 때 필요한 모든 메소드가 구현되어 있습니다.
    하지만 제품을 만들어주는 팩토리 메소드는 추상 메소드로 정의되어 있을 뿐 구현되어 있진 않습니다.
    Creator의 모든 서브클래스에서 factoryMethod() 추상 메소드를 구현해야 합니다.
  • ConcreteCreator는 실제로 제품을 생산하는 factoryMethod()를 구현합니다.
  • 제품 클래스는 모두 똑같은 인터페이스를 구현해야 합니다. 그래야 그 제품을 사용할 클래스에서 구상 클래스가 아닌 인터페이스의 레퍼런스로 객체를 참조할 수 있으니까요.

Q&A (p169)

Q1. 구상 생산자 클래스가 하나밖에 없다면 팩토리 메소드 패턴을 쓸 필요가 있나요?

구상 생산자 클래스가 하나밖에 없더라도 팩토리 메소드 패턴은 충분히 유용합니다.

제품을 생산하는 부분과 사용하는 부분을 분리할 수 있으니까요.

다른 제품을 추가하거나 제품 구성을 변경하더라도 Creator 클래스가 ConcreteProduct와 느슨하게 결합되어 있으므로 Creator는 건드릴 필요가 없죠.

Q3. 팩토리 메소드와 생산자 클래스는 추상으로 선언해야 하나요?

꼭 그래야 하는 건 아닙니다. 몇몇 간단한 구상 제품은 기본 팩토리 메소드를 정의해서 Creator의 서브 클래스 없이 만들 수 있습니다.

Q4. 모든 구상 생산자 클래스에서 여러 개의 제품을 만들어야 하나요? 아니면 그중에 한두 개만 만들어도 되나요?

앞에서는 매개변수 팩토리 메소드를 사용했습니다. 전달받은 매개변수를 바탕으로 한 가지 이상의 객체를 만들 수 있죠.

하지만 팩토리에서 매개변수를 쓰지 않고 그냥 한 가지 객체만 만드는 경우도 많습니다. 어떤 방법을 쓰던지 팩토리 메소드 패턴을 사용한다는 데는 변함이 없죠

Q5. 근데 매개변수 팩토리 메소드를 사용하면 형식 안전성(type-safety)에 지장이 있지 않나요?

그냥 String을 매개변수로 전달할 뿐이잖아요. 'clam'을 잘못 쳐서 'calm'이라고 치면 어떻게 되죠?
네, 지적한 내용이 맞습니다. 형식 안정성을 조금 더 잘 보장해줄 수 있는 기법이 있습니다.

형식 오류를 컴파일 시에 잡아낼 수 있죠. 예를 들어 매개 변수 형식을 나타내는 객체를 만들 수도 있고, enum을 사용할 수도 있습니다.

Q6. 간단한 팩토리와 팩토리 메소드 패턴의 차이를 아직 잘 모르겠어요.

팩토리 메소드 패턴에서 피자를 리턴하는 클래스가 서브클래스라는 점을 빼면 거의 같잖아요.
맞습니다. 하지만 간단한 팩토리는 일회용 처방에 불과한 반면, 팩토리 메소드 패턴을 사용하면 여러 번 재사용이 가능한 프레임워크를 만들 수 있습니다.

예를 들어, 팩토리 메소드 패턴의 orderPizza() 메소드는 피자를 만드는 일반적인 프레임워크를 제공합니다. 그 프레임워크는 팩토리 메소드 피자 생성 구상 클래스를 만들었죠.

PizzaStore 클래스의 서브 클래스를 만들 때, 어떤 구상 제품 클래스에서 리턴할 피자를 만들지를 결정합니다.

간단한 팩토리는 객체 생성을 캡슐화하는 방법을 사용하긴 하지만 팩토리 메소드만큼 유연하지는 않습니다. 생성하는 제품을 마음대로 변경할 수 없기 때문이죠.

  • 무엇을 배웠나요?
    • 객체를 생성하는 코드를 캡슐화할 수 있다는 사실을 배웠습니다.
      구상 클래스의 인스턴스를 만드는 코드가 있다면 그 부분은 쉽게 바뀔 수 있습니다.
      이렇게 인스턴스를 만드는 행동을 캡슐화할 수 있게 해주는 '팩토리'라는 테크닉을 배웠습니다.
  • 팩토리를 썼을때 어떤 장점이 있나요?
    • 객체 생성 코드를 전부 한 객체 또는 메소드에 넣으면 코드에서 중복되는 내용을 제거할 수 있고,
      나중에 관리할 때도 한 군데에만 신경을 쓰면 됩니다. 그리고 객체 인스턴스를 만들 때 인터페이스만 있으면 됩니다.
      이 방법을 사용하면 인터페이스를 바탕으로 프로그래밍할 수 있어 유연성과 확장성이 뛰어난 코드를 만들 수 있습니다.

객체 의존성 살펴보기(p172)

객체 인스턴스를 직접 만들면 구상 클래스에 의존해야 합니다.

앞쪽에 있던 심하게 의존적인 PizzaStore를 한번 살펴봅시다. 이 코드에서는 모든 피자 객체를 팩토리에 맡겨서 만들지 않고 PizzaStore 클래스 내에서 직접 만들었습니다.

PizzaStore 코드를 다이어그램으로 그려 보면 다음과 같습니다.

의존성 뒤집기 원칙(p173)

구상 클래스 의존성을 줄이면 좋다는 사실은 이제 확실히 알았습니다. 이 내용을 정리해 놓은 객체지향 디자인 원칙이 있습니다.

바로 의존성 뒤집기 원칙(Dependency Inversion Principle, 기존에 의존 역전 원칙이라고 외웠음)입니다.

디자인 원칙 - 추상화된 것에 의존하게 만들고 구상 클래스에 의존하지 않게 만든다.

이 원칙과 "구현보다는 인터페이스에 맞춰서 프로그래밍한다"라는 원칙이 똑같다는 생각이 들 수도 있습니다.

물론 비슷하긴 하지만 의존성 뒤집기 원칙에서는 추상화를 더 많이 강조합니다.

이 원칙에는 고수준 구성 요소가 저수준 구성요소에 의존하면 안되며, 항상 추상화에 의존하게 만들어야 한다는 뜻이 담겨 있습니다. 그런데 이게 대체 무슨 소리일까요?

지금까지 만들어왔던 예제 중 PizzaStore는 고수준 구성 요소라고 할 수 있고, 피자 클래스는 저수준 구성 요소라고 할 수 있습니다.

(고수준 구성 요소는 다른 저수준 구성 요소에 의해 정의되는 행동이 들어있는 구성요소를 뜻합니다.

PizzaStore는 다양한 피자 객체를 만들고, 피자를 준비하고, 굽고 자르고 포장하죠.이때 PizzaStore에서 사용하는 피자 객체는 저수준 구성 요소입니다.)

PizzaStore 클래스는 구상 피자 클래스에 의존하고 있다는 사실을 확실하게 알 수 있습니다.

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

이 원칙은 고수준 모듈과 저수준 모듈에 모두 적용될 수 있습니다. 이 원칙은 어떻게 적용할 수 있는걸까요?

의존성 뒤집기 원칙 적용하기(p174)

심하게 의존적인 PizzaStore의 가장 큰 문제점은 PizzaStore가 모든 종류의 피자에 의존한다는 점입니다. orderPizza() 메소드에서 구상 형식의 인스턴스를 직접 만들기 때문이죠.

Pizza라는 추상 클래스를 만들긴 했지만, 이 코드에서 구상 피자 객체를 생성하는 것은 아니기에 추상화로 얻는 것이 별로 없습니다.

어떻게 해야 인스턴스 만드는 부분을 orderPizza()에서 뽑아낼 수 있을까요? 이미 배운대로 팩토리 메소드 패턴으로 인스턴스 만드는 부분을 뽑아낼 수 있습니다.

고수준 모듈과 저수준 모듈이 둘 다 하나의 추상 클래스에 의존하게 됩니다.

의존성 뒤집기 원칙을 지키는 방법(p177)

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

이 가이드라인은 항상 지켜야하는 것이 아니라, 우리가 지향해야 할 바를 알려줄 뿐입니다.

하지만 이 가이드라인을 완전히 습득한 상태에서 디자인한다면 원칙을 지키지 않은 부분을 명확하게 파악할 수 있으며, 합리적인 이유로 불가피한 상황에서만 예외를 둘 수 있습니다.

예를 들어, 어떤 클래스가 바뀌지 않는다면 그 클래스의 인스턴스를 만드는 코드를 작성해도 그리 큰 문제가 생기지 않습니다. (ex. String 클래스)

하지만 여러분이 만들고 있는 클래스가 바뀔 수 있다면 팩토리 메소드 패턴을 써서 변경될 수 있는 부분을 캡슐화해야 합니다.

원재료군으로 묶기(p179)

뉴욕과 시카고에서 사용하는 재료는 서로 다릅니다. 최첨단 피자의 명성이 높아져서 캘리포니아 지점을 열었더니, 또 다른 재료를 사용하게 되었습니다.

나중에 다른 곳에 지점을 열면 또 다른 재료를 사용해야 할 겁니다. 이렇게 서로 다른 재료를 제공하려면 원재료군(families of ingredients)를 처리할 방법을 생각해 봐야 합니다.

  • 각 피자는 같은 구성 요소로 이루어지지만, 지역마다 구성 요소를 다른 종류로 구현합니다.
  • 각 군(family)은 특정 형식의 반죽, 소스, 치즈, 그리고 해산물 토핑으로 구성됩니다. (그외에도 자잘한 것들이 있습니다.)
  • 이렇게 총 3개의 지역이 서로 다른 원재료군을 이룹니다. 그러니 특정 재료로 구성된 군을 각 지역마다 구현해야 합니다.

원재료 팩토리 만들기(p180)

public interface PizzaIngredientFactory {
 
	public Dough createDough();
	public Sauce createSauce();
	public Cheese createCheese();
	public Veggies[] createVeggies();
	public Pepperoni createPepperoni();
	public Clams createClam();
 
}

뉴욕 원재료 팩토리 만들기(p181)

public class NYPizzaIngredientFactory implements PizzaIngredientFactory {
 
	public Dough createDough() {
		return new ThinCrustDough();
	}
 
	public Sauce createSauce() {
		return new MarinaraSauce();
	}
 
	public Cheese createCheese() {
		return new ReggianoCheese();
	}
 
	public Veggies[] createVeggies() {
		Veggies veggies[] = { new Garlic(), new Onion(), new Mushroom(), new RedPepper() };
		return veggies;
	}
 
	public Pepperoni createPepperoni() {
		return new SlicedPepperoni();
	}

	public Clams createClam() {
		return new FreshClams();
	}
}

Pizza 클래스 변경하기(p183)

Pizza 클래스가 팩토리에서 생산한 원재료만 사용하도록 코드를 고쳐봅시다.

public abstract class Pizza {
	String name;

	Dough dough;
	Sauce sauce;
	Veggies veggies[];
	Cheese cheese;
	Pepperoni pepperoni;
	Clams clam;

	abstract void prepare();

	void bake() {
		System.out.println("Bake for 25 minutes at 350");
	}

	void cut() {
		System.out.println("Cutting the pizza into diagonal slices");
	}

	void box() {
		System.out.println("Place pizza in official PizzaStore box");
	}

	void setName(String name) {
		this.name = name;
	}

	String getName() {
		return name;
	}

	public String toString() {
		...
	}
}

기존 코드에서 달라진 점은 원재료를 팩토리에서 바로 가져온다는 점 말고는 없습니다.

팩토리 메소드 패턴을 사용한 코드를 만들었을 때 NYCheesPizza와 ChicagoCheesePiza 클래스를 만들었죠?

그 두 클래스를 살펴보면 지역별로 다른 재료를 사용한다는 것만 빼면 똑같은 형식으로 구성되어 있습니다. 피자를 이루는 기본 요소가 반죽, 소스, 치즈라는 건 마찬가지니까요.

야채 피자와 조개 피자도 마찬가지입니다. 재료만 다를 뿐 준비 단계는 똑같습니다.

따라서 피자마다 클래스를 지역별로 따로 만들 필요가 없습니다. 지역별로 다른 점은 원재료 팩토리에서 처리합니다.

치즈 피자 코드는 다음과 같이 만들 수 있습니다.

public class CheesePizza extends Pizza {
	PizzaIngredientFactory ingredientFactory;
 
	public CheesePizza(PizzaIngredientFactory ingredientFactory) {
		this.ingredientFactory = ingredientFactory;
	}
 
	void prepare() {
		System.out.println("Preparing " + name);
		dough = ingredientFactory.createDough();
		sauce = ingredientFactory.createSauce();
		cheese = ingredientFactory.createCheese();
	}
}

바뀐 내용 되돌아보기

코드가 꽤 여러번 바뀌었습니다.

추상 팩토리라고 부르는 새로운 형식의 팩토리를 도입해서 피자 종류에 맞는 원재료 군을 생산하는 방법을 구축했습니다.

  • 추상 팩토리는 제품군(여기서는 피자를 만들 때 필요한 재료)을 위한 인터페이스를 제공합니다.

추상 팩토리로 제품군을 생성하는 인터페이스를 제공할 수 있습니다.

이 인터페이스를 사용하면 코드와 제품을 생산하는 팩토리를 분리할 수 있습니다.

이렇게 함으로써 지역, 운영체제, 룩앤필 등 서로 다른 상황에 맞는 제품을 생산하는 팩토리를 구현할 수 있습니다.

코드가 실제 제품과 분리되어 있으므로 다른 결과가 필요하면 다른 팩토리를 사용하면 됩니다.

추상 팩토리 패턴의 정의(p190)

추상 팩토리 패턴(Abstract Factory Pattern)은 구상 클래스에 의존하지 않고도 서로 연관되거나 의존적인 객체로 이루어진 제품군을 생산하는 인터페이스를 제공합니다.

구상 클래스는 서브 클래스에서 만듭니다.

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

따라서 클라이언트와 팩토리에서 생산되는 제품을 분리할 수 있습니다. 클래스 다이어그램을 보면서 어떤 식으로 돌아가는지 알아봅시다.

  • AbstractFactory는 모든 구상 팩토리에서 구현해야 하는 인터페이스 입니다. 제품을 생산할 때 일련의 메소드가 정의되어 있죠.
  • 구상 팩토리는 서로 다른 제품군을 구현합니다.
    클라이언트에서 제품이 필요하면 이 팩토리 가운데 적당한 걸 골라서 쓰면 되기에 제품 객체의 인스턴스를 직접 만들 필요가 없죠.
  • 클라이언트를 만들 때는 추상 팩토리를 바탕으로 만듭니다. 실제 팩토리는 실행 시에 결정되죠.
  • 제품군은 각 구상 팩토리에서 필요한 제품을 모두 만들 수 있습니다.
  • PizzaIngredientFactory는 서로 관련된 제품군(피자를 만들 때 필요한 모든 제품)을 만드는 방법을 정의하는 추상 인터페이스입니다.
  • 구상 피자 팩토리는 피자 원재료를 만드는 일을 합니다. 각 팩토리는 자기 지역에 맞는 재료를 만드는 법도 알고 있죠.
  • 추상 팩토리의 클라이언트는 PizzaStore의 인스턴스인 NYPizzaStore와 ChicahoPizzaStore입니다.

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

추상 팩토리 패턴에서 메소드가 팩토리 메소드로 구현되는 경우도 종종 있습니다.

추상 팩토리가 원래 일련의 제품을 만드는 데 쓰이는 인터페이스를 정의하려고 만들어진 것이니 어찌보면 당연합니다.

그 인터페이스에 있는 각 메소드는 구상 제품을 생산하는 일을 맡고, 추상 팩토리의 서브클래스를 만들어서 각 메소드의 구현을 제공합니다.

따라서 추상 팩토리 패턴에서 제품을 생산하는 메소드를 구현하는 데 있어서 팩토리 메소드를 사용하는 건 너무나도 저연스러운 일이라고 할 수 있습니다.

패턴 집중 인터뷰(p192) 요약

  • 두 패턴 모두 객체를 만드는 일을 함

추상 팩토리 패턴

  • 클라이언트에서 서로 연관된 일련의 제품(제품군)을 만들어야 할 때 사용
  • 구성(Composition)으로 객체를 만든다.
  • 제품군을 만드는 추상 형식(PizzaIngredientFactory안의 메소드들)을 제공함.
    제품이 생산되는 방법은 서브클래스(NYPizzaIngredientFactory 등)에서 정의함. 팩토리를 사용하고 싶으면 인스턴스를 만든 다음 추상 형식을 써서 만든 코드에 전달하면 됨.
    따라서 팩토리 메소드 패턴을 쓸 때와 마찬가지로 클라이언트와 실제 구상 제품이 분리됨. 일련의 연관된 제품을 하나로 묶을 수 있음
  • 제품군에 제품을 추가하는 식으로 관련 제품을 확대해야 하면 인터페이스를 바꿔야하는데, 모든 서브클래스의 인터페이스를 변경해야 한다는 단점도 있다.
  • 구상 팩토리를 구현할 때 팩토리 메소드로 제품을 생산할 때가 종종 있음.

팩토리 메서드 패턴

  • 클라이언트 코드(PizzaStore)와 인스턴스를 만들어야 할 구상 클래스(Pizza)를 분리시켜야 할 때 사용
  • 상속으로 객체를 만든다. 클래스를 확장하고 팩토리 메소드(createPizza)를 오버라이드 해야함.
  • 팩토리 메소드 패턴을 사용하는 이유는 서브클래스로 객체를 만들려고 하기 위해서임. 클라이언트(PizzaStore)는 구상 형식(NyStyleCheesePizza)을 서브클래스(NYPizzaStore)에서 처리해 주니까, 자신이 사용할 추상 형식(Pizza)만 알면 됨.
  • 서브클래스에서 만드는 구상 형식을 활용하는 추상 생산자에서 코드를 구현함
  • 팩토리 메소드 패턴은 한 가지 제품만 생산함. 복잡한 인터페이스 필요 X, 메소드도 하나만 있으면 됨.

핵심 정리 (p139)

  • 객체지향 원칙

    • 바뀌는 부분은 캡슐화한다.
    • 상속보다는 구성을 활용한다.
    • 구현보다는 인터페이스에 맞춰서 프로그래밍 한다.
    • 상호작용하는 객체 사이에서는 가능하면 느슨한 결합을 사용해야 한다.
    • 클래스는 확장에는 열려있어야 하지만 변경에는 닫혀있어야 한다.(OCP)
    • 추상화된 것에 의존하게 만들고 구상 클래스에 의존하지 않게 만든다.
      • 가능하면 모든 것을 추상화하라는 원칙이 추가되었습니다.
  • 객체지향 패턴 - 추상 팩토리 패턴

    • 구상 클래스에 의존하지 않고도 서로 연관되거나 의존적인 객체로 이루어진
      제품군을 생성하는 인터페이스를 제공합니다. 구상 클래스는 서브클래스에서 만듭니다.
  • 객체지향 패턴 - 팩토리 메소드 패턴

  • 객체를 생성할 때 필요한 인터페이스를 만듭니다. 어떤 클래스의 인스턴스를 만들지는 서브클래스에서 결정합니다. 팩토리 메소드를 사용하면 인스턴스 만드는 일을 서브클래스에 맡길 수 있습니다.

  • 팩토리를 쓰면 객체 생성을 캡슐화 할 수 있습니다.
  • 간단한 팩토리는 엄밀하게 말해서 디자인 패턴은 아니지만, 클라이언트와 구상 클래스를 분리하는 간단한 기법으로 활용할 수 있습니다.
  • 팩토리 메소드 패턴상속을 활용합니다. 객체 생성을 서브클래스에게 맡기죠. 서브 클래스는 팩토리 메소드를 구현해서 객체를 생산합니다.
  • 추상 팩토리 패턴객체 구성을 활용합니다. 팩토리 인터페이스에서 선언한 메소드에서 객체 생성이 구현되죠.
  • 모든 팩토리 패턴은 애플리케이션의 구상 클래스 의존성을 줄여줌으로써 느슨한 결합을 도와줍니다.
    의존성 뒤집기 원칙을 따르면 구상 형식 의존을 피하고 추상화를 지향할 수 있습니다.
  • 팩토리는 구상 클래스가 아닌 추상 클래스와 인터페이스에 맞춰서 코딩할 수 있게 해주는 강력한 기법입니다.

0개의 댓글