본문 바로가기

CS/디자인패턴

[디자인패턴] Bridge 패턴

브릿지 패턴을 설명하는 그림

설명

 구현과 추상을 분리하여 독립적으로 존재하게 만든다. 확장성과 유연성이 좋다.

 추상적 개념을 구체화하는 경우 상속을 이용할 수 있다. 이때 상속의 경우 하면 할수록 클래스가 구체적인 구현 사항에 종속되기 때문에 수정 및 확장이 어려워진다.

상속 기반으로 구현을 처리하는 모습

 위 그림의 Paint를 생각해 보자. Paint는 도형들에 대한 가장 추상적인 인터페이스로, 도형을 화면에 그리는 draw 메서드를 정의한다. 구체적인 도형을 의미하는 Circle, Rectangle은 Paint 인터페이스를 구현함으로써 화면에 표현될 수 있으며, 이를 통해 모든 도형을 draw( )라는 일관된 메서드를 통해 그릴 수 있게 된다.  

여기서 문제점이 되는 부분은 각 도형을 구체적으로 그리는 구현과 밀접하게 관계되는 OpenGL__, DirectX__ 클래스들이다. 해당 클래스들은 OpenGL, DirectX라는 구체적인 API 환경에 종속되며, 구현 사항 역시 해당 환경에 종속된다. 이들은 구체적인 구현 사항과 밀접하게 연관되어 있으므로 확장성과 유연성이 상당히 떨어진다.

 예를 들어 위 상황에서 새로운 도형 Line을 그리는 동작을 추가하고 싶은 경우 Line + OpenGLLine + DirectXLine 클래스를 모두 구현해야 한다. 2개 정도의 도형이라면 문제없을 수도 있지만, 만약 환경이 100개라면 1개의 도형 클래스와 100개의 구현 클래스를 추가해야 한다. 비슷한 맥락으로 새로운 환경 Vulkan을 추가할 때 도형이 100개라면 마찬가지로 100개의 클래스를 추가해야 하는 불상사가 발생한다. 즉, 추상과 구현이 결합함으로써 새로운 추가가 발생할 때마다 전 방면에 지나친 변경이 발생하게 된다.

브릿지 패턴을 적용한 모습. Impl 클래스와 API의 관계는 의존보다 좋은 표현법이 있을것이라고 생각함.

 위 예제에서 Circle, Rectangle과 같은 추상적인 부분은 변하지 않는다. 계속 변하는 부분은 환경 및 구현과 관련되어 있는 OpenGL__, DirectX__ 클래스들 뿐이다. 따라서 변하지 않는 추상적인 Paint 클래스 부분과 구체적인 API 기반 구현 계통을 따로 분리한다면 변화에 따른 파급효과 없이 각 계통을 독립적으로 확장할 수 있게 된다. 추상적인 부분은 집합을 이용하여 구현부를 사용하며, 구현부에서 제공하는 인터페이스를 이용하여 자신들의 기능을 추상적 수준에서 구현하게 된다.

 어댑터 패턴과 브릿지브리지 패턴은 사용하는 관점이 다르다. 어댑터 패턴의 경우 서로 관련 없는 클래스들의 인터페이스를 맞추는 것으로, 이미 각 클래스가 독립적으로 존재하지만 동일한 인터페이스로 처리하고 싶을 때 사용한다. 반면 브리지 패턴은 설계 초기 단계부터 변하지 않는 추상부와 변하는 구현부를 분리하여 추상화 및 구현을 독립적으로 나누기 위해 사용한다.

 활용하는 상황은 다음과 같다. 자세한 사항은 gof책 참고.

  1. 추상적 개념과 구현 사이의 지속적인 종속 관계를 피하고 싶은 경우. ex) 구현을 교체하는 경우가 존재
  2. 추상적 개념과 구현 모두 독립적으로 서브클래싱 기반 확장해야 할 때
  3. 구현을 완벽하게 은닉하기를 원할 때
    : 구현 사항은 Impl 클래스 내부에 정의되며, 외부에는 메서드 인터페이스만 제시되므로 실제 코드를 노출하지 않는다.
  4. 서브 클래스의 급증을 방지하고 싶을 때 ex) Vulkan 구현을 추가해도 추상적 개념은 변하지 않는다.

활용한 결과는 다음과 같다.

  • 인터페이스와 구현을 분리, 런타임에 사용할 구현을 선택할 수 있다. 인터페이스 및 구현 사이에서 발생하는 의존성을 제거하여 두 영역에 대한 컴파일이 별개로 동작한다.
  • 인터페이스와 구현의 독립적 확장이 가능해진다.
  • 세부적인 구현 사항을 사용자에게 숨길 수 있다.

구성 요소

  • Abstraction: 추상적 개념에 대한 인터페이스(서비스 측면)를 제공한다. Impl 객체의 참조자를 관리한다.
  • RefinedAbstraction: 추상적 개념을 확장하는 클래스. Impl 객체에 정의된 기능을 이용하여 자신의 기능을 추상적인 수준에서 구현한다.
  • Implementor: 구현 클래스에 대한 인터페이스(구현 연산 측면)를 제공한다. 여기에 정의되는 인터페이스를 기반으로 RefinedAbstraction 클래스들에서 실제 기능을 구현하게 되므로, 해당 클래스들이 요구하는 기능에 대한 인터페이스들을 정의해야 한다.
  • ConcreteImplementor: Implementor에 제시된 인터페이스들을 각 환경에 맞게 구현하는 클래스로, 실제 구현 사항이 포함된다.

Abstraction과 Implementor의 인터페이스가 유사한 형태를 띠기도 하지만, 둘이 반드시 같을 필요는 없다.

예시 코드

 위에서 제시한 Paint 클래스를 브릿지 패턴으로 표현해 봤다.

abstract class Paint {
    protected _impl: Implementor;
    constructor(impl: Implementor) {
        this._impl = impl;
    }
    set impl(impl: Implementor) {
        this._impl = impl;
    }

    abstract draw(): void;
}

class Circle extends Paint {
    protected _r: number; // 반지름?

    get r() {
        return this._r;
    }
    set r(r: number) {
        this._r = r;
    }

    draw() {
        this._impl.drawCircle();
    }

    constructor(r: number, impl: Implementor) {
        super(impl);
        this._r = r;
    }
}

class Rectangle extends Paint {
    protected _width: number;
    protected _height: number;

    get width() {
        return this.width
    };
    
    set width(_width: number) {
        this._width = _width;
    }

    get height() {
        return this._height;
    }

    set height(_height: number) {
        this._height = _height;
    }

    draw() {
        this._impl.drawRectangle();
    }

    constructor(width: number, height: number, impl: Implementor) {
        super(impl);
        this._width = width;
        this._height = height;
    }
}

class Triangle extends Paint {
    protected _side: number;

    get side() {
        return this._side
    };
    
    set side(_side: number) {
        this._side = _side;
    }

    draw() {
        this._impl.drawTriangle();
    }

    constructor(side: number, impl: Implementor) {
        super(impl);
        this._side = side;
    }
}

Paint의 경우 추상클래스로 선언하고 구현자를 생성자를 통해 받도록 설정했다. 하위 클래스인 Cirle, Rectangle 및 Triangle은 Paint에 정의된 추상 메서드인 draw을 implementor이 제공하는 메서드를 이용하여 구현한다. 실제 구현 사항이 impl로 분리되어, 실제로 어떤 구현 사항을 가지고 있는지는 Paint의 서브 클래스만으로는 알 수 없다.

interface Implementor {
    drawCircle(): void;
    drawRectangle(): void;
    drawTriangle(): void;
}

class TextImpl implements Implementor {
    drawCircle() {
        console.log("Circle");
    }

    drawRectangle() {
        console.log("Rectangle");
    }
    
    drawTriangle() {
        console.log("Triangle");
    }
}

class CharImpl implements Implementor {
    drawCircle() {
        console.log("◎");
    }

    drawRectangle() {
        console.log("■");
    }

    drawTriangle() {
        console.log("▲");
    }
}

 Implementor은 각 도형을 그리기 위한 draw_ 메서드를 정의하고 있으며, 하위 클래스에서 해당 메서드를 각자의 방식으로 구현하고 있다. TextImpl의 경우 텍스트 출력, CharImpl의 경우 도형 모양의 문자 출력을 수행한다.

 

'CS > 디자인패턴' 카테고리의 다른 글

[디자인패턴] Facade 패턴  (0) 2023.04.30
[디자인패턴] SOLID 원칙  (0) 2023.04.27
[디자인패턴] Singleton 패턴  (0) 2023.04.20
[디자인패턴] Adapter 패턴  (1) 2023.04.14
[디자인패턴] Composite 패턴  (0) 2023.04.11