생성 패턴 이란?
소프트웨어 엔지니어링에서 생성 디자인 패턴은 개체 생성 메커니즘을 다루는 디자인 패턴으로 상황에 적합한 방식으로 개체를 생성하려고 합니다. 개체 생성의 기본 형태는 디자인 문제를 일으키거나 디자인에 복잡성을 더할 수 있습니다. 생성 디자인 패턴은 이 객체 생성을 어떻게든 제어함으로써 이 문제를 해결합니다. --wikipeda--
어떤 아이디어에 착안해서 패턴으로 자리 잡았는가?
2가지 의 메인 콘셉트가 있다. 하나는 시스템 이 사용하는 구체적인 클래스에 대해 캡슐화하는 것이고, 다른 하나는 이런 구체적인 클래스의 인스턴스 생성 결합되는 방식을 숨기는 것이다.
나아가 생성패턴 은 클래스와 객체 생성 패턴으로 분류될 수 있다. 객체 생성 패턴은 객체를 , 클래스 생성 패턴은 클래스 인스턴스화를
처리한다.
Common Structure
왼쪽 은 대부분의 생성 패턴이 공통적으로 가지고 있는 간단한 클래스 다이어그램이다.
다른 생성 패턴에는 추가 및 다른 참여 클래스가 필요하다.
간단 한 설명을 보자면
· Creator = 개체의 인터페이스를 선언하고, 반환값을 준다.
· Concrete Creator = 개체 인터페이스를 구현하는 클래스이다.
이에 따라 잘 알려진 생성 디자인 패턴은
추상 팩토리, 빌더, 팩토리 메서드, 프로토 타입, 싱글톤 패턴으로 나뉘고 이중 팩토리 메서드 패턴부터 보자.
[팩토리 메서드 패턴]
클래스 기반 프로그래밍에서 정확한 클래스를 지정하지 않고도 객체 생성 문제를 처리하는 방식이다. 생성자를 호출하는 대신
인터페이스에 지정되고 자식클래스에 의해 구현되거나 기본,파생 클래스 에 의해 선택적으로 구현되는 방식이다.
"객체 생성을 공장 클래스, 함수로 캡슐화 처리하여 대신 생성 하게 하는 생성 디자인 패턴이다."
"객체 생성에 필요한 과정을 템플릿처럼 미리 구성해 놓고 , 객체 생성에 관한 전, 후 처리를 통해 객체를 유연하게 만들 수 있다"
아래와 같은 문제에 직면하게 된다면? 팩토리 메서드 패턴을 적용해 보면 최적의 선택이 될 수 있다.
- 서브 클래스가 인스턴스 화 할 클래스를 재정의하려면 어떻게 해야 할까?
- 클래스는 어떻게 인스턴화를 하위 클래스에 위임할 수 있을까?
의 주된 문제를 해결하는 설루션이 팩토리 메서드 패턴이다.
바로 가보자.
피자 가게를 운영한다고 가정해보자. 주문에 해당하는 코드를 아래와 같이 정의해 보자.
type Pizza struct{}
func (p *Pizza) prepare() {
fmt.Println("Prepare")
}
func (p *Pizza) bake() {
fmt.Println("Bake")
}
func (p *Pizza) cut() {
fmt.Println("cut")
}
func (p *Pizza) box() {
fmt.Println("boxing")
}
func orderPizza(t string) Pizza {
var p Pizza
switch t {
case "Cheese":
// 치즈 피자가 p 가 된다.
case "Hawaiian":
// 하와이안 피자가 p 가 된다
case "Margherita":
// 마르게리타 피자가 p 가 된다.
default:
// 디폴트 피자 는 햄 피자가 p 가 된다.
}
p.prepare()
p.bake()
p.cut()
p.box()
return p
}
이 코드의 문제점 만약 새로운 피자가 추가되거나 삭제되거나 하게 된다면 기존 코드 의 변경이 필요하다. 즉 저 orderPizza 부분 이 문제가 된다는 소리이다. Switch에 해당하는 부분을 캡슐화해보자.
package main
import "fmt"
type Pizza interface {
prepare()
bake()
cut()
box()
}
type pizza struct {
name string
}
func (p *pizza) prepare() {
fmt.Println("피자 소스, 토핑, 치즈 준비중 .....")
fmt.Println("오븐 달구는중 .......")
time.Sleep(time.Millisecond * 500)
}
func (p *pizza) bake() {
fmt.Println("피자 굽는중 ....")
time.Sleep(time.Second)
}
func (p *pizza) cut() {
fmt.Println("피자 자르는중 .........")
}
func (p *pizza) box() {
fmt.Printf("[%s] 박스에 넣기 완료\n", p.name)
}
type Cheese struct {
pizza
}
type Hawaiian struct {
pizza
}
type Margherita struct {
pizza
}
type Ham struct {
pizza
}
type FactoryPizza struct {
p Pizza
}
func (f *FactoryPizza) createPizza(t string) Pizza {
switch t {
case "Cheese":
// 치즈 피자가 p 가 된다.
f.p = &Cheese{pizza{"치즈 피자"}}
case "Hawaiian":
// 하와이안 피자가 p 가 된다
f.p = &Cheese{pizza{"하와이안 피자"}}
case "Margherita":
// 마르게리타 피자가 p 가 된다.
f.p = &Cheese{pizza{"마르게리타 피자"}}
default:
// 디폴트 피자 는 햄 피자가 p 가 된다.
f.p = &Cheese{pizza{"햄 피자"}}
}
return f.p
}
조금 많은 과정이 들어갔지만 하나씩 차근차근 살펴보자.
피자에 해당하는 구조체 들을 각각 선언했으며, 피자 인터페이스 와 중간 구현체 그리고 상속을 받는 구조이다.
이렇게 함에 따라 최하위 구현체들에게 생성의 책임을 위임할 수 있으며 중간구현체 가 있음으로 공통되는 부분을 제거해서
사용할 수 있다.
이에 대해 클라이언트 코드 도 수정해 보자.
type PizzaStore struct {
factory FactoryPizza
}
func (ps *PizzaStore) orderPizza(pizza string) Pizza {
p := ps.factory.createPizza(pizza)
p.prepare()
p.bake()
p.cut()
p.box()
return p
}
팩토리를 피자 스토어에 던져주고 그걸 이용해 피자만 생성해 주면 된다.
이 코드를 보면 희한한 감이 생긴다 그냥 스위치 부분을 아래 구조체에 그냥 넘긴 게 아닌가? 뭐가 다른지 모를 수도 있다.
만약 이 피자
팩토리를 사용하는 곳이 정말 많고 생성된 피자의 객체를 받아와서 설명하고, 가격을 알려주는 함수가 추가적으로 있다면 하위클래스 모든 곳에서 이에 해당하는 함수를 전부 구현해야 합니다.
가게를 확장해 보자. 새로운 지점과 서로 다른 피자를 제공하는 경우가 생긴다면?
단순하게 생각해 본다면 factoryzPizza를 여러 개 만들면 되지 않을까? 바로 가보자.
메인 타입
type PizzaStore interface {
orderPizza(name string) Pizza //최상위 인터페이스 강제할 함수
}
type PizzaProducer interface {
createPizza(name string) Pizza //인터페이스 구현체 에서 사용될 함수 를 강제할 부분
}
type pizzaStore struct {
p Pizza
pp PizzaProducer // 최초 구현체 생성시 인자를 주입받기 위해 생성
}
func (p *pizzaStore) orderPizza(name string) Pizza {
p.p = p.pp.createPizza(name)
p.p.prepare()
p.p.bake()
p.p.cut()
p.p.box()
return p.p
}
하위 구현체
type NyPizza struct {
pizzaStore
}
func (n *NyPizza) createPizza(t string) Pizza {
switch t {
case "Cheese":
// 치즈 피자가 p 가 된다.
n.p = &Cheese{pizza{"뉴욕 스타일 치즈 피자"}}
case "Hawaiian":
// 하와이안 피자가 p 가 된다
n.p = &Cheese{pizza{"뉴욕 스타일 하와이안 피자"}}
case "Margherita":
// 마르게리타 피자가 p 가 된다.
n.p = &Cheese{pizza{"뉴욕 스타일 마르게리타 피자"}}
default:
// 디폴트 피자 는 햄 피자가 p 가 된다.
n.p = &Cheese{pizza{"뉴욕 스타일 햄 피자"}}
}
return n.p
}
type LAPizza struct {
pizzaStore
}
func (n *LAPizza) createPizza(t string) Pizza {
switch t {
case "Cheese":
// 치즈 피자가 p 가 된다.
n.p = &Cheese{pizza{"LA 스타일 치즈 피자"}}
case "Hawaiian":
// 하와이안 피자가 p 가 된다
n.p = &Cheese{pizza{"LA 스타일 하와이안 피자"}}
case "Margherita":
// 마르게리타 피자가 p 가 된다.
n.p = &Cheese{pizza{"LA 스타일 마르게리타 피자"}}
default:
// 디폴트 피자 는 햄 피자가 p 가 된다.
n.p = &Cheese{pizza{"LA 스타일 햄 피자"}}
}
return n.p
}
최종 클라이언트 코드
func main() {
a := &pizzaStore{pp: &NyPizza{}} // 피자 의 프로듀서 를 각각 주입 해준다.
b := &pizzaStore{pp: &LAPizza{}}
a.orderPizza("Cheese")
fmt.Println()
b.orderPizza("Hawaiian")
}
이렇게 특정 피자 가게 의 주입을 넣어줄 수 있으면서 피자 생성이 가능해진다. 이렇게 생산된 피자의 제조 방법 은 모두 캡슐화되어 관리된다. 그림으로 보면 아래와 같다.
이렇게 병렬 적으로 구성된 생산자, 제품 클래스는 무한히 확장 가능한 형태가 된다.
"팩토리 메서드 패턴은 객체를 생성할 때 피룡한 인터페이스를 만듭니다."
"어떤 클래스의 인스턴스를 만들지는 서브 클래스에서 결정합니다. "
크리에이터에 해당하는 부분은 실질적인 제품의 생산이 아닌 어떤 방식으로 만들어야 하는지에 대한 방식만 결정하고 있다.
이에 반해 제품에서는 실질적은 인스턴스의 반환 이 일어나고 있다.
다시 말해 사용하는 서브클래스에 따라 반환되는 객체 인스턴스가 결정된다.
마지막 클라이언트 호출부에서 고민이 생긴다.
함수형태로 반환해서 이름을 주입하는 방법 도 하나의 좋은 방법이 될 거 같아 공유하고자 한다.
type PizzaStore interface {
orderPizza(p PizzaProducer) func(name string) Pizza
}
func main() {
// 호출 /....
var ps PizzaStore
pa.orderPizza(&NyPizza{})("cheese")
a := &pizzaStore{pp: &NyPizza{}}
a.orderPizza("cheese")
}
구현의 방식은 중요하지 않다. 중요한 부분은 생성하는 서브 클래스 의 생성자 주입이고 그거의 형태를 최초 생성 시 넣어주느냐
아니면 함수 호출 과정에 넣어주느냐의 차이이다.
단 이런 추상 팩토리 패턴은 가끔씩 안티패턴으로 불리기도 한다. 왜?
- 과도한 추상화: 추상화는 객체 생성에 대한 복잡성을 감추기 위한 목적으로 사용되어야 합니다. 그러나 과도한 추상화는 코드를 이해하기 어렵게 만들고 유지보수를 어렵게 만듭니다.
- 복잡성 증가: 팩토리 메서드 패턴은 객체 생성을 처리하는 인터페이스를 정의하기 때문에, 팩토리 클래스와 그 클래스의 서브 클래스 사이의 상호작용이 복잡해질 수 있습니다.
- 새로운 타입 추가의 어려움: 팩토리 메소드 패턴을 사용하면 새로운 객체 타입을 추가하는 것이 상대적으로 어려워집니다. 새로운 타입을 추가하려면 팩토리 클래스와 그 클래스의 서브 클래스를 수정해야 합니다.
- 성능 저하: 팩토리 메소드 패턴은 객체 생성 시간이 길어지는 경우 성능에 영향을 미칠 수 있습니다. 이는 팩토리 클래스가 객체를 생성하는 과정에서 오버헤드가 발생하기 때문입니다.
1번 관점에서 이유없는 추상화가 발생한다면 이런경우에 해당한다. 추상화 된 인터페이스에 대해 아무 행동이 없다면 ? 굳이 추상화를 거칠 필요가 없다. 다시말해 그냥 일반적으로 구현하는게 보다 직관적 이라는 소리 이다.
2번 은 구현하면서 상당히 많이 느꼇다. 실제적으로 인터페이스 팩토리 클래스 사이 에 어떤 연관 을 규정하고 구현 해야할지 상당히 애를 먹었다.
3번 의 관점에서 본다면 위에 구현된 예제는 안티패턴이 되어버린다. 왜? OCP를 어기고 있다. 새로운 타입 이 추가되면 기존코드를 고쳐야한다. 예를들어 Switch 문에서 새로운 뉴욕스타일의 피자가 추가된다면 ? 기존 코드를 고쳐야 한다. 이는 OCP 를 어기고 있다.
그럼에도 팩토리 메서드 패턴이 사용되는 이유로는 기존 클래스 들과 의 연관관계 가 상당히 적어진다.
즉 개별적으로 동작한다는 소리이다.
새로운 피자가 추가된다고 해서 기존 클래스에 어떤 연관관계 가 있는지 생각해 본다면 보다 명확해지지 않을까 싶다.
예를 들어 기존 뉴욕 치즈 피자에 새로운 스트링이 추가된다면 ? 그 부분만 수정한다고 해서 LA 피자가 바뀌는가 ? 라고 생각해보면 쉽게 답할수 있다.
4번 예를 들어 db 의 커넥션 팩토리 메소드 가 있다고 생각해보자. 그 커녁센 이 무조건 있어야 실행되는 어플리케이션 이라면 ? 그 커넥션 을 위해 계속적인 리트라이가 일어나게 된다면 ? 성능의 저하가 발생될수도 있다.
다시 처음에 설명했던 생성패턴 은 2가지 콘셉트에 대해 살펴보자.
1. 하나는 시스템 이 사용하는 구체적인 클래스에 대해 캡슐화하는 것이고,
: orderPizza 안에 creaetPizza를 숨겨 클라이언트는 피자의 생성 방식을 알지 못한다.
2. 다른 하나는 이런 구체적인 클래스의 인스턴스 생성 결합되는 방식을 숨기는 것이다.
: 각각의 하위 구현체 들은 피자의 생성된다.
이로 인해 얻는 장점은?
객체의 생성을 제어하고, 단순화한다.
단순 한 줄이면 우리는 원하는 객체를 얻을 수가 있으며 심지어 다양한 객체를 생성할 수도 있는 힘이 생긴다.
그렇게 생성된 객체에 대해 우리는 특정 행동을 명시하고 제어할 수 있는 제어권 이 생긴다.
또한 생성패턴이 적용되어. 캡슐화되어 있기 때문에 단위 테스트 하는 데 있어 용이해진다.
보이는가? 정말 낮은 단계에서부터 상위까지 이렇게 정말 쉽게 모든 생성자에 대해 손쉽게 생성할 수 있다.
팩토리 메서드 패턴의 장점이 여기서 드러난다.
이렇게 생성된 객체 들에 대해 추가적인 행동에 대해 검증하면 된다.
정말 손쉽고 작은 단위로 모든 객체에 대해 이렇게 테스트가 쉽다는 것은 정말 큰 장점이라고 생각한다.
패턴은 너무 어렵다 ....
출처
'Go > Design Pattern' 카테고리의 다른 글
[Design Pattern] 행동패턴-전략패턴 (Strategy Pattern) (0) | 2023.07.02 |
---|---|
[Design Pattern] 생성패턴 (Singleton Pattern) (0) | 2023.05.10 |
[Design Pattern] 생성패턴 (Prototype Pattern) (0) | 2023.05.08 |
[Design Pattern] 생성패턴 (Builder Pattern) (0) | 2023.04.10 |
[Design Pattern] 생성패턴 (AbstractFactory Method) (0) | 2023.03.23 |