들어가기에 앞서...
필자는 해당 패턴이 디자인 패턴에서 제일 중요하다고-적어도 Go언어에서는-생각한다. 만약 독자가 해당 시리즈를 정주행하고 있다면 한 번 쉬고 오기를 바란다. 아, 해당 패턴이 어렵다는 뜻은 아니니 걱정하지 않아도 된다.
해당 패턴에서는 도형을 그리는 예제를 이용할 것이다. 도형을 그리는 방식으로 Raster 방식과 Vector 방식을 들 수 있는데, Raster 방식은 점을 일일히 찍어서 표현하는 방식이고 Vector 방식은 그래프를 이용해 수학적 방식으로 표현하는 방식이다. 예제에서 구현할 도형은 원과 직사각형 두가지로 한정한다.
모든 경우의 수를 고려한다면 각 경우의 수를 담당하는 클래스를 만드는 방법을 떠올릴 수 있다. RasterRactangle, VectorRactangle, RasterCircle, VectorCircle로 총 4개의 클래스가 나온다. 하지만 이렇게 만들 경우 새로운 도형을 추가할 때마다 해당 도형의 Raster, Vector 클래스를 추가해야 한다. 게다가 새로운 표현 방법을 도입하게 된다면...
가교 패턴
가교 패턴은 구현(Implementor)과 추상(Abstraction)을 분리하는 패턴이다. 이렇게 분리된 Implementor와 Abstraction은 서로 종속되지 않고 독립적으로 개발할 수 있다. 장황하게 서술해 놓았지만, 가교패턴은 객체의 합성 그 자체이다.
장점
- 객체지향적 설계의 핵심이다. 객체지향적 설계의 원칙은 추상 중심의 프로그래밍인데 이를 위해서는 추상과 구현의 분리가 필요하다. 즉, 객체지향적인 설계를 위해서는 가교 패턴의 사용이 필수적이다. 사실 이전과 이후의 예시 거의 모든 곳에 이미 가교 패턴이 녹아들어 있다.
- 병렬적 개발이 가능하다. 가교 패턴을 이용하면 구현부와 추상부가 서로 독립적이기 때문에 따로 개발해도 다른 부분에 영향을 끼치지 않는다.
- 클래스의 수를 줄인다. 카타시안 곱 문제라고도 불리는데, 만약 서로 n개의 객체가 m가지 방법으로 표현되면 모든 객체를 표현하기 위해서 클래스는 n*m개가 필요할 것이다. 하지만 가교패턴을 이용하면 n+m(+1)개의 클래스만으로도 모든 객체를 모든 방법으로 표현할 수 있다.
구현
도형 클래스는 Shape 인터페이스를 상속한다.
package main
import "math"
type Shape interface {
Area() float64
Name() string
}
type circle struct {
radius float64
}
func NewCircle(r float64) Shape {
c := new(circle)
c.radius = r
return c
}
func (c *circle) Area() float64 {
return c.radius*c.radius * math.Pi
}
func (c *circle) Name() string {
return "circle"
}
type rectangle struct {
w, h float64
}
func NewRect(w, h float64) Shape {
r := new(rectangle)
r.w, r.h = w, h
return r
}
func (r *rectangle) Area() float64 {
return r.w * r.h
}
func (r *rectangle) Name() string {
return "rectangle"
}
표현 방식 클래스는 DrawImp 인터페이스를 상속한다.
package main
import "fmt"
type DrawImp interface {
paint(Shape)
}
type rasterImp struct {
}
func NewRasterImp() DrawImp {
return new(rasterImp)
}
func (d *rasterImp) paint(shape Shape) {
fmt.Printf("Draw %s dot by dot taking area of %f\n", shape.Name(), shape.Area())
}
type vectorImp struct {
}
func NewVectorImp() DrawImp {
return new(vectorImp)
}
func (d *vectorImp) paint(shape Shape) {
fmt.Printf("Draw %s mathmatically taking area of %f\n", shape.Name() ,shape.Area())
}
이 둘을 합성한 클래스 drawer는 도형과 표현 방법을 받아 생성되고 표현한다.
package main
type Drawer interface {
DrawShape()
}
type drawer struct {
imp DrawImp
shape Shape
}
func NewDrawer(imp DrawImp, shape Shape) Drawer {
d := new(drawer)
d.imp = imp
d.shape = shape
return d
}
func (d *drawer) DrawShape() {
d.imp.paint(d.shape)
}
만약 새로운 도형이 추가되면 Shape 인터페이스를 상속받는 클래스를, 새로운 표현 방식이 추가되면 DrawImp 인터페이스를 상속받는 클래스를 하나만 만들면 된다.
package main
func main() {
vector := NewVectorImp()
raster := NewRasterImp()
rectangle := NewRect(2.5, 4.0)
circle := NewCircle(3.0)
d := []Drawer{NewDrawer(vector, rectangle), NewDrawer(raster, rectangle), NewDrawer(vector, circle), NewDrawer(raster, circle)}
for _, drawer := range d {
drawer.DrawShape()
}
}
Draw rectangle mathmatically taking area of 10.000000
Draw rectangle dot by dot taking area of 10.000000
Draw circle mathmatically taking area of 28.274334
Draw circle dot by dot taking area of 28.274334
마치며...
가교 패턴은 Go언어를 이용한 객체지향적 설계에 있어서 필수불가결한 디자인 패턴이다. 해당 패턴은 Go언어의 객체지향 방식인 객체의 합성을 패턴화한 것이기 때문이다.
참고 자료
- GoF(에릭 감마, 리처드 헬름, 랄프 존슨, 존 블리시디스), GoF의 디자인 패턴, 프로텍 미디어, 2015
- Dmitri Nesteruk, Design Patterns in Go, Udemy, 2020.8