본 포스트는 헤드퍼스트 디자인패턴을 읽고 정리한 글입니다. |
Ch4 팩토리 패턴
OverView
- 팩터리 패턴은 불필요한 의존성 없애 결합 문제를 해결한다.
- 모든 팩토리 패턴은 객체 생성을 캡슐화한다.
- 구상 클래스 의존성을 줄인다. → 네 번째 디자인 원칙, 느슨한 결합
- 구상 클래스가 아닌 추상 클래스와 인터페이스에 맞추어 코딩할 수 있다.
- 두 번째 디자인 원칙, 인터페이스에 맞추어 코딩하라.
Question: 피자 가게 구축
메뉴를 유동적으로 추가/삭제 할 수 있는 피자 가게를 만들어야 한다.
단순 해결
입력 받은 피자 종류에 따라 피자 객체 인스턴스를 생성한다.
public class PizzaStore {
public Pizza orderPizza(String type) {
Pizza pizza;
if (type.equals("cheese")) {
pizza = new CheesePizza();
} else if (type.equals("pepperoni")) {
pizza = new PepperoniPizza();
} else {
pizza = new GreekPizza();
}
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
return pizza;
}
}
Problem
new를 사용하여 Pizza의 구상 클래스 인스턴스를 생성하는 과정은 빠질 수 없다.
if (type.equals("cheese")) {
pizza = new CheesePizza();
} else if (type.equals("pepperoni")) {
pizza = new PepperoniPizza();
} else {
pizza = new GreekPizza();
}
피자 가게에서는 메뉴가 수시로 추가/삭제되므로 인스턴스를 생성할 구상 클래스를 선택하는 코드는 바뀌는 부분에 해당한다.
따라서, 위 코드처럼 구현하면 메뉴의 추가/삭제 여부에 따라 코드를 항상 수정해야 한다.
즉, 유연성이 매우 떨어지는 코드이다.
바뀌는 부분을 파악했으니, 첫 번째 디자인 원칙대로 바뀌지 않는 부분과 분리하고 캡슐화해보자.
Problem Solve: 간단한 팩토리
객체 생성(인스턴스 생성)을 처리하는 클래스
SimplePizzaFactory
orderPizza 메서드는 SimplePizzaFactory의 클라이언트가 된다.
즉, SimplePizzaFactory에서 만든 객체를 호출한다.
public class SimplePizzaFactory {
public Pizza createPizza(String type) {
Pizza pizza = null;
if (type.equals("cheese")) {
pizza = new CheesePizza();
} else if (type.equals("pepperoni")) {
pizza = new PepperoniPizza();
} else {
pizza = new GreekPizza();
}
return pizza;
}
}
SimplePizzaFactory에서는 클라이언트가 받을 피자를 만드는 일만 한다.
단일 책임의 원칙 준수
간단한 팩토리 (Simple Factory)
클라이언트와 구상 클래스를 간단하게 분리할 수 있다.
구조
클라이언트는 팩토리로부터 인스턴스를 받는다.
팩토리는 인스턴스를 생성하는 곳으로, 유일하게 구상 클래스를 직접 참조한다.
인터페이스는 팩토리에서 만드는 것에 해당된다.
구상 클래스는 인터페이스를 구현한다.
간단한 팩토리 피자 가게 구조도
한계점
확장이 어렵다. -> OCP 위반
- 팩토리가 클래스로 이루어져 있기 때문
간단한 팩토리는 디자인 패턴보다 ‘관용구’에 가깝다. |
Question2: 피자 가게의 여러 지점
PizzaStore 코드를 모든 지점에서 활용해야 한다.
임시방편 문제 해결
SimplePizzaFactory를 삭제하고 지점 당 팩토리 클래스 생성
ex. NYPizzaFactory, ChicagoPizzaFactory
NYPizzaFactory nyFactory = new NYPizzaFactory();
PizzaStore nyStore = new PizzaStore(nyFactory);
nyStore.orderPizza("Veggie");
ChicagoPizzaFactory chicagoFactory = new ChicagoPizzaFactory();
PizzaStore chicagoStore = new PizzaStore(chicagoFactory);
chicagoStore.orderPizza("Veggie");
Problem
피자를 굽는 방식이 달라지거나, 피자를 자르는 것을 까먹기도 하고, 이상하게 생긴 피자 상자를 사용하기도 한다.
이 문제를 해결하려면 PizzaStore와 피자 제작 코드 전체를 하나로 묶어야 한다.
단, 유연성을 잃어서는 안 된다.
SimplePizzaFactory를 만들기 전에는, 피자 제작 코드가 PizzaStore와 직접 연결되어 있지만 유연성이 전혀 없었다. |
팩토리 메서드 패턴 (Factory Method Pattern)
객체 생성을 캡슐화한다.
객체를 생성할 때 필요한 인터페이스를 만들고, 어떤 클래스의 인스턴스를 생성할 지는 서브 클래스에서 결정한다. 이 디자인 패턴을 사용하면 클래스 인스턴스를 만드는 일을 서브 클래스에게 맡긴다.
팩토리 메서드 패턴은 상속을 활용하여 객체 생성을 서브 클래스에게 맡긴다.
서브 클래스는 팩토리 메서드를 구현하여 객체 생성을 구현한다.
팩토리 메서드
팩토리 메서드는 객체 생성을 서브 클래스에 캡슐화한다.
이렇게 하면, 슈퍼 클래스에 있는 클라이언트 코드와 서브 클래스의 객체 생성 코드를 분리할 수 있다.
abstract Product factoryMethod(String type)
abstract - 추상 메서드
추상 메서드로 선언하여 서브 클래스가 객체 생성을 책임지도록 한다.
Product - 반환 객체
팩터리 메서드는 특정 객체를 리턴하며, 이는 보통 슈퍼 클래스가 정의한 메서드 내에서 사용된다.
factoryMethod
클라이언트에서 실제로 생성되는 구상 객체가 무엇인지 알 수 없게 만든다.
String type - 매개변수
만들 객체 종류를 선택할 수 있다.
팩토리 메서드 패턴의 구조
생산자(Creator) 클래스 - Factory
추상 클래스
서브 클래스에서 객체를 생성하기 위하여, 팩토리 메서드(추상 메서드)를 정의한다.
제품으로 할 모든 작업은 팩토리 메서드를 제외한 다른 메서드로 구현한다. |
구상 클래스 (구상 생산자)
추상 클래스를 구현한다.
팩토리 메서드를 구현하여 실제로 객체를 생성(인스턴스 생성)한다.
제품(Product) 클래스
생산자는 제품을 생산(생성)한다.
고유한 특징을 가지는 다양한 제품이 존재할 수 있으므로, 추상 제품 클래스와 구상 제품 클래스로 구성된다.
구상 생산자에서는 구상 제품 인스턴스를 생성한다.
구조도
생산되는 객체 인스턴스는 사용하는 서브 클래스에 따라 결정된다.
'결정한다’
: 실행 중에 서브 클래스에서 어떤 클래스의 인스턴스를 만들지 결정해서가 아닌,
생산자 클래스가 실제 생산될 제품을 전혀 모르는 상태로 만들어지기 때문이다.
장단점
장점
생산자와 제품 클래스 모두 추상 클래스로 시작하며, 구상 클래스를 통해 유연한 확장이 가능하다.
각 생산자 구상 클래스에서는 생산하고자 하는 제품을 실제로 만드는 작업(객체 인스턴스 생성)을 한다.
또, 해당 제품을 구현하는 방법이 캡슐화되어 있다.
- 팩토리 메서드는 이 방법을 구현하는 것에 핵심적인 역할을 한다.
단점
매개변수 팩토리 메서드를 사용하면 형식 안전성(type-safety)에 지장이 생길 수 있다.
- 형식 안전성을 보장할 수 있는 기법(enum, 객체 생성 등)을 사용해야 한다.
Problem Solve2: 팩토리 메서드 createPizza()
PizzaStore에 orderPizza 메서드를 정의하고 createPizza 메서드를 추상 메서드로 선언하면, 각 서브 클래스에서 구현하게 된다.
이렇게 하면, 각 지점마다 달라질 수 있는 것은 피자 스타일이다.
구조도
실제 구현 코드
public abstract class PizzaStore {
public Pizza orderPizza(String type) {
Pizza pizza;
// 정의한 메서드를 고쳐 사용할 수 없도록 하려면 final 로 선언할 것
pizza = createPizza(type);
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
return pizza;
}
abstract Pizza createPizza(String type); // 추상 메서드 = 팩토리 메서드
}
public class NYStylePizzaStore extends PizzaStore {
@Override
Pizza createPizza(String item) {
// 추상 메서드 구현
if (item.equals("cheese")) {
return new NYStyleCheesePizza();
} else if (item.equals("veggie")) {
return new NYStyleVeggiePizza();
} else if (item.equals("pepperoni")) {
return new NYStylePepperoniPizza();
} else return null;
}
}
피자 가게 예시에서의 팩토리 메서드
//abstract Product factoryMethod(String type)
abstract Pizza createPizza(String type);
완전히 분리된 Pizza와 PizzaStore
orderPizza 메서드는 추상 클래스 PizzaStore에 정의되어 있다. 해당 클래스의 서브 클래스가 만들어 지기 전까지는 구상 클래스가 만들어지지 않는다.
orderPizza 메서드에서 Pizza 객체를 가지고 피자를 준비하고, 굽고, 자르고, 포장하는 여러 작업을 한다.
하지만, Pizza가 추상 클래스이기 때문에 orderPizza 메서드는 실제로 어떤 구상 클래스에서 작업이 처리되는지 알 수 없다.
네 번째 디자인 원칙, 느슨한 결합
PizzaStore는 실제로 어떤 피자(구상 클래스)가 만들어지는지 모른다.
즉, orderPizza 메서드에서 createPizza 메서드를 호출하여 피자 객체를 받지만,
어떤 종류의 피자를 받는지는 orderPizza 메서드가 결정하지 않는다.
피자의 종류는 서브 클래스(피자 가게)에 따라 달라진다.
기존(처음)의 PizzaStore는 모든 피자 객체에 의존하고 있었다.
여기에 팩토리 메서드 패턴을 적용하여 의존성을 줄였고, 훨씬 유연하고 확장성 좋은 프로그램을 구축할 수 있었다.
이와 같이 구상 클래스의 의존성은 줄이면 좋다!
이것을 ‘의존성 뒤집기 원칙(DIP)’이라고 한다.
디자인 원칙
여섯 번째, DIP(Dependency Inversion Principle)
추상화된 것에 의존하게 만들고, 구상 클래스에 의존하지 않게 만든다.
고수준 구성 요소가 저수준 구성 요소에 의존하면 안 되며, 항상 추상화에 의존하게 만들어야 한다.
고수준/저수준 구성 요소
고수준 구성 요소는 저수준 구성 요소에 의해 정의되는 행동이 들어있는 구성 요소이다.
Example
초기 피자 가게 시스템에서 PizzaStore의 행동은 Pizza에 의해 정의되므로, PizzaStore가 고수준 구성 요소이고 Pizza(구상 클래스 객체)가 저수준 구성 요소라고 할 수 있다.
따라서, PizzaStore는 구상 Pizza 클래스에 의존하고 있다.
DIP에 따르면, 구상 클래스가 아닌 추상 클래스(또는 인터페이스)와 같이 추상적인 것에 의존하는 코드를 만들어야 한다.
Problem
고수준 구성 요소인 PizzaStore가 저수준 구성 요소인 Pizza에 의존하고 있다.
orderPizza 메서드에서 구상 인스턴스를 직접 만들기 때문이다.
Pizza라는 추상 클래스를 만들었지만, 해당 코드에서 객체를 생성하는 것이 아니기 때문에 항상 추상화에 의존하고 있지 않다.
Factory Method Pattern
팩토리 메서드 패턴을 적용하면 PizzaStore는 구상 Pizza 클래스가 아닌 추상 Pizza 클래스에만 의존한다.
즉, 고수준 구성 요소인 PizzaStore과 저수준 구성 요소인 Pizza 객체 모두가 추상 Pizza 클래스에 의존한다.
앞의 다이어그램에서는 의존성이 위에서 아래로 내려가기만 했지만,
팩토리 메서드 패턴을 적용한 후에는 그 방향이 일부 뒤집어져 있음을 확인할 수 있다.
일반적으로 생각하는 방법과 반대로, ‘뒤집어’ 생각한다는 점에서 의존성 뒤집기 원칙이라고 한다. |
의존성 뒤집기 원칙을 지키는 방법
가이드라인
- 변수에 구상 클래스의 레퍼런스를 저장하지 않는다.
- new 연산자를 사용하면 구상 클래스의 레퍼런스를 사용하게 된다.
- → 팩토리를 사용하여 구상 클래스의 레퍼런스를 변수에 저장하는 것을 방지한다.
- 구상 클래스에서 유도된 클래스를 만들지 않는다.
- 유도된 클래스를 만들면 특정 구상 클래스에 의존하게 된다.
- → 추상 클래스나 인터페이스와 같이 추상화된 것으로부터 클래스를 만든다.
- 베이스 클래스에 이미 구현되어 있는 메서드를 오버라이드하지 않는다.
- 이미 구현되어 있는 메서드를 오버라이드하면 베이스 클래스가 제대로 추상화되지 않는다.
- → 모든 서브 클래스에서 공유할 수 있는 메서드만 베이스 클래스에서 정의한다.
GitHub
전체 실습 코드는 아래 레포지터리의 ch2_FactoryPattern/FactoryMethodPattern 폴더에서 확인할 수 있습니다.