브릿지 패턴은 abstract object structure 와 implementation object structure 의 계층이 분리되어 coupling 을 약화시키는 방향의 패턴이다. 여기서는 역할 Object 와 구현 Object 의 분리를 말한다기보단, 개념적은 측면으로서 역할 계층과 구현 계층을 확실하게 분리해주는 방향을 말한다.
문제 개요
위와 같은 UML 로직을 만든다고 해보자. 초기 요구사항에 맞춰서 Rectangle 을 그리기 위해서 Rectangle 을 abstract 화 하였고, 어떤 Rectangle 을 그리느냐에 따라 다른 방식으로 그릴 수 있도록 만들어줬다.
public abstract class Rectangle {
public void draw(){ // Rectangle 을 그려줄 Template 제공 (템플렛 패턴)
drawLine(x1,y1, x2,y2);
//...
}
protected abstract void drawLine(double x1, double y1, double x2, double y2);
}
class V1Rectangle extends Rectangle {
protected void drawLine(double x1, double y1, double x2, double y2){
DP1.draw_a_line(x1,x2,y1,y2);
}
}
class V2Rectangle extends Rectangle {
protected void drawLine(double x1, double y1, double x2, double y2){
DP2.draw_b_line(x1,x2,y1,y2);
}
}
이렇게 될 경우 새로운 Rectangle 이 들어와도, 해당 Rectangle 을 그리는 drawLine 만 잘 구현되면 쉽게 추가될 수 있다. 이 때, 언제나 그렇듯이 Circle 을 그려야 하는 요구사항이 추가되었다고 하자. 그럼 언제든지 다른 것을 그릴 수 있는 요구사항들이 들어올 수 있다는 판단이 서고, Shape 라는 더 상위의 객체에 대해 생각해볼 수 있다.
public abstract class Shape {
abstract public void draw();
}
abstract class Circle extends Shape {
public void draw(){
drawCircle(x,y,r);
}
protected abstract void drawCircle(double x, double y, double r);
}
abstract class Rectangle extends Shape {
public void draw(){
drawLine(x1,y1,x2,y2);
// .. draw more Lines
}
protected abstract void drawLine(double x1, double y1, double x2, double y2);
}
위와 같은 Shape 계층이 추가되어 한 번 더 encapsulate 하는 과정이 추가되었음을 확인할 수 있다. Client 에서는 자신이 선택할 Circle 이나 Rectangle 을 가져온 후 draw() 를 호출하면 되기 때문이다.
괜찮아 보이긴 하지만, 앞으로 괜찮을 건지에 대한 문제에서 시작한다. 새로운 Shape 들, 새로운 Version 의 구현체들이 더욱 많이 추가될 것으로 예상된다면 (항상 지금 적용하는 애플리케이션의 입장에서 봐야한다) 거기에 대비한 디자인을 만들어줘야 할 것이다.
뭐 괜찮을거면 이번 포스트 할 필요가 없어진다. 그냥 앞으로 늘어나게 된다면 Class 가 계속 늘어나게 되고, 위에 상황은 꽤나 복잡한 상황이 될거니까 브릿지 패턴에 대한 얘기로 넘어오게 된다.
브릿지 패턴의 등장
지금까지의 문제점을 살펴보면, abstraction (Shape 의 종류) 계층과 implementation (구현되는 Shape 들) 계층의 Coupling 이 지나치게 강하다는 점이다. 이를 해결해보기 위해서 다음 두 가지에 집중해보자
1. Commonality Analysis - 공통적인 영역 파악 (추상화 적용, abstract)
2. Variability Analysis - 변종들이 발생하는 영역 파악 (구현화 적용, concrete)
우선 공통저긴 영역은 [도형] 이라는 영역과, [그린다] 라는 영역이다. 이는 역할체로 추상화시킬 것임을 예상할 수 있다. 각 역할체별로 [도형]의 구현체인 Rectangle, Circle 과, [그린다]의 구현체인 V1Drawing, V2Drawing 따위를 만들 수 있다. 이렇게 기존 모델과 다르게 Drawing 한다는 interface 를 만들어 볼 수 있다.
하지만 어쨌든 [도형을 그려야]하기 때문에 둘은 연결될 수밖에 없다. 둘을 기존 모델처럼 하나의 Class 로 합치는 것보단, Object Composition 관계로 만들어 내는 것을 추천한다. 이 때 다음 두 가지 의존관계에 대한 선택권이 있다.
1. 구현 영역에서 역할영역을 호출하여 사용한다
2. 역할 영역에서 구현영역을 호출하여 사용한다
역할체와 구현체를 나누는 이유는 Client 로 하여금 구현체를 모르게 하여, 구현체의 변동에 있어서 유지 / 보수의 유리함을 가져가기 위함이다. 따라서 1번의 방식은 Client 에게 구현 영역 객체들을 노출시키므로, encapsulation 을 해주지 못한다. 따라서 원래 하던대로 2번 방식을 사용하는 것이 맞다.
단순히 객체관계별로 생각을 해서 역할 / 구현을 정의하는 것보단, 영역을 살펴보는 것이다. [도형을 그린다] 로 부터 시작해 두 개의 역할 영역을 나눠서 위와 같이 분리를 함으로써, [그린다] 라는 알고리즘을 여러 구현체별로 알아서 사용할 수 있도록 하는 전략 패턴과 유사한 방식으로 패턴을 만들 수 있다. 코드를 통해 살펴보자.
public abstract class ShapeV2 {
protected Drawing drawingTool;
public ShapeV2(Drawing drawingTool) {
this.drawingTool = drawingTool;
}
public abstract void draw();
}
public class CircleV2 extends ShapeV2 {
private double x1;
private double y1;
private double r;
public CircleV2(Drawing drawingTool, double x1, double y1, double r) {
super(drawingTool);
this.x1 = x1;
this.y1 = y1;
this.r = r;
}
@Override
public void draw() {
super.drawingTool.drawCircle(this.x1, this.y1, this.r);
}
}
public class RectangleV2 extends ShapeV2{
private double x1;
private double y1;
private double x2;
private double y2;
public RectangleV2(Drawing drawingTool, double x1, double y1, double x2, double y2) {
super(drawingTool);
this.x1 = x1;
this.y1 = y1;
this.x2 = x2;
this.y2 = y2;
}
@Override
public void draw() {
super.drawingTool.drawLine(this.x1, this.y1, this.x2, this.y2);
}
}
위와 같이 abstract object 가 있는 영역을 구현할 수 있다. 역할 영역 객체들이 사용할 구현 영역 interface 를 위에서 정했듯이 Composition 관계를 가지고 있고, 모든 역할 구현체들은 동일한 Interface 만을 사용하면 되기 때문에 부모 클래스에 정의한다 (헷갈리긴 하는데, 역할 영역 / 구현 영역 각각에 역할체 / 구현체가 있는것이다).
public interface Drawing {
// 각 역할체들을 위한 Drawing Tool 제공해준다
// 인터페이스가 많이 바뀌어야 할 듯..?
void drawLine(double x1, double y1, double x2, double y2);
void drawCircle(double x1, double y1, double r);
}
public class V1Drawing implements Drawing {
@Override
public void drawLine(double x1, double y1, double x2, double y2) {
System.out.println("draw line from (x1, y1) to (x2,y2)");
}
@Override
public void drawCircle(double x1, double y1, double r) {
System.out.println("draw circle of radius r from (x1,y1)");
}
}
public class V2Drawing implements Drawing {
@Override
public void drawLine(double x1, double y1, double x2, double y2) {
System.out.println("draw upgraded version of line from (x1, y1) to (x2,y2)");
}
@Override
public void drawCircle(double x1, double y1, double r) {
System.out.println("draw upgraded version of circle of radius r from (x1,y1)");
}
}
이와 같이 구현 영역에 대한 역할 / 구현체들도 제공해줄 수 있다. [역할 영역의 구현체들을 위한 Interface] 를 정의해야 한다는 특성이 있으므로, Interface 가 많이 바뀔 수 있을 것 같다. 다음과 같은 Test 를 해볼 수 있다.
public static void main(String[] args) {
// 역할체가 필요 // 구현체를 주입
// 사용할 Drawing Tool 은 사전에 원하는 방식대로 주입시켜준 상태이다
Drawing drawingTool = new V1Drawing();
Drawing drawingTool2 = new V2Drawing();
// Client 는 역할을 가져온다 (어떤 것을 그릴지 선택)
ShapeV2 shape = new RectangleV2(drawingTool, 1, 1, 2, 2); // V1 Line 을 그린다
ShapeV2 anotherShape = new CircleV2(drawingTool2, 6, 6, 2); // V2 Circle 을 그린다
// 같은 역할체로 묶을 수 있다
List<ShapeV2> shapes = List.of(shape, anotherShape);
for (ShapeV2 s : shapes) {
System.out.println("DRAW EVERY SHAPE");
s.draw();
}
}
-- 실행 결과
DRAW EVERY SHAPE
draw line from (x1, y1) to (x2,y2)
DRAW EVERY SHAPE
draw upgraded version of circle of radius r from (x1,y1)
이처럼 두 영역을 분리해주고 Association 을 통해 (이 관계를 Bridge 라고 한다) Delegation 을 진행해줌으로써 Bridge Pattern 을 구현할 수 있다.
위 UML 에서 알 수 있듯이, 각 영역은 각자 역할체 / 구현체로 구분되어 있기 때문에 새로운 요구사항이 들어와도 각 영역별 구현체를 추가해주면 되기 때문에 변종이 나오더라도 Client 가 사용함에 있어서 아무 변동을 주지 않을 수 있다 (encapsulate 이 잘 됨). 각 영역별 뿐만 아니라, Abstraction 영역이 Implementor 를 Associate 하고 있으므로 자신이 하는 일을 Implementor 에게 delegate 함으로써 Client 로부터 구현 영역을 모두 보호할 수 있다.
해당 패턴은 Adapter Pattern 과 구분시켜줄 필요가 있다 (두 역할 객체들이 Association 관계를 형성하는 측면이 있음). 하지만 늘 그렇듯 패턴의 의도가 다르다. Adapter Pattern 은 유사성이 있는, 다 디자인된 패턴 둘이서 서로 상호 교류가 필요할 때 적용하는 패턴이지만, Bridge Pattern 은 추후 구조적인 문제를 대비하여 미리 보다 큰 범위의 역할 영역과 구현 영역을 나누어서 설계 단계에서 적용하는 패턴이다.
정리하며
Bridge Pattern 은 interface 라는 개념을 Class 간 도입하는 기존 틀에서 벗어나, 전체적인 영역에 적용하여 동작하는 한 시스템을 볼 수 있었다. 가장 중요한 term 은 "abstraction 영역" 과 "implementation 영역"으로, 일반적으로 말하던 역할체 / 구현체보다 크게 묶은 개념임을 기억하자.
Structural 디자인 패턴 영역이 전체적으로 다른 두 영역의 디자인 패턴보다 난이도가 있는 것 같다. 사실 interface 만을 가지고 이렇게 많은 디자인 패턴이 나오다 보니 전체적으로 유사한 느낌이 들긴 하지만, 대부분 디자인 패턴의 [의도]에 있어서 만큼은 큰 차이들이 있는 것 같다. 디자인 패턴은 [의도]와 [영역(structural, behavioral, creational)] 을 같이 기억하면 필요한 요구사항에 적용해볼 수 있지 않을까?
출처
1) 에릭 프리먼, 『Head First Design Patterns』, O'Reilly Media (2004)
2) 중앙대학교 소프트웨어학부 이찬근 교수님 디자인 패턴 수업자료 중
'SW 설계 > Design Pattern' 카테고리의 다른 글
[Design Pattern] Structural - 컴포지트 패턴 (Composite Pattern) (0) | 2023.10.22 |
---|---|
[Design Pattern] Structural - 아답터 패턴 (Adapter Pattern) (0) | 2023.10.22 |
[Design Pattern] Structural - 데코레이터 패턴 (Decorator Pattern) (1) | 2023.10.21 |
[Design Pattern] Creational - 싱글톤 패턴 (Singleton Pattern) (1) | 2023.10.21 |
[Design Pattern] Creational - 빌더 패턴 (Builder Pattern) (0) | 2023.10.21 |