정상에서 IT를 외치다

[디자인패턴] 팩토리패턴 본문

디자인패턴

[디자인패턴] 팩토리패턴

Black-Jin 2019. 5. 13. 23:36
반응형

안녕하세요. 블랙진입니다.

책 HeadFirstDesignPattern 을 보며 코드를 Kotlin 으로 바꿔가며 공부한 내용입니다.


New는 구상 객체를 뜻합니다.


new를 사용하는 것은 구상 클래스의 인스턴스를 만드는 것입니다. 당연히 인터페이스가 아닌 특정 구현을 사용합니다. 구상 클래스를 바탕으로 코딩을 하면 나중에 코드를 수정해야 할 가능성이 높아지고, 유연성이 떨어지게 됩니다.



문제


피자를 주문할 수 있는 orderPizza가 있습니다. type을 매개변수로 받아 Cheese, Greek, Pepperoni 피자를 new 를 사용해 만들고 있습니다. 이런 코드가 있다는 것은 뭔가 변경하거나 확장해야 할 때 코드를 다시 확인하고 추가 또는 제거해야 되는 불편함이 있습니다. 또한 오류가 생길 가능성도 높아지겠죠?

fun orderPizza(type: String):Pizza? {

var pizza: Pizza? = null

when(type) {

"cheese" -> {
pizza = CheesePizza()
}
"greek" -> {
pizza = GreekPizza()
}
"pepperoni" -> {
pizza = PepperoniPizza()
}
}

pizza?.run {
prepare()
bake()
cut()
box()
}
return pizza
}




문제 해결 1


바뀌는 부분과 바뀌지 않은 부분으로 나눠 캡슐화를 진행해줍니다. 이때 간단한 팩토리라는 객체 생성 부분을 캡슐화 하여 객체 생성을 처리하는 클래스인 팩토리를 만들어 보겠습니다.


class SimplePizzaFactory {

fun createPizza(type: String):Pizza? {

var pizza: Pizza? = null

when(type) {

"cheese" -> {
pizza = CheesePizza()
}
"greek" -> {
pizza = GreekPizza()
}
"pepperoni" -> {
pizza = PepperoniPizza()
}
}

return pizza
}

}

피자를 생성하는 부분을 SimplePizzaFactory 라는 클래스로 분리했습니다. 이렇게 피자를 생성하는 작업을 한 클래스에 캡슐화시켜 놓으면 구현을 변경해야 하는 경우에 여기저기 다 들어가서 고칠 필요 없이 이 팩토리 클래스 하나만 고치면 됩니다.


class PizzaStore(private val factory: SimplePizzaFactory) {

fun orderPizza(type: String): Pizza? {

val pizza = factory.createPizza(type)

pizza?.run {
prepare()
bake()
cut()
box()
}

return pizza
}

}

PizzaStore 에서는 프로퍼티로 SimplePizzaFactory 를 받아 피자를 생성해 주고 있습니다. 이렇게 받은 피자를 prepare() bake() cut() box() 하여 반환해 줍니다.


간단한 팩토리


객체 생성을 처리하는 클래스를 팩토리라고 부릅니다. 간단한 팩토리는 디자인 패턴이라고는 할 수 없습니다. 프로그래밍을 하는데 있어서 자주 쓰이는 관용주에 가깝다고 할 수 있습니다.





문제 


피자 가게가 잘되 여러 동네에도 PizzaStore가 있었으면 하는 바램에 프렌차이즈를 운영하게 되었습니다. 지역별로 조금씩 다른 (뉴욕과 시카고 스타일의) PizzasStore을 만들어 보겠습니다


class NYPizzaFactory: PizzaFactory() {

override fun createPizza(type: String):Pizza? {

var pizza: Pizza? = null

when(type) {

"cheese" -> {
pizza = NYCheesePizza()
}
"greek" -> {
pizza = NYGreekPizza()
}
"pepperoni" -> {
pizza = NYPepperoniPizza()

}
}

return pizza
}

}

class ChicagoPizzaFactory: PizzaFactory() {

override fun createPizza(type: String):Pizza? {

var pizza: Pizza? = null

when(type) {

"cheese" -> {
pizza = ChicagoCheesePizza()

}
"greek" -> {
pizza = ChicagoGreekPizza()

}
"pepperoni" -> {
pizza = ChicagoPepperoniPizza()

}
}

return pizza
}

}

뉴욕과 시카고 팩토리를 정의하여 새로운 분점을 냈습니다.


PizzaStore(NYPizzaFactory()).orderPizza("cheese")

PizzaStore(ChicagoPizzaFactory()).orderPizza("cheese")

이제 PizzaStore은 두 팩토리를 사용해 피자를 뉴욕피자와 시카고피자를 만들 수 있습니다. 하지만! 역시 위와 같은 방법으로는 분점마다 다른 방식의 피자를 만드는 것을 관리할 수 없습니다. 한 예로 뉴욕치즈피자는  뉴욕팩토리에서만 관리하고 있지 본점하고의 아무 연결 고리가 없습니다. 즉 분점 팩토리들은 자신 마음대로 피자를 만들 수 있게 됩니다. 이에 맞춰 피자 가게와 피자 제장 과정 전체를 하나로 묶어주는 프레임워크가 필요해 졌습니다.



문제 해결 2


abstract class PizzaStore {

fun orderPizza(type: String): Pizza? {

val pizza = createPizza(type)

pizza?.run {
prepare()
bake()
cut()
box()
}

return pizza
}

/**
* 팩토리 메소드 선언
*
* 구상 클래스의 인스턴스를 만드는 일을 한 객체에서 전부 처리하는 방식에서 일련의 서브클래스에서 처리하는 방식으로 넘어오게 되었습니다.
* Pizza 인스턴스를 만드는 일은 이제 팩토리 역활을 하는 메소드에서 맡아서 처리합니다.
*
* 팩토리 메소드는 객체 생성을 처리하며, 팫토리 메소드를 이용하면 객체를 생성하는 작업을 서브클래스에 캡슐화시킬 수 있습니다.
*/
abstract fun createPizza(type: String): Pizza?

}


기존 피자 스토어를 추상 클래스로 바꿔 줍니다. createPizza 를 수상 메소드로 선언하고 각 지역마다 고유의 스타일에 맞게 PizzaStore의 서브클래스를 만들도록 해줍니다. 여기서 사용한 createPizza 가 바로 팩토리 메소드입니다.


이제 PizzaStore을 상속받은 지역별 팩토리를 생성해 보겠습니다.

class NYPizzaStore : PizzaStore() {

override fun createPizza(type: String):Pizza? {

var pizza: Pizza? = null

when(type) {

"cheese" -> {
pizza = NYStyleCheesePizza()
}
"greek" -> {
pizza = NYStyleGreekPizza()
}
"pepperoni" -> {
pizza = NYStylePepperoniPizza()
}
}

return pizza
}
}

class ChicagoPizzaStore : PizzaStore(){

override fun createPizza(type: String):Pizza? {

var pizza: Pizza? = null

when(type) {

"cheese" -> {
pizza = ChicagoStyleCheesePizza()
}
"greek" -> {
pizza = ChicagoStyleGreekPizza()
}
"pepperoni" -> {
pizza = ChicagoStylePepperoniPizza()
}
}

return pizza
}
}

뉴욕과 시카고 팩토리에서 각각의 스타일에 맞는 피자를 생성합니다. 여기서 이전 코드와의 큰 다른점은 바로 분점 팩토리 들은 PizzaStore 의 createPizza 를 오버라이드해서 만든다는 것입니다. 이렇게 함으로서 분점에서 제멋대로 만들었던 피자를 본점에서 관리할 수 있게 되었습니다.


그럼 주문을 한번 해보겠습니다. 에단은 얇고 바삭바삭한 빵에 치즈는 약간만 올리고 맛있는 소스를 사용하는 뉴욕풍 피자를 주문합니다. 조엘은 두껍고 깊숙한 크러스트에 치즈 왕창 들어있는 시카고 피자를 주문합니다.

/**
* 주문하기
*
* 에단은 뉴욕풍 피자를 좋아하고
* 조엘은 시카고풍 피자를 좋아합니다.
*/
fun main() {

val nyStore = NYPizzaStore()
val chicagoStyle = ChicagoPizzaStore()

var pizza: Pizza?

pizza = nyStore.orderPizza("cheese")
println("Ethan ordered a : ${pizza?.name} \n")

pizza = chicagoStyle.orderPizza("cheese")
println("Joel ordered a : ${pizza?.name} \n")
}


팩토리 메소드 패턴


팩토리 메소드 패턴에서는 객체를 생성하기 위한 인터페이스를 정의하는데, 어떤 클래스의 인스턴스를 만들지는 서브클래스에서 결정하게 만듭니다. 팩토리 메소드 패턴을 이용하면 클래스의 인스턴스를 만드는 일을 서브클래스에게 맡기는 것이죠.




문제


여기서 또하나의 문제가 발생했습니다, 바로 분점마다 거리가 있어 사용하는 재료의 원산이가 다르다는 것입니다. 분점에서 좋은 재료를 사용하도록 관리해봅시다. 원재료를 생성하는 공장을 만들고 분점까지 재료를 배달할 수 있게 해보겠습니다.


interface PizzaIngredientFactory {

fun createDough(): Dough
fun createSauce(): Sauce
fun createCheese(): Cheese
//...
}

우선 모든 원재료를 생상할 팩토리를 위한 인터페이스를 정의해 줍니다.


class NYPizzaIngredientFactory : PizzaIngredientFactory {

override fun createDough(): Dough {
return ThinCrustDough()
}

override fun createSauce(): Sauce {
return MarinaraSauce()
}

override fun createCheese(): Cheese {
return ReggianoCheese()
}

}

이제 뉴욕 원재료 공장을 구현합니다. 이 팩토리에서는 얇고 바삭바삭한 빵과 마리나라 소스 그리고 레지아노 치즈를 전문적으로 생산합니다. 이제 재료를 생산할 준비가 끝났습니다. Pizza 클래스에서 팩토리에서 생성한 원재료만 사용하도록 코드를 고쳐줍니다.



문제 해결 3

abstract class Pizza(
var dough: Dough? = null,
var sauce: Sauce? = null,
var cheese: Cheese? = null
) {

abstract fun prepare()
}

class CheesePizza(val ingredientFactory: PizzaIngredientFactory): Pizza() {

override fun prepare() {

dough = ingredientFactory.createDough()

sauce = ingredientFactory.createSauce()

cheese = ingredientFactory.createCheese()
}
}

우리는 CheesePizza에 원재료 공장을 매개변수로 받습니다. 기존에는 아래와 같이 

class NYStyleCheesePizza: Pizza(
name = "NYStyleCheesePizza",
dough = "Thin Crust Dough",
sauce = "Marinara Sauce"
)

class ChicagoStyleCheesePizza: Pizza(
name = "ChicagoStyleCheesePizza",
dough = "Extra Thick Crust Dough",
sauce = "Plum Tomato Sauce"
)

뉴욕스타일치즈피자와 시카고스타일치즈피자가 따로 존재했습니다. 이는 본점에서 관리할 수 없었지만 위와같이 재료 공장을 만들어 줌으로서 CheesePizza 하나만을 가지고 여러 스타일의 치즈 피자를 만들 수 있게 되었습니다.


sauce = ingredientFactory.createSauce()


피자 코드에서는 팩토리를 이용하여 피자에서 쓰이는 재료를 만듭니다. 만들어지는 재료는 어떤 팩토리를 쓰는지에 따라 달라지죠. 피자 클래스에서는 전혀 신경을 쓰지 않습니다. 피자를 만드는 방법만을 알고 있을 뿐이니까요.



피자 가게를 다시 살펴보겠습니다.

class NYPizzaFactory : PizzaStore() {

/**
* 추상 팩토리 패턴에서는 인터페이스를 이용하여 서로 연관된, 또는 의존하는 객체를 구상 클래스를 지정하지 않고도 생성할 수 있습니다.
* 서로 연관된 또는 의존적인 객체들로 이루어진 제품군을 생성하기 위한 인터페이스를 제공합니다. 구상 클래스는 서브클래스에 의해 만들어지죠.
*/
override fun createPizza(type: String):Pizza? {

var pizza: Pizza? = null

val nyIgredientFactory = NYPizzaIngredientFactory()

when(type) {

"cheese" -> {
pizza = CheesePizza(nyIgredientFactory)
}
"greek" -> {
pizza = GreekPizza(nyIgredientFactory)
}
"pepperoni" -> {
pizza = PepperoniPizza(nyIgredientFactory)
}
}

return pizza
}
}


피자스토어 안에는 뉴욕 재료 팩토리가 있습니다. 이 팩토리를 이용해 뉴욕 스타일의 다양한 피자를 생성할 수 있습니다.




브레인 파워


여기에 있는 createPizza() 메소드와 앞에서 팩토리 메소드 패턴을 써서 처음으로 만들었던 메소드를 비교해 봅시다.


<팩토리 메소드 패턴>

class NYPizzaFactory : PizzaStore() {

override fun createPizza(type: String):Pizza? {

var pizza: Pizza? = null

when(type) {

"cheese" -> {
pizza = NYStyleCheesePizza()
}
"greek" -> {
pizza = NYStyleGreekPizza()
}
"pepperoni" -> {
pizza = NYStylePepperoniPizza()
}
}

return pizza
}
}


차이가 바로 보이시나요? 팩토리 메소드 패턴에서는 CheesePizza 를 분점에서 자체적으로 생산했습니다. 하지만 팩토리 추상 패턴을 사용함으로서 본점에서 관리하는 재료를 사용해 CheesePizza()를 만들게 되었습니다.


추상 팩토리 패턴


추상 팩토리 패턴에서는 인터페이스를 이용하여 서로 연관된, 또는 의존하는 객체를 구상 클래스를 지정하지 않고도 생성할 수 있습니다.




정리


팩토리 메서드 패턴

abstract fun createPizza(type: String): Pizza?


객체를 생성하기 위해 인터페이스를 정의하지만, 어떤 클래스의 인스턴스를 생성할 지에 대한 결정은 서브클래스가 내리도록 합니다. 즉 클래스가 객체를 생성합니다.



추상 팩토리 패턴

interface PizzaIngredientFactory {

fun createDough(): Dough
fun createSauce(): Sauce
fun createCheese(): Cheese
//...
}


상세화된 서브클래스를 정의하지 않고도 서로 관련성이 있거나 독립적인 여러 객체 군을 생성하기 위한 인터페이스를 제공합니다. 객체가 객체를 만드는 방법으로 상속 구조를 둠으로서 세밀한 팩토리 관리가 가능해 집니다.


반응형
Comments