Read Book/헤드퍼스트 디자인 패턴
4장. 팩토리 패턴
nowwater
2023. 4. 5. 20:07
728x90
new 연산자의 문제점
- new 연산자 자체의 문제는 없다
- 그러나 구상 클래스를 만들게 되고, 변화에 취약하다는 것이 진짜 문제
이러한 변화에 유연하게 대응할 수 있게 해주는 것이 인터페이스를 바탕으로 만든 코드
- 어떤 클래스든 특정 인터페이스만 구현하면 사용할 수 있기 때문
예시: 피자 만들기
1. 인스턴스를 만드는 구상 클래스 선택
- 피자 종류가 바뀔 때마다 코드를 계속 고쳐야 함
public class PizzaStore {
Pizza orderPizza(String type) {
Pizza pizza;
// 바뀌는 코드
if (type.equals("cheese")) {
pizza = new CheesePizza();
} else if (type.equals("pepperoni")) {
pizza = new Pepperoni();
}
// ...
// 바뀌지 않는 코드
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
return pizza;
}
}
2. Simple 팩토리를 사용하여 객체 생성 부분을 캡슐화
- 팩토리: 객체 생성을 처리하는 클래스
- 구현을 변경할 때 팩토리 클래스 하나만 고치면 됨
- 디자인 패턴이라기 보다는 자주 쓰이는 관용구
- 정적 메소드로 만들어서 팩토리 객체 인스턴스 생성하지 않고도 객체 생성 메서드 호출할 수 있게 가능
- 서브클래스를 만들어서 객체 생성 메소드의 행동을 변경할 수 없다는 단점 존재
public class PizzaStore {
SimplePizzaFactory factory;
public PizzaStore(SimplePizzaFactory factory) {
this.factory = factory;
}
public Pizza orderPizza(String type) {
Pizza pizza = factory.createPizza(type);
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
return pizza;
}
}
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 Pepperoni();
}
// ...
return pizza;
}
}
SimplePizzaFactory nyFactory = new NYPizzaFactory();
PizzaStore nyStore = new PizzaStore(nyFactory);
nyStore.orderPizza("Veggie");
3. PizzaStore 를 추상 클래스로 변경 - 팩토리 메소드 패턴 적용
팩토리 메소드: 서브 클래스에서 어떤 클래스를 만들지 결정하여 객체를 생성하는 메서드
- 피자를 주문하는 일 자체 로직은 구현 (final 로 오버라이드 불가하게 선언 가능)
- 피자를 생성하는 로직(달라지는 부분)은 각 서브 클래스마다 오버라이드하여 작성
- orderPizza 메서드는 실제로 어떤 구상 클래스에서 작업이 처리되고 있는지 알 수 없음
- PizzaStore 와 Pizza 는 서로 완전히 분리 !
- 피자는 어떤 서브클래스를 선택했느냐에 따라 결정됨
public abstract class PizzaStore {
public final Pizza orderPizza(String type) {
Pizza pizza = createPizza(type);
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
return pizza;
}
abstract Pizza createPizza(String type);
}
public class NYStylePizzaStore extends PizzaStore {
@Override
public Pizza createPizza(String type) {
Pizza pizza = null;
if (type.equals("cheese")) {
pizza = new NYStyleCheesePizza();
} else if (type.equals("pepperoni")) {
pizza = new NYStylePepperoni();
}
// ...
return pizza;
}
}
PizzaStore nyPizzaStore = new NYPizzaStore();
nyPizzaStore.orderPizza("cheese");
4. 원재료군으로 묶기 - 추상 팩토리 패턴 적용
- 추상 팩토리로 제품군을 생성하는 인터페이스를 제공
- 코드가 실제 제품과 분리되어 있으므로, 다른 결과가 필요하면 다른 팩토리 사용
public interface PizzaIngredientFactory {
public Dough createDough();
public Sauce createSauce();
public Cheese createCheese();
public Veggies[] createVeggies();
public Pepperoni createPepperoni();
public Clams createClam();
}
public abstract class Pizza {
Dough dough;
Sauce sauce;
Veggies veggies[];
Cheese cheese;
Pepperoni pepperoni;
Clams clams;
abstract void prepare(); // 피자 생성에 필요한 재료를 가져옴
// ...
}
public class NYPizzaIngredientFactory implements PizzaIngredientFactory {
public Dough createDough() {
return new ThinCrustDough();
}
public Sauce createSauce() {
return new MarinaraSauce();
}
// ...
}
public class NYPizzaStore extends PizzaStore {
protected Pizza createPizza(String name) {
Pizza pizza = null;
PizzaIngredientFactory ingredientFactory = new NYPizzaIngredientFactory();
if (item.equals("cheese")) {
pizza = new CheesePizza(ingredientFactory);
} else if (item.equals("veggie")) {
pizza = new VeggiePizza(ingredientFactory);
}
// ...
return pizza;
}
}
팩토리 메소드 패턴
- 객체를 생성할 때 필요한 인터페이스를 만든다.
- 서브클래스에서 팩토리 메소드를 구현해서 객체 생성 → 사용하는 서브 클래스에 따라 생산되는 객체 인스턴스가 달라짐
- 클래스 인스턴스를 만드는 일을 서브클래스에 맡김
- 상속을 사용 → 객체 생성을 위한 팩토리 메서드를 서브 클래스에서 오버라이드하여 구현
- 단점: 하나의 서브클래스를 통해 한 가지 제품만 생산 가능 (재사용 불가)
추상 팩토리 패턴
- 구상 클래스에 의존하지 않고도 연관되거나 의존적인 객체로 이루어진 제품군을 생산하는 인터페이스를 제공
- 팩토리 인터페이스에서 선언한 메소드에서 객체 생성이 구현
- 클라이언트는 추상화된 객체와 객체 생성을 위한 추상화된 팩토리를 가짐
- 구성을 사용 → 객체 생성을 위한 추상화된 팩토리(인터페이스)를 갖고 있고, 그 팩토리의 구현체로 객체를 생성
- 장점: 일련의 연관된 제품을 하나로 묶을 수 있음 (재사용 가능) → 추상 팩터리 인터페이스(추상 클래스)의 구현체 단위로 객체를 생성하기 때문
- 단점: 새로운 제품을 추가하려면 인터페이스를 바꿔야 함
→ 두 패턴 모두 객체 생성을 캡슐화하여 애플리케이션의 느슨한 결합을 도와주고, 특정 구현에 덜 의존하도록 만들 수 있게 해줌 (클라이언트 코드와 구상 클래스 생성을 분리)
추상 팩토리 패턴에서 팩토리 메소드 패턴을 사용할 수 있음
객체 생성을 위한 추상 팩토리의 구현체를 만드는 부분을 팩토리 메서드로 구현해서, 서브 클래스에서 오버라이드하여 제공!
디자인 원칙
추상화된 것에 의존하게 만들고 구상 클래스에 의존하지 않게 만든다.
→ 의존성 뒤집기 원칙(Dependency Inversion Principle)
고수준 구성 요소가 저수준 구성 요소에 의존하면 안되며, 항상 추상화에 의존하게 만들어야 함
고수준 구성 요소
- 다른 '저수준' 구성 요소에 의해 정의되는 행동이 들어있는 구성 요소
- e.g. PizzaStore (피자에 따라 행동이 결정됨)
저수준 구성 요소
- e.g. Pizza
의존성 뒤집기를 통해 고수준 모듈과 저수준 모듈이 둘 다 하나의 추상 클래스에 의존하게 됨 ( PizzaStore → Pizza ← Pizza 구현체 )
의존성 뒤집기 원칙 가이드라인
- 변수에 구상 클래스의 레퍼런스 저장 X
- 팩토리를 써서 구상 클래스 레퍼런스 저장 방지
- 구상 클래스에서 유도된 클래스 생성 X
- 특정 구상 클래스 의존을 막기 위해 인터페이스나 추상 클래스로부터 클래스 생성
- 베이스 클래스에 이미 구현되어 있는 메소드를 오버라이드 X
- 이미 구현되어 있는 메소드를 오버라이드하면 베이스 클래스가 제대로 추상화되지 않음
팩토리의 장점
- 객체 생성 코드를 한 군데서 관리 → 중복 제거 및 관리 용이
- 객체 인스턴스 생성 시 인터페이스만 있으면 됨 → 구상 클래스가 아닌 추상 클래스와 인터페이스에 맞춰 코딩할 수 있게 해주는 강력한 기법, 구상 클래스 의존성을 줄여줌
- 객체 생성을 캡슐화하여 클라이언트 코드와 실제 클래스 구현을 분리