본문 바로가기

SW 설계/Design Pattern

[Design Pattern] Structural - 브릿지 패턴 (Bridge Pattern)

728x90

브릿지 패턴은 abstract object structure 와 implementation object structure 의 계층이 분리되어 coupling 을 약화시키는 방향의 패턴이다. 여기서는 역할 Object 와 구현 Object 의 분리를 말한다기보단, 개념적은 측면으로서 역할 계층과 구현 계층을 확실하게 분리해주는 방향을 말한다. 

 

 

 

문제 개요

 

 

단순한 Interface 적용 예시

 

 

위와 같은 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 라는 더 상위의 객체에 대해 생각해볼 수 있다.

 

 

추가된 Circle 요구사항에 대응하기 위한 Shape Interface 모델링한 모습

 

 

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번 방식을 사용하는 것이 맞다. 

 

 

Bridge 패턴이 적용된 각 영역별 모습

 

 

단순히 객체관계별로 생각을 해서 역할 / 구현을 정의하는 것보단, 영역을 살펴보는 것이다. [도형을 그린다] 로 부터 시작해 두 개의 역할 영역을 나눠서 위와 같이 분리를 함으로써, [그린다] 라는 알고리즘을 여러 구현체별로 알아서 사용할 수 있도록 하는 전략 패턴과 유사한 방식으로 패턴을 만들 수 있다. 코드를 통해 살펴보자. 

 

 

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 을 구현할 수 있다. 

 

 

Bridge Pattern 의 일반적 UML

 

 

위 UML 에서 알 수 있듯이, 각 영역은 각자 역할체 / 구현체로 구분되어 있기 때문에 새로운 요구사항이 들어와도 각 영역별 구현체를 추가해주면 되기 때문에 변종이 나오더라도 Client 가 사용함에 있어서 아무 변동을 주지 않을 수 있다 (encapsulate 이 잘 됨). 각 영역별 뿐만 아니라, Abstraction 영역이 Implementor 를 Associate 하고 있으므로 자신이 하는 일을 Implementor 에게 delegate 함으로써 Client 로부터  구현 영역을 모두 보호할 수 있다. 

 

 

해당 패턴은 Adapter Pattern 과 구분시켜줄 필요가 있다 (두 역할 객체들이 Association 관계를 형성하는 측면이 있음). 하지만 늘 그렇듯 패턴의 의도가 다르다. Adapter Pattern 은 유사성이 있는, 다 디자인된 패턴 둘이서 서로 상호 교류가 필요할 때 적용하는 패턴이지만, Bridge Pattern 은 추후 구조적인 문제를 대비하여 미리 보다 큰 범위의 역할 영역과 구현 영역을 나누어서 설계 단계에서 적용하는 패턴이다. 

 

 

 

정리하며

 

 

Bridge Pattern 은 interface 라는 개념을 Class 간 도입하는 기존 틀에서 벗어나, 전체적인 영역에 적용하여 동작하는 한 시스템을 볼 수 있었다. 가장 중요한 term 은 "abstraction 영역" 과 "implementation 영역"으로, 일반적으로 말하던 역할체 / 구현체보다 크게 묶은 개념임을 기억하자.

 

 

대상에 대한, 행위에 대한 영역을 나누어서 Association 관계를 맺어준다

 

 

Structural 디자인 패턴 영역이 전체적으로 다른 두 영역의 디자인 패턴보다 난이도가 있는 것 같다. 사실 interface 만을 가지고 이렇게 많은 디자인 패턴이 나오다 보니 전체적으로 유사한 느낌이 들긴 하지만, 대부분 디자인 패턴의 [의도]에 있어서 만큼은 큰 차이들이 있는 것 같다. 디자인 패턴은 [의도]와 [영역(structural, behavioral, creational)] 을 같이 기억하면 필요한 요구사항에 적용해볼 수 있지 않을까?

 

 

 

출처 

 

 

1) 에릭 프리먼, Head First Design Patterns』, O'Reilly Media (2004)

 

 

2) 중앙대학교 소프트웨어학부 이찬근 교수님 디자인 패턴 수업자료 중

728x90