팩토리 패턴
객체 생성을 서브 클래스에 캡슐화 할 수 있다. 그러면 슈퍼클래스에 있는 클라이언트 코드와 서브클래스에 있는 객체 생성 코드를 분리할 수 있습니다.
즉, 슈퍼클래스에 있는 코드에서 서브클래스에서 생성되는 객체가 무엇인지 알 수 없게 만드는 역할도 합니다.
Factory Method Pattern(팩토리 메소드 패턴)
객체를 생성할 때 필요한 인터페이스를 만듭니다. 어떤 클래스의 인스턴스를 만들지는 서브클래스에서 결정합니다. 팩토리 메소드 패턴을 사용하면 클래스 인스턴스 만드는 일을 서브클래스에게 맡기게 됩니다.
Abstract Factory Pattern(추상 팩토리 패턴)
구상 클래스에 의존하지 않고도 서로 연관되거나 의존적인 객체로 이루어진 제품군을 생산하는 인터페이스를 제공합니다. 구상 클래스는 서브 클래스에서 만듭니다.
1. 피자가게 만들기
맛있고 장사잘되는 피자가게를 만들려고 합니다. 하지만 문제가 생겼어요.
기존에는 치즈/그리스/페페로니 피자만 있었는데, 잘 안팔리는 그리스 피자를 없애고 조개/야채 피자를 넣으려고 하니 코드를 변경해야만 합니다.
앞으로도 메뉴는 시도때도없이 변할 것인데 그때마다 코드를 바꿔줄 순 없어요. 변하는 부분을 따로 빼서 캡슐화 해야 합니다.
2. 간단한 팩토리(Simple Factory)
객체 생성을 처리하는 클래스를 팩토리(Factory)라고 부릅니다.
위의 코드에서 변하는 부분을 빼내서 SimplePizzaFactory로 만들고, 앞으로 객체가 필요할 땐 SimplePizzaFactory 에게 객체를 만들어서 리턴해달라고 부탁하면 됩니다.
단순히 객체 생성을 다른 클래스로 넘겨버린 것이지만, 메뉴를 추가/변경할 때 팩토리 클래스 하나만 고칠 수 있고 또 피자주문을 배달로 하는 HomeDeliveryPizza 시스템에도 이 팩토리를 재사용할 수 있습니다.
이런 것을 Simple Factory라고 부릅니다. 패턴은 아닙니다. 그리고 간단한 팩토리 같은 경우 static 메소드도 사용하곤 하는데, 그것은 SimplePizzaFactory.createPizza("cheese") 처럼 인스턴스를 만들지 않기 위해 사용합니다. 다만 이 방법은 서브클래스를 만들어서 객체 생성 메소드의 행동을 변경할 수 없다는 단점은 있지요.
3. 팩토리 메소드 패턴
사업이 확장되어 같은 피자라도 뉴욕/시카고/캘리포니아 스타일의 피자를 생성해야 합니다. 각 피자는 지역에 따라 같은 종류의 피자라고 하더라도 레시피가 다릅니다. 뉴욕 스타일 피자는 빵이 얇고, 시카고 피자 스타일은 두껍죠.
SimplePizzaFactory와 factory 인스턴스를 삭제하고 createPizza() 를 추상메소드로 선언합니다.
각 지점에 맞는 서브클래스를 생성합니다. (NYPizzaStore/ChicagoPizzaStore/CaliforniaPizzaStore)
여기서 등장하는 것이 바로 팩토리 메소드 패턴입니다. 아래의 그림을 보면 더 이해가 빠르겠지만, 팩토리(객체)를 생성하는 메소드 패턴을 말합니다.
설명하자면, orderPizza()는 피자를 만드는 과정을 나타내는 메소드죠. 얘는 피자를 만들기만 합니다. 결정적으로 피자 객체를 생성하는 팩토리 메소드는 createPizza()인 거죠. 하지만 위에서도 말했다시피 createPizza()를 추상 메소드로 설정했기 때문에, 부모 클래스인 PizzaStore는 createPizza()를 실행하면 피자가 리턴된다 정도만 알지, 무슨 피자가 리턴되는지 같은 것은 모릅니다. 캡슐화 되어 있다고 표현할 수 있고, Pizza와 PizzaStore가 서로 완전히 분리되어 있다고 말할 수도 있을 것 같아요.
그러면 피자 종류는 누가 결정할까요? 자식 클래스인 NYPizzaStore 같은 녀석들이 합니다. 추상 메소드이기 때문에 createPizza()를 꼭 구현해야 하죠.
자세한 것은 아래 코드로 보는 것이 더 편할 겁니다.
4. 의존성 뒤집기 원칙(DIP)
상위 모듈은 하위 모듈에 의존해서는 안된다. 상위 모듈과 하위 모듈 모두 추상화에 의존해야 한다는 원칙입니다(Dependency Inversion Principle).
Dependency(의존성)란 ?
객체에 의존한다는 건 아래의 그림과 같은 겁니다. DependentPizzaStore는 모든 피자 객체를 클래스 내에서 직접 만들고 있어요.
각 피자 클래스의 클래스명이 변경된다거나, 또는 피자 메뉴가 추가된다면 PizzaStore까지 고쳐야해요. 상황에 따라서는 각 피자 클래스들의 내부 구현이 변경되도 바꿔야 할 수도 있죠.
그렇기 때문에 DependentPizzaStore는 피자 클래스 구현에 의존한다고 말할 수 있습니다.
Dependency Inversion Principle(의존성 뒤집기 원칙)
"구현보다는 인터페이스에 맞춰서 프로그래밍 한다." 라는 원칙과 유사할 수 있지만, DIP는 추상화를 더 강조합니다. 고수준 구성요소가 저수준 구성요소에 의존하면 안되고 항상 추상화에 의존하게 만들어야 한다는 뜻이 있어요.
고수준 구성요소와 저수준 구성요소라 함은 그런겁니다. 피자스토어를 예로 들면, 피자 스토어는 다양한 피자 객체를 만들고, 피자를 준비하고, 굽고, 자르고, 포장하죠. 즉, 피자스토어의 행동은 피자에 의해 결정되죠. 그래서 피자스토어는 고수준 구성요소이고, 피자스토어에서 사용하는 피자 객체는 저수준 구성요소 입니다.
지금 구현해놓은 팩토리 메소드 패턴을 사용하면 DIP 원칙을 지킬 수 있는데요. 위에서 구현한 코드를 보면 PizzaStore는 인터페이스고 Pizza 클래스를 사용합니다. PizzaStore가 고수준 구성요소, Pizza 클래스(얘또한 추상클래스)가 저수준 구성요소죠.
또, 구상 클래스인 피자 클래스 또한 인터페이스인 Pizza를 준수하죠. Pizza에 의존적인거죠. 여기서 중요한 것이 화살표인데요. 보통 저수준 -> 고수준, 아래에서 위로 화살표가 향하는데 아래 그림을 보면 고수준 구성요소인 PizzaStore 또한 Pizza에 의존합니다. 화살표 방향이 반대로 바뀐거죠. 그래서 의존성을 뒤집었다고 의존성 뒤집기 원칙이라고 하는 겁니다.
생각을 뒤집기
일반적인 생각(DIP 적용 전)
PizzaStore를 구현해야 한다고 합시다. 뭐부터 해야할까요 ?
PizzaStore는 Pizza를 준비하고, 굽고, 포장할 수 있어야 겠죠. Pizza 종류도 많아야 하고요.
하지만 PizzaStore가 각 Pizza 종류같은 것들을 알면 구상 Pizza Class에 의존하는 문제가 생겨요. 그렇다면, PizzaStore부터 생각하지 말고 뒤집어서 Pizza부터 생각해보죠.
생각 뒤집기(DIP 적용 후)
Pizza는 종류별로 다 다르지만 전부 다 Pizza입니다. 그러면 Pizza Protocol를 공유하는 것이 좋겠죠?
그리고 피자를 Pizza라는 인터페이스로 추상화했으니 PizzaStore는 Pizza 클래스를 신경쓰지 않아도 될 거에요.
다만, Pizza 인터페이스를 만든다고 끝이 아니라 PizzaStore에서 각 Pizza 클래스들을 없애야 겠죠. 그리고 그 방법 여러가지 중에 팩토리 메소드 패턴을 우리는 사용한거구요.
즉, 이런식으로 생각을 위에서 아래로 내려오는 것을 뒤집는다고 해서 의존성 뒤집기 원칙이라고 말합니다.
DIP를 지키는 가이드라인
변수에 구상 클래스의 레퍼런스를 저장하지 맙시다.
let pizza = ChicagoCheesePizza() 같은 걸 하지 말라는 뜻입니다. 클래스의 인스턴스를 만들지 말라는 것이죠.
구상 클래스에서 유도된 클래스를 만들지 맙시다.
인터페이스나 추상클래스처럼 추상화된 것으로 부터 클래스를 만들라는 말이에요. 그렇지 않으면 구상 클래스에 의존하게 되버리니까 말이죠.
베이스 클래스에 이미 구현되어 있는 메소드를 오버라이드 하지 맙시다.
이미 구현되어 있는 클래스를 오버라이드 해버리면 베이스 클래스가 제대로 추상화되지 않죠.
이 가이드라인을 다 지킬 수 있나요 ?
사실 불가능하고, 단지 지향해야 할 바를 알려주는 겁니다. 이 원칙을 알고 디자인하면 합리적인 이유로 불가피한 상황에서만 예외를 둘 수 있어요.
예를 들어, 어떤 클래스가 바뀌지 않는다면 그 클래스의 인스턴스를 만드는 코드를 작성해도 그리 큰 문제가 생기지 않죠.
Swift는 Struct지만 보통 String 객체의 인스턴스는 생각없이 만들어서 쓰죠. 이것도 엄밀히 따지면 원칙 위배지만 String 클래스가 바뀌는 일은 정말 거~~의 없죠. 언어의 Version이 바뀌어도 기능이 추가되면 됐지 String 클래스 자체가 바뀌는 일은 잘 없으니까요.
하지만, 우리가 만드는 클래스가 바뀔 수 있다면 변경될 수 있는 부분은 캡슐화를 해야 합니다.
5. 추상 팩토리 패턴
지역별로 피자의 종류는 같지만, 피자에 들어가는 재료가 다릅니다.
모든 피자에 관한 원재료를 생산하는 PizzaIngredientFactory 인터페이스를 만들고, 각 지역별로 NYPizzaIngredientFactory, ChicagoPizzaIngredientFactory와 같은 팩토리를 만듭니다.
또 각 팩토리에서 사용할 원재료 클래스를 구현합니다. Dough를 예로 들면, Dough 라는 인터페이스로 ThinDough, ThickDough, CheeseCrustDough 재료 클래스를 만드는 식이죠.
그리고 이렇게 만든 재료들을 PizzaStore에서 사용하도록 하나로 묶어야 합니다.
기존에 하드코딩 했던 재료들을 없애고, 원재료들을 Pizza 클래스에 넣어줍니다. 그리고 prepare() 메소드 또한 추상메소드로 선언합니다. 여기에서 각 지역별로 필요한 재료들을 가져올거에요. 나머지 메소드는 그대로 두어도 괜찮습니다.
그리고 이제 원재료를 팩토리에서 가져오도록 바꿔야 합니다. 원래는 CheesePizza도 각 지역별로 NYStyleCheesePizza와 같이 따로 따로 만들었지만, 이제 그럴 필요가 없어요.
치즈/조개/페퍼로니/야채 피자를 큰 틀로 두고 피자를 이루는 재료들만 각 지역에 맞게 다르게 넣어주면 됩니다.
마지막으로 각 지역별 PizzaStore에서 해당 지역에 맞는 팩토리를 넣어주면 됩니다.
6. 팩토리 메소드 패턴 vs 추상 팩토리 패턴
비슷하지만 공통점, 차이점이 있어요.
둘 다 객체를 만드는 일을 합니다.
팩토리 메소드 패턴
상속(Inheritance)으로 객체를 만듭니다. 정확히는 서브클래스로 객체를 만들죠.
클래스를 확장하고 팩토리 메소드를 오버라이드합니다.
PizzaStore 인터페이스, ChicagoPizzaStore 클래스에서 팩토리 메소드인 createPizza() 를 통해 Pizza 인터페이스를 return합니다.
그렇기 때문에 인터페이스(Protocol)는 메소드명, 리턴값만 알면 되죠. 구현은 서브클래스에서 하니까요.
정리하면, 서브클래스로 객체를 만들려고 사용합니다. 서브클래스에서 객체를 만ㄷ늘어서 주니까 인터페이스는 추상 형식만 알면 됩니다.
추상 팩토리 패턴
구성(Composition)으로 객체를 만듭니다. 그래서 PizzaStore가 각 지점에 맞는 Factory 인스턴스를 가지고 있지요.
추상 형식 PizzaIngredientFactory 을 제공합니다. 그리고 서브클래스인 NYPizzaIngredientFactory 가 제품을 만들죠.
그리고 각 서브클래스 팩토리가 제품을 생산합니다. 제품은 종류별로 인터페이스로 구성되어 있고, 팩토리에서 지점의 특성에 따라 맞는 제품을 리턴하죠. (도우라는 인터페이스에 클래스로 씬도우/두꺼운도우/치즈도우)
제품군을 확대하려면 인터페이스를 바꿔야 합니다. 그럼 서브클래스도 바꿔야 하죠. 하지만, 추상 팩토리는 보통 제품군을 만들 때 사용해요. (NYStyle, ChicagoStyle 처럼) 그래서 막 자주 바뀌진 않아요. 각 지역 스타일이 계속 바뀌지는 않을테니까요.
7. 정리하기
초반에는 Simple Factory를 사용했는데 차이점은?
팩토리 메소드 패턴은 실제 구현하는 구상 클래스를 만들 때 createPizza() 추상 메소드가 정의되어 있는 확장해서 만들었습니다. 서브 클래스는 해당 메소드를 오버라이드 해서 결정합니다.
반면에 Simple Factory를 사용할 때는 팩토리가 PizzaStore 안에 포함되는 별개의 인스턴스 였습니다. 전자가 캡슐화도 되어있고 더 유연하다고 볼 수 있죠.
Simple Factory는 객체 생성을 캡슐화했고, 일회성인 반면 팩토리 메소드 패턴은 재사용이 가능한 프레임워크를 제공합니다. 그리고 더 유연하죠. 어떤 피자 스토어를 만드느냐에 따라서 생성하는 제품을 마음대로 변경할 수 있으니까요.
구상 클래스가 하나밖에 없더라도 팩토리 메소드 패턴은 유용합니다. 생산하는 부분과 사용하는 부분을 분리할 수 있기 때문입니다. 새 메뉴가 나오더라도 PizzaStore는 수정할 게 없죠.(느슨한 결합)
여기서는 type을 String으로 사용했는데, 사실 enum 을 사용하는 것이 더 좋습니다.
8. 생각해보기
Factory라는 이름으로 createPizza() 메소드를 가지고 있는 인터페이스를 선언하고 PizzaStore는 하나만 두고, Factory를 NY, Chicago, California로 늘립니다.
이것은 뭐가 다를까. 각 장/단점이 있는걸까. 전략패턴?이라고 볼 수 있나.
Reference
https://www.hanbit.co.kr/store/books/look.php?p_code=B6113501223
Last updated