Goal
- 추상 팩토리에 대해 알아본다
- 객체 의존성에 대해 고찰 해보기
- 샘플 코드를 통한 추상 팩토리를 이해한다
추상 팩토리 패턴(Abstact Factory Pattern)
추상 팩토리 패턴은 상세화된 서브 클래스를 정의하지 않고도(= 구상 클래스에 의존하지 않고도) 서로 관련성이 있거나 독립적인 여러 객체의 군을 생성하기 위한 인터페이스를 제공한다. 이 인터페이스를 사용하면 코드와 제품을 생산하는 팩토리를 분리할 수 있다.
추상 팩토리 패턴은 팩토리 메서드 패턴을 좀 더 캡슐화한 방식이라고 볼 수가 있다.
왜 or 언제 추상 팩토리 패턴을 사용하는가?
- 객체가 생성되거나 구성, 표현되는 방식과 무관하게 시스템을 독립적으로 만들고자 할 때
- 여러 제품군 중 하나를 선택해서 시스템을 설정해야 하고 한 번 구성한 제품을 다른 것으로 대체할 수 있을 때
- 관련된 제품 객체들이 함께 사용되도록 설계되었고, 이 부분에 대한 제약이 외부에도 지켜지도록 하고 싶을 때
- 제품에 대한 클래스 라이브러리를 제공하고, 그들의 구현이 아닌 인터페이스를 노출 시키고 싶을 때
잠깐
서로 관련성이 있거나 독립적인 여러 객체의 군을 생성하기 위함이라고 함은 = 서로의 객체 의존성을 낮춘다는 의미가 됨
어떻게 의존성을 낮출 수 있을까? 의존성에 관해 살펴보자
객체 의존성 살펴보기
기존의 심하게 의존적인 PizzsStore 코드
문제점
- 모든 피자 객체를 팩토리에 맡겨서 만들지 않고, PizzaStore 클래스 내에서 직접 만들었음
- 모든 피자 객체를 직접 생성해야 하므로, 이 PizzaStore는 모든 피자 객체에 직접 의존하게 됨
- 피자 구상 클래스가 변경되면 PizzaStore의 코드 수정 가능성이 있으므로, "PizzaStore는 피자 클래스 구현에 의존한다"
- 피자 종류를 새로 추가하면 PizzaStore는 더 많은 피자 객체에 의존하게 됨
따라서, 클래스 의존성을 줄이면 좋다!
디자인 원칙
04. 추상화 된 것에 의존하게 만들고, 구상클래스에 의존하지 않게 만든다 => LSP 리스코프 치환 원칙
의존성 뒤집기 원칙
- 의존성 뒤집기 원칙에서는 추상화를 더 많이 강조한다.
- 고수준 구성 요소가 저수준 구성 요소에 의존하면 안 되며, 항상 추상화에 의존하게 만들어야 한다.
- 구상 클래스처럼 구체적인 것이 아닌 추상 클래스나 인터페이스와 같이 추상적인 것에 의존하는 코드를 만들어야 한다.
고수준
다른 '저수준' 구성 요소에 의해 정의되는 행동이 들어있는 행동 요소
예를 들어 PizzaStore의 행동은 피자에 의해 정의되므로 PizzaStore는 고수준 구성 요소이다.
PizzaStore는 다양한 피자 객체를 만들고, 굽고, 자르고, 포장
저수준
이때 PizzaStore에서 사용하는 피자 객체
의존성 뒤집기 원칙 적용하기
팩토리 메소드 패턴을 적용한 PizzsStore 코드
Pizza라는 추상 클래스를 만들긴 했으나, 이 코드에서 구상 피자 객체를 생성하는 것이 아니기에 얻는 것이 별로 없음
문제점
- 팩토리 메소드 패턴을 적용하면 고수준 구성 요소인 PizzaStore와 저수준 구성 요소인 피자 객체 모두 추상 클래스인 Pizza에 의존
- 여전히 저수준 구성요소가 고수준 추상 클래스에 의존. 이전의 다이어그램에서 의존성이 위에서 아래로 뒤집혔을 뿐. 고수준 모듈과 저수준 모듈이 추상 클래스에 의존하고 있음
의존성 뒤집기 원칙을 지키는 방법
- 변수에 구상 클래스의 레퍼런스 저장하지 않기
- new 연산자를 사용하면 구상 클래스의 레퍼런스를 사용하게 된다. 그러니 팩토리를 써서 구상 클래스의 레퍼런스를 변수에 저장하는 일을 미리 방지
- 구상 클래스에서 유도된 클래스 만들지 않기
- 구상 클래스에서 유도된 클래스르 만들면, 특정 구상 클래스에 의존하게 된다. 인터페이스나 추상 클래스처럼 추상화된 것으로 부터 클래스를 만들어야 한다
- 베이스 클래스에 이미 구현되어 있는 메소드를 오버라이드 하지 않기
- 이미 구현되어 있는 메소드를 오버라이드 한다면 베이스 클래스가 추상화가 제대로 되지 않으므로, 베이스 클래스에서 메소드를 정의할 때는 서브클래스에서 공유할 수 있는 것만 정의해야한다.
추상화 팩토리 만들기
상황
각 지점에서 우리가 정한(굽고, 자르고, 상자에 담는 등의) 절차는 따르나, 몇 지점에서 자잘한 재료를 더 싼 재료로 바꿔 마진을 높이고 있다.
요구 사항
- 각 지점에서 서로 다른 재료를 사용하도록 관리
- 원재료를 생산하는 공장을 만들고 각 지점까지 재료를 배달하도록 만들기
해결 방안
원재료 군으로 묶고,원재료를 생산하는 팩토리를 만든다
- 원재료 군 - 반죽, 소스,치즈,해산물 토핑 or 야채, 향신료
- 생산지 - 뉴욕, 시카고, 캘리포니아
원재료 팩토리 만들기
public interface PizzaIngredientFactory {
// 반죽, 소스, 치즈 등
public Dough createDough();
public Sauce createSauce();
public Cheese createCheese();
public Veggies[] createVeggies();
public Pepperoni createPepperoni();
public Clams createClam();
}
- 이 팩토리에서는 원재료군에 들어있는 각각의 원재료를 생산한다.
- 인터페이스에 각 재료별 메소드를 정의
- 재료가 추가 된다면, 재료마다 클래스를 만들어야 한다
뉴욕 원재료 팩토리 만들기
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();
}
}
- PizzaIngredientFactory를 뉴욕 원재료 팩토리에서도 구현
- 뉴욕 팩토리에서는 마리나라 소스, 레지아노 치즈, 신선한 조개 등을 전문적으로 생산
피자 클래스 변경하기
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("175도에서 25분간 굽기");
}
void cut() {
System.out.println("피자를 사선으로 자르기");
}
void box() {
System.out.println("상자에 피자 담기");
}
void setName(String name) {
this.name = name;
}
String getName() {
return name;
}
public String toString() {
// 기타 출력문
}
}
- prepare()를 추상 메소드로 만들어, 피자를 만드는 데 필요한 재료들을 가져온다. 모든 재료는 원재료 팩토리에서 가져옴
- prepare()를 제외한 다른 메소드는 변경되지 않는다
치즈 피자 코드
public class CheesePizza extends Pizza {
PizzaIngredientFactory ingredientFactory;
// 피자의 원재료를 제공하는 팩토리가 필요.
public CheesePizza(PizzaIngredientFactory ingredientFactory) {
this.ingredientFactory = ingredientFactory;
}
void prepare() {
System.out.println("준비중 " + name);
dough = ingredientFactory.createDough();
sauce = ingredientFactory.createSauce();
cheese = ingredientFactory.createCheese();
}
}
- 피자마다 클래스를 지역별로 따로 만들 필요가 없다. 지역별로 다른 점은 원재료 팩토리가 처리
- 각 피자 클래스는 생성자로부터 팩토리를 전달받고, 그 팩토리를 인스턴스 변수에 저장한다.
- prepare()메소드에서 치즈 피자를 만드는 각 단계를 처리한다. 재료가 필요할 때마다 팩토리에 있는 메소드를 호출해서 만든다
sauce = ingredientFactory.createSauce();
- sauce - Pizza에 있는 인스턴스 변수에 이 피자에서 사용할 특정 소스의 레퍼런스를 대입
- ingredientFactory - 우리가 사용하는 원재료 팩토리. Pizza 클래스는 원재료 팩토리가 맞기만 하면 어떤 팩토리든 상관 x
- createSauce(); - 해당 지역에서 사용하는 소스를 리턴
뉴욕 피자 가게 코드
public class NYPizzaStore extends PizzaStore {
protected Pizza createPizza(String item) {
Pizza pizza = null;
// 뉴욕 지점에는 뉴욕 피자 원재료 팩토리를 전달해줘야 함.
// 뉴욕 스타일 피자를 만들 때 필요한 재료는 이 팩토리에서 공급
PizzaIngredientFactory ingredientFactory =
new NYPizzaIngredientFactory();
if (item.equals("cheese")) {
// 피자에 맞는 재료를 만드는 팩토리를 피자 객체에 전달
pizza = new CheesePizza(ingredientFactory);
pizza.setName("뉴욕 스타일 치즈 피자");
} else if (item.equals("veggie")) {
pizza = new VeggiePizza(ingredientFactory);
pizza.setName("뉴욕 스타일 야채 피자");
} else if (item.equals("clam")) {
pizza = new ClamPizza(ingredientFactory);
pizza.setName("뉴욕 스타일 조개 피자");
} else if (item.equals("pepperoni")) {
pizza = new PepperoniPizza(ingredientFactory);
pizza.setName("뉴욕 스타일 페페로니 피자");
}
return pizza;
}
}
- 피자 형식마다 새로운 Pizza 인스턴스를 만들고 원재료를 공급받는데 필요한 팩토리를 지정해줌
클라이언트 코드
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("Rosie가 주문한 " + pizza + "\n");
pizza = chicagoStore.orderPizza("pepperoni");
System.out.println("posie가 주문한 " + pizza + "\n");
}
}
결과
--- Making a 뉴욕 스타일 치즈 피자 ---
준비중 뉴욕 스타일 치즈 피자
175도에서 25분간 굽기
피자를 사선으로 자르기
상자에 피자 담기
Rosie가 주문한 ---- 뉴욕 스타일 치즈 피자 ----
씬 크러스트 도우
마리노 소스
레지노 치즈
--- Making a 시카고 스타일 페페로니 피자 ---
준비중 시카고 스타일 페페로니 피자
175도에서 25분간 굽기
피자를 사선으로 자르기
상자에 피자 담기
posie가 주문한 ---- 시카고 스타일 페페로니 피자 ----
아주 두꺼운 크러스트 도우
플럼 토마토 소스
잘게 썬 모짜렐라
새로운 코드로 피자가 만들어지는 과정
- 피자 가게 생성
- PizzaStore nyPizzaStore = new NYPizzaStore();
- nyPizzaStore 인스턴스 생성
- 피자 주문
- nyPizzaStore.orderPizza("cheese");
- nyPizzaStore 인스턴스의 orderPizza()메소드가 호출
- 피자 생성
- orderPizza() 메소드는 일단 createPizza() 메소드 호출
- Pizza pizza = createPizza("cheese");
- 원재료 팩토리 사용
- Pizza pizza = new CheesePizza(nyIngredientFactory);
- PizzaStore에서 원재료 팩토리를 선택하고 그 인스턴스를 생성. 원재료 팩토리는 각 피장의 생성자에 전달됨
- 뉴욕 원재료 팩토리를 사용하는 Pizza 인스턴스 생성
- 피자 준비
- prepare()메소드를 호출하면 팩토리에 원재료 주문이 들어감
- 피자 만들기 작업 마무리
- orderPizza()메소드는 피자를 굽고, 자르고 포장함
다이어그램
정리
추상팩토리 패턴은 구상 클래스에 의존하지 않고도 서로 연관되거나 의존적인 객체로 이루어진 제품군을 생산하는 인터페이스를 제공한다. 구상클래스는 서브클래스에서 만든다.
역대급 다이어그램 소요,, 그래도 추상팩토리 이해완 !!
참고
헤드퍼스트 디자인 패턴
https://devowen.com/326#%EC%B0%B8%EA%B3%A0%EB%AC%B8%ED%97%8C-1
'JAVA > 디자인 패턴' 카테고리의 다른 글
[Factory Pattern] 팩토리 패턴 - 팩토리 메소드 (0) | 2023.07.09 |
---|---|
[Simple Factory] 간단한 팩토리 (0) | 2023.07.07 |
[Strategy Pattern] 전략 패턴 (1) | 2023.07.06 |
[Decorator Pattern] 데코레이터 패턴 (0) | 2023.07.04 |
[Proxy Pattern] 프록시 패턴 - 보호 프록시 (0) | 2023.07.04 |