* 본 포스팅은 한빛미디어의 헤드퍼스트 디자인 패턴을 공부한 내용을 정리한 글입니다.
저의 부족한 생각과 주관으로 틀린 내용이 있을 수 있으니,
자세한 내용이 궁금하시다면 해당 책을 읽어보시길 추천드립니다.
* 내용이 너무 많아, 이번 팩토리 챕터는 나눠서 작성하게 되었습니다
1. 지난 포스팅
지난 포스팅에선 심플 팩토리 패턴에 대해서 다뤘습니다.
심플 팩토리 패턴은 엄밀히 말하면 관용구에 가깝다고 말씀 드렸습니다.
최첨단 피자의 최초 코드에서 생성부를 심플 팩토리 패턴을 사용해 캡슐화 하였습니다.
2. 팩토리 메서드 패턴
팩토리 메소드 패턴은 서브클래스에서 어떤 클래스를 만들지 결정함으로써 객체 생성을 캡슐화합니다.
예를 들자면, 최첨단 피자가게는 크게 생산자 클래스와 제품 클래스로 나눌 수 있습니다.
Creator Class
- Abstract Creator Class : 추상 생산자 클래스
객체(상품)을 구현하는 팩토리 메소드를 정의합니다. 단, 이 제품 클래스의 객체는 서브클래스에 의해 만들어지기 때문에 생산자 자체는 어떤 구상 제품 클래스가 만들어질지는 알 수 없습니다.
- Concrete Creator Class : 구상 생성자 클래스
추상 생산자 클래스를 구현한 서브클래스로써, 객체(상품)을 생산합니다.
피자가게를 예로 든다면, 각 분점마다 추상 생산자 클래스(PizzaStore)의 서브클래스가 존재해 분점마다 고유의 피자를 만들 수 있습니다.
Product Class
- Creator Class의 팩토리 메소드를 통해 만들어진 제품입니다.
2-1. 최첨단 피자에 패토리 매서드 패턴 적용
최첨단피자가 성공해, 체인이 전국적으로 퍼져나가게 되었습니다.
하지만 미국의 주마다 피자 스타일이 다 다른데, 같은 종류만을 판다면 매출은 폭망하겠죠.
그래서 지역의 특색에 맞게 팩토리를 추가하게 되었습니다.
NYPizzaFactory nyFactory = new NYPizzaFactory();
PizzaStore nyStore = new PizzaStore(nyFactory);
nyStore.orderPizza("Veggie");
ChicagoPizzaFactory chicagoFactory = new ChicagoPizzaFactory();
PizzaStore chicagoStore = new PizzaStore(chicagoFactory);
chicaoStore.orderPizza("Veggie");
이렇게 되면, 뉴욕과 시카고에서 "Veggie"를 주문해도 지역에 맞게 차별화된 피자가 나오게 됩니다.
뉴욕은 도우가 얇고 바삭하지만, 시카고는 도우가 두툼하고 치즈가 가득 담긴 피자가 나오게 되죠.
하지만, PizzaSotre class는 구상클래스여서 캡슐화하지 않은 prepare, bake, cut, box들을 제각각 구현될 수 있습니다.
체인은 맛의 표준화가 최우선인만큼 프레임워크를 구성해 일관성을 유지하게끔 해야 합니다.
캡슐화한 피자 팩토리 외의 과정도 일관성을 유지할 수 있도록 리팩토링해 보겠습니다.
2-2 PizzaSotre를 추상클래스로 리팩토링
public abstract class PizzaStore{
public Pizza orderPizza(String type){
Pizza pizza;
pizza = createPizza(type);
pizza.prepate();
pizza.bake();
pizza.cut();
pizza.box();
return pizza;
}
abstract Pizza createPizza(String type);
}
얼핏보면 달라진점이 없어보이지만, PizzaStore의 타입이 class -> abstract class로 변화했습니다,.
추상메서드로 선언했으므로, orderPizza 메소드에 대해 각 지점들이 개입할 수 없어졌습니다.
그리고 주입받은 factory대신 PizzaStore에 있는 createPizza()를 통해 Pizza를 획득합니다.
어? 그러면 pizza는 언제 생성되는걸까요?
Pizza는 PizzaSotre의 서브클래스에서 생성합니다.
public class NYPizzaStore extends PizzaStore{
Pizza createPizza(String item){
if(item.equals("cheese")){
return new NTStyleCheesPizza();
} else if(item.equals("veggie")){
return new NYStyleVeggiePizza();
}
//...etc
}
이렇게 되면, 추상매서드의 orderPizza는 어떤 피자가 반환되는지 모르게 됩니다.
pizza객체를 받아 처리했을 뿐이죠. 이런 관계를 느슨한 결합 관계라고 합니다.
3. 병렬 클래스 계층 구조
제품과 생산자는 둘다 추상 클래스를 상속받습니다.
그리고 제품과 생산자를 확장하는 구상 클래스들이 존재합니다.
거기에 각 생산자 클래스의 구체적인 구현은 제품 클래스의 구상 클래스들이 책임지고 있습니다.
NYPizzaStore에는 뉴욕스타일 피자를 만드는 모든 방법이 캡슐화 되어있지만 추상생산자 클래스를 확장합니다.
뉴욕스타일 피자는 NYPizzaStore를 통해 만들어지지만, 제품클래스를 상속받고 있죠.
4. 팩토리 메소드 패턴 정의
💡팩토리 메소드 패턴이란?
객체를 생성할 때 필요한 인터페이스를 만드는 것
팩토리 메소드 패턴을 사용하면 클래스 인스턴스 만드는 일을 서브클래스에서 결정하게 됩니다.
여기서, 서브클래스가 결정한다는 표현을 쓰는 이유는 생산자 클래스가 실제 생산될 제품을 전혀 모르는 상태로 인스턴스가 만들어지기 때문입니다.
즉, 사용하는 서브클래스에 따라 생산되는 객체 인스턴스가 결정됩니다.
생산자 추상 클래스에서 객체를 만드는 factoryMethod()를 제공합니다.
생산자의 추상 클래스에 구현되어 있는 다른 메소드는 위 메소드로 생산된 제품으로 필요한 작업을 처리합니다.
그러나, 실제로 factoryMethod()를 구현하고 제품을 반환하는 일은 서브클래스에서만 할 수 있습니다.
팩토리 메소드 패턴의 특징
- 구상 생산자 클래스가 하나밖에 없어도 생산과 사용을 분리할 수 있고,
이후 변화에 대응하기 유리하기때문에 유용할 수 있다. - 팩토리 메소드의 매개변수가 단순 String이라면 typeSafety에 지장이 올 수 있다.
이를 방지하기 위해 매개변수 형식을 특정 객체로 사용하거나, 정적상수, 또는 enum을 사용할 수 있다. - 심플 팩토리는 일회용이고, 팩토리 메소드는 재사용이 가능한 프레임워크이다.
new를 사용한 구상 클래스의 인스턴스 생성은 프로그램을 짜는데 없어선 안되는 코드입니다.
하지만, 모아두고 체계적으로 관리할 수 있는 디자인을 짠다면, 유지보수에 굉장히 도움이 되겠죠?
5. 의존성 역전의 법칙(Dependency Inversion)
객체 인스턴스를 직접 만들면 구상 클래스에 의존할 수 밖에 없습니다.
피자라는 객체 인스턴스를 직접 만들게 되면, 각 인스턴스들이 변경될때마다 피자가게의 코드를 수정해야 합니다.
또한 새로운 피자가 추가될때마다 의존성이 추가가 되게 되죠.
만약, 피자가게가 Pizza라는 추상객체만 만들게 된다면 피자가게는 Pizza 추상객체에만 의존성을 가지도록 줄일수 있죠.
좌 디자인의 문제는심하게 의존적인구조란 점입니다. 피자가게가 모든 종류의 피자에 의존하죠.
왜냐면 오더 피자 메소드에서 구상 형식의 인스턴스를 직접 만들기 때문입니다.
피자라는 추상 클래스를 만들었지만, 구상 피자 객체를 생성하는것은 아니니까요.
오더 피자에서 팩토리 메소드 패턴을 사용해 인스턴스 생성부를 분리해 우의 사진처럼 의존성을 줄일 수 있습니다.
우리가 지금까지 배워온것중 하나가 구상클래스의 의존성을 줄일수록 좋다는 것입니다.
이를 위한 디자인 원칙이 존재하는데 그것이 바로 의존성 역전의 법칙입니다.
💡의존성 역전의 법칙(Dependency Inversion)
구상 클래스에 의존하지 않고 추상화에 의존하게 만든다.
고수준의 구성 요소가 저수준의 구성 요소에 의존하면 안 되며, 항상 추상화에 의존하게 만들어야 한다
- 고수준 구성 요소 : 저수준 구성 요소에 의해 정의되는 행동들 (PizzaStore)
- 저수준 구성 요소 : 행동이 정의되야 하는 객체 (Pizza)
DI는 구현보다는 인터페이스에 맞춰서 프로그래밍한다라는 원칙보다, 추상화를 더 강조합니다.
예를 즉면, 피자가게의 행동은 피자에 의해 정의되므로 피자가게는 고수준 구성요소입니다.
피자가게는 다양한 피자 객체를 만들고 준비하고 굽고 자르고 포장하는데,
이때 사용하는 피자 객체는 저수준 구성 요소입니다.
즉, 피자가게가 피자에 결정되면 안된다 라고 할 수 있죠.
뉴욕의 피자가게에서 뉴욕치즈피자를 만드는것이지, 뉴욕치즈피자때문에 뉴욕피자가게가 생기는건 아니니까요!
DI원칙을 준수하는 유일한 방법이 팩토리 메소드는 아니지만, 적합한 방법중 하나라 할 수 있습니다.
DI 원칙을 준수하는 법
- 변수에 구상 클래스의 레퍼런스를 저장하지 않는다.
new연산자를 사용하면 구상 클래스의 래퍼런스를 사용하게 되므로,
팩토리를 써서 구상클래스의 레퍼런스를 변수에 저장하는 일을 방지한다. - 구상 클래스에서 유도된 클래스를 만들지 않는다.
해당 케이스에 경우 특정 구상 클랫그에 의존하게 되므로, 추상화된 것으로부터 클래스를 만들어야 한다. - 베이스 클래스에 이미 구현된 메소드를 오버라이드 하지 않는다.
이미 구현된 메소드를 오버라이드하면 베이스클래스가 제대로 추상화되지 않는다. 그러므로, 서브클래스에서 공유할 수 있는 것만 정의하도록 해야 한다.
물론 원칙을 다 지킬 수 없습니다. 지향해야할 바를 알려주는 가이드라인인 것이죠. ㅎ
마무리로 지금까지의 주문부터 피자 생산까지의 흐름을 보겠습니다.
다음 포스팅은 추상 팩토리 패턴으로 찾아오겠습니다.
'개발공부 > Code design' 카테고리의 다른 글
헤드퍼스트 디자인 패턴 06.커맨드(Command) 패턴 (0) | 2022.12.10 |
---|---|
헤드퍼스트 디자인 패턴 05.싱글턴(Singleton) 패턴 (0) | 2022.11.22 |
헤드퍼스트 디자인 패턴 04.팩토리(Factory) 패턴 - 심플 팩토리 (0) | 2022.09.25 |
헤드퍼스트 디자인 패턴 03.데코레이터(Decorator) 패턴 (0) | 2022.08.08 |
헤드퍼스트 디자인 패턴 2장. 옵저버(Observer)패턴 (0) | 2022.07.17 |