들어가기에 앞서...
이번 글 부터는 구조 패턴에 관해 설명할 것이다. 구조 패턴은 연관된 객체를 합성하는 방법에 관한 디자인 패턴이다. 생성패턴과는 다르게 구조패턴은 서로 연관성이 없는 경우가 많기 때문에 각 글마다 예시가 다를 것이다.
해당 예시는 외부에서 위치를 표현하는 Point 구조체와 글을 표현하는 TextView 인터페이스 및 클래스를 들여왔다고 가정한다. 그리고 우리는 도형을 표현하는 Shape 인터페이스를 작성했다. 우리가 원하는 작업은 위의 TextView를 Shape 처럼 이용하는 것이다. 즉, TextView 클래스를 Shape 인터페이스에 맞추는 것이다.
//point.go
package main
import "fmt"
type Point struct {
X, Y int
}
func NewPoint(x, y int) Point {
return Point{x, y}
}
func (p Point) String() string {
return fmt.Sprintf("(%d, %d)", p.X, p.Y)
}
//shape.go
package main
import (
"fmt"
)
type Shape interface {
BoundingBox() (topLeft, bottomRight Point)
Show()
}
type shape struct {
bottomLeft, topRight Point
}
func NewShape(topLeft, bottomRight Point) Shape {
s := new(shape)
s.bottomLeft, s.topRight = topLeft, bottomRight
return s
}
func (s *shape) BoundingBox() (bottomLeft, topRight Point) {
bottomLeft, topRight = s.bottomLeft, s.topRight
return
}
func (s *shape) Show() {
a, b := s.BoundingBox()
fmt.Println("Rectangle from " + a.String() + " to " + b.String())
}
//textView.go
package main
import "fmt"
type TextView interface {
GetOrigin() (x, y int)
GetExtent() (width, height int)
GetContent() string
Show()
}
type textView struct {
x, y, width, height int
content string
}
func NewTextView(x, y, w, h int, content string) TextView {
t := new(textView)
t.x, t.y, t.width, t.height = x, y, w, h
t.content = content
return t
}
func (t *textView) GetOrigin() (x, y int) {
return t.x, t.y
}
func (t *textView) GetExtent()(width, height int) {
return t.width, t.height
}
func (t *textView) GetContent() string {
return t.content
}
func (t *textView) Show() {
fmt.Println(t.content)
}
적응자 패턴
적응자 패턴은 이미 완성되어 있는 객체(adaptee)가 원하는 인터페이스(target)를 만족하도록 하는 객체(adapter)를 만드는 패턴이다. adapter 객체는 adaptee 객체를 포함하거나 adaptee 객체를 상속받는다. adapter 클래스는 adaptee 객체를 이용해 target 인터페이스를 만족시킨다.
장점
- 이미 완성된 객체를 이용할 경우 코드의 양을 획기적으로 줄인다. 사용자는 새로운 코드를 처음부터 짤 필요없이 이미 개발된 기능을 이용해 구현할 수 있다.
- 객체를 설계하는 입장에서 부담이 줄어든다. 객체를 설계할 때 모든 사용자의 요구를 맞추는 것은 불가능한 일이다. 하지만 적응자 패턴의 존재는 사용자의 요구들로부터 설계를 자유롭게 한다.
단점
- 클래스의 수를 불필요하게 늘일 수 있다. adaptee 객체를 직접 수정할 수 있고 수정에 많은 수고가 들지 않는 경우라면, 차라리 해당 adaptee 객체만 target 인터페이스에 맞춰서 수정하는 편이 나을 수 있다.
구현
우선 기본적인 방법이다.
type textShape struct {
view TextView
}
func NewTextShape(v TextView) Shape {
t := new(textShape)
t.view = v
return t
}
func (t *textShape) Show() {
x, y := t.view.GetOrigin()
w, h := t.view.GetExtent()
fmt.Println("Rectangle from " + NewPoint(x, y).String() + " to " + NewPoint(x + w, y + h).String())
t.view.Show()
}
func (t *textShape) BoundingBox()(topLeft, bottomRight Point) {
x, y := t.view.GetOrigin()
w, h := t.view.GetExtent()
topLeft = NewPoint(x, y)
bottomRight = NewPoint(x + w, y + h)
return
}
새로 추가할 클래스에 멤버변수로 TextView 객체를 넣은 형태이다. 해당 멤버변수는 첫 글자를 소문자로 표기해 외부로부터의 접근을 차단한다. 이후, Shape인터페이스에 맞춰서 메소드들을 추가한다. 이 때, Go언어는 인터패이스의 함수를 포함하기만 하면 해당 인터페이스처럼 쓸 수 있는 duck-taping 방식이기 때문에 특별히 Shape 인터페이스를 명시할 필요는 없다. 이로써 TextShape를 이용해 TextView를 Shape처럼 쓸 수 있다.
다른 경우를 생각해보자. 파워포인트 같은 경우 Shape를 TextView로 만드는 기능이 있다. 즉, TextView의 인터페이스와 Shape의 인터페이스를 동시에 만족하는 클래스가 필요할 수 있다. 이 때 필요한 것이 양방향 적응자이다. 코드는 아래와 같다.
package main
import "fmt"
type TextNShape interface {
TextView
Shape
}
type textNShape struct {
TextView
}
func NewTextNShape(v TextView) TextNShape {
t := new(textNShape)
t.TextView = v
return t
}
func (t *textNShape) Show() {
x, y := t.GetOrigin()
w, h := t.GetExtent()
fmt.Println("Rectangle from " + NewPoint(x, y).String() + " to " + NewPoint(x + w, y + h).String())
t.TextView.Show()
}
func (t *textNShape) BoundingBox()(topLeft, bottomRight Point) {
x, y := t.GetOrigin()
w, h := t.GetExtent()
topLeft = NewPoint(x, y)
bottomRight = NewPoint(x + w, y + h)
return
}
위의 TextShape와 별 차이 없다. Shape와 TextView를 만족하는 인터페이스 TextNShape를 추가하고 TextView를 직접 넣어서 외부에서도 접근 가능하게 하면 된다.
package main
import "fmt"
func Show(s Shape) {
s.Show()
}
func main() {
t := NewTextView(1, 1, 3, 5,"Hello, world!")
s1 := NewShape(NewPoint(1,1), NewPoint(4,6))
s2 := NewTextShape(t)
s3 := NewTextNShape(t)
Show(s1)
Show(s2)
Show(s3)
fmt.Println(s3.GetContent())
}
Rectangle from (1, 1) to (4, 6)
Rectangle from (1, 1) to (4, 6)
Hello, world!
Rectangle from (1, 1) to (4, 6)
Hello, world!
Hello, world!
Process finished with exit code 0
마치며...
어느 정도 프로그래밍을 했다면 외부 라이브러리의 힘을 빌려 일부 기능을 구현해본 경험은 있을 것이다. 적응자 패턴은 이러한 일련의 작업을 패턴화했을 뿐이다. 어렵게 생각하지 말자.
다중 상속을 통해 적응자 패턴을 사용할 때에는 adaptee 객체의 서브 클래스에는 적용이 안되는 부작용이 있다. Go언어는 객체 합성을 이용하므로 다중 상속 문제로부터 자유롭다. 객체지향적으로도 바람직하고 부작용도 없는 만큼 필자는 해당 패턴의 사용을 강력히 추천한다.
참고 자료
- GoF(에릭 감마, 리처드 헬름, 랄프 존슨, 존 블리시디스), GoF의 디자인 패턴, 프로텍 미디어, 2015
- Dmitri Nesteruk, Design Patterns in Go, Udemy, 2020.8