Bridge Pattern

소프트웨어 마에스트로 14기로 활동하면서 전담 멘토님께서 주관하시는 디자인 패턴에 대한 스터디에 참여하게 되었고, 도서 및 스터디를 통해 얻은 지식들을 공유하고자 게시글을 작성하게 되었다.


Bridge Patten?

  bridge란 ‘다리’라는 의미이다. 현실 세계에서 다리가 강 양쪽을 연결하는 역할을 하듯이, Bridge 패턴도 두 장소를 연결하는 역할을 한다. 여기서 장소는 클래스 계층을 의미하는데, 기능의 클래스 계층구현의 클래스 계층이다.

클래스 계층의 두 가지 역할

  클래스 계층(클래스들의 상속 계층)은 클래스들 간의 상속 관계를 의미한다. 클래스 계층은 기능의 클래스 계층구현의 클래스 계층으로 나뉜다. 두 개의 클래스 계층을 설명하기 위해, 그래픽 도형을 다루는 예시를 준비해 보았다.

기능의 클래스 계층

  • 상위클래스: DrawingAPI (그리기 방식을 정의)
  • 하위클래스: PenDrawingAPI, GraphicDrawingAPI 등 (구체적인 그리기 방식을 구현)

  이 계층에서 DrawingAPI 클래스는 그래픽 도형을 어떻게 그릴지를 정의한다. 예를 들어, 도형을 선으로 그리거나 채우는 방식 등을 DrawingAPI 클래스에 정의할 수 있다. 이후 PenDrawingAPI, GraphicDrawingAPI 등의 하위클래스를 만들어 구체적인 그리기 방식을 구현한다. 각 하위클래스는 DrawingAPI 클래스에서 정의된 그리기 방식을 상속받아 사용하며, 구체적인 그리기 방식을 추가로 구현할 수 있다.

  요약하자면, 상위클래스는 기본적인 기능을 가지고 있고, 하위클래스는 새로운 기능을 추가한다. 만약 PenDrawingAPI 클래스에 새로운 기능을 추가하여, PenDrawingAPI 클래스의 하위클래스로 GraphicDrawingAPI 클래스를 만들게 되면 아래와 같이 클래스의 계층이 한층 더 깊어진다.

DrawingAPI
  └──PenDrawingAPI
      └──GraphicDrawingAPI

구현의 클래스 계층

  • 상위클래스: Shape (Abstract 클래스,도형의 기본 특징 정의)
  • 하위클래스: Circle, Rectangle, Triangle 등 (Concrete 클래스, 각 도형의 특성을 추가로 정의)

  이 계층에서 Shape 클래스는 모든 도형이 가지는 공통된 특징을 정의한다. 예를 들면, 모든 도형은 위치를 가지며, 크기를 가질 수 있다. 이런 공통된 특성을 Shape 클래스에 정의하여 코드의 중복을 방지하고, 도형들 간에 공통된 인터페이스를 제공한다. 그리고 이러한 Shape 클래스를 상속받아 구체적인 도형 클래스(하위클래스)인 Circle, Rectangle, Triangle 등을 만들어낸다. 이 하위클래스들은 Shape 클래스에서 정의된 속성과 메서드를 상속받아 사용하며, 각 도형의 특성을 추가로 정의할 수 있다.

  요약하자면, 상위클래스는 추상 메서드로 인터페이스(API)를 규정하고, 하위클래스는 구상 메서드로 그 인터페이스(API)를 구현한다. 만약 상위 클래스인 Shape 클래스의 추상 메서드를 구현한 하위 클래스들이 Circle, Rectangle, Triangle이 있다면 다음과 같이 클래스 계층이 만들어진다.

Shape
  ├──Circle
  ├──Rectangle
  └──Triangle

예제

  Bridge 패턴을 사용한 예제 프로그램을 살펴보자. 여기서 만들 것은 무언가를 표시하기 위한 프로그램이다.

클래스 목록

역할이름설명
기능의 클래스 계층Display‘표시한다’ 클래스
기능의 클래스 계층CountDisplay‘지정 획수만큼 표시한다’ 기능을 추가한 클래스
구현의 클래스 계층DisplayImpl‘표시한다’ 클래스
구현의 클래스 계층StringDisplayImpl‘문자열을 사용해서 표시한다’ 클래스
 Main동작 테스트용 클래스

클래스 다이어그램

classDiagram

class Display {
  impl
  open()
  print()
  close()
  display()
}

class CountDisplay {
  multiDisplay()
}

class DisplayImpl {
  <<abstract>>
  rawOpen()*
  rawPrint()*
  rawClose()*
}

class StringDisplayImpl {
  rawOpen()
  rawPrint()
  rawClose()
}

Display <|-- CountDisplay
DisplayImpl <--o Display
DisplayImpl<|--StringDisplayImpl

Display 클래스

package bridge_pattern;

// Display 클래스 : '기능의 클래스 계층' 최상위에 있는 클래스
public class Display {
 // impl 필드 : Display 클래스의 '구현'을 나타내는 인스턴스, **두 클래스 계층의 다리 역할을 함**
 private DisplayImpl impl;

 // 생성자 : 구현을 나타내는 인스턴스를 인수로 받음
 public Display(DisplayImpl impl) {
  // 인수로 받은 인스턴스를 필드에 대입, 이후 처리에 사용됨
  this.impl = impl;
 }

 // open, print, close 메소드 : Display 클래스에서 제공하는 인터페이스(API)
 // 이 메소드들은 모두 DisplayImpl 클래스의 메소드를 호출함으로써, Display의 인터페이스가 DisplayImpl의 인터페이스로 변환

 // 표시의 전처리
 public void open() {
  impl.rawOpen();
 }

 // 표시 그 자체
 public void print() {
  impl.rawPrint();
 }

 // 표시의 후처리
 public void close() {
  impl.rawClose();
 }

 // display 메서드 : '표시한다'라는 처리를 실현
 public final void display() {
  open();
  print();
  close();
 }
}

CountDisplay 클래스

package bridge_pattern;

// CountDisplay 클래스 : '기능의 클래스 계층'에 속하는 클래스, Display 클래스에 기능을 추가한 클래스
public class CountDisplay extends Display {
 public CountDisplay(DisplayImpl impl) {
  super(impl);
 }

 // CountDisplay 클래스에서 추가된 기능, Display 클래스에서 상속받응ㄴ open, print, close 메서드를 사용하여 multiDisplay라는 새로운 메서드를 구현
 public void multiDisplay(int times) {
  open();
  for (int i = 0; i < times; i++)
   print();
  close();
 }
}

DisplayImpl 클래스

package bridge_pattern;

// DisplayImpl 클래스 : '구현의 클래스 계층'의 최상위에 있는 클래스, 추상클래스, Display 클래스의 open, print, close 메서드에 대응하는 추상메서드를 선언
public abstract class DisplayImpl {

 public abstract void rawOpen();

 public abstract void rawPrint();

 public abstract void rawClose();
}

StringDisplayImpl 클래스

package bridge_pattern;

// StringDisplayImpl 클래스 : '구현의 클래스 계층'에 속하는 클래스, DisplayImpl 클래스의 하위 클래스
// rawOpen, rawPrint, rawClose 메서드를 구현함으로써 문자열을 표시
public class StringDisplayImpl extends DisplayImpl {
 private String string;
 private int width;

 public StringDisplayImpl(String string) {
  this.string = string;
  this.width = string.getBytes().length;
 }

 @Override
 public void rawOpen() {
  printLine();
 }

 @Override
 public void rawPrint() {
  System.out.println("|" + string + "|");
 }

 @Override
 public void rawClose() {
  printLine();
 }

 private void printLine() {
  System.out.print("+");
  for (int i = 0; i < width; i++)
   System.out.print("-");
  System.out.println("+");
 }
}

Main 클래스

package bridge_pattern;

public class Main {
 public static void main(String[] args) {
  // Display 인스턴스 - StringDisplayImpl 인스턴스를 구현 객체로 가짐
  Display d1 = new Display(new StringDisplayImpl("Hello, Korea."));

  // CountDisplay 인스턴스 - StringDisplayImpl 인스턴스를 구현 객체로 가짐
  Display d2 = new CountDisplay(new StringDisplayImpl("Hello, world."));
  CountDisplay d3 = new CountDisplay(new StringDisplayImpl("Hello, universe."));

  // Display 인스턴스의 display 메서드 호출 가능
  d1.display();
  d2.display();
  d3.display();   // CountDisplay 인스턴스의 multiDisplay 메서드 호출 불가능

  // CountDisplay 인스턴스의 multiDisplay 메서드 호출 가능
  d3.multiDisplay(5);
 }
}

실행 결과

image

Bridge 패턴 등장인물

Abstraction(추상화) 역

  기능의 클래스 계층의 최상위 클래스이다. Implementor 역의 메소드를 사용하여 기본 기능만 기술된 클래스이다. 이 인스턴스는 Implemntor 역을 가진다. 예제에서는 Display 클래스이다.

RefinedAbstraction(개선된 추상화) 역

  Abstraction 역에 기능을 추가하였다. 예제에서는 CountDisplay 클래스가 이 역할을 담당하여 multiDisplay 기능이 추가되었다.

Implementor(구현자) 역

  구현의 클래스 계층의 최상위 클래스이다. Abstraction 역의 인터페이스(API)를 구현하기 위한 메서드를 규정하는 역할이다. 예제에서는 DisplayImpl 클래스가 이 역할을 맡아 추상 메서드인 rawOpen, rawPrint, rawClose 메서드를 규정하였다.

ConcreteImplementor(구체적인 구현자) 역

  구체적으로 Implementor 역의 인터페이스(API)를 구현한다. 예제에서는 StringDisplayImpl 클래스가 이 역할을 담당하여 rawOpen, rawPrint, rawClose 메서드를 실제 구현하였다.

Bridge 패턴의 클래스 다이어 그램

classDiagram

class Abstraction {
  impl
  method1()
  method2()
  method3()
}

class RefinedAbstraction {
  refinedMethodA()
  refinedMethodB()
}

class Implementor {
  <<abstract>>
  implMethodX()*
  implMethodY()*
}

class ConcreteImplementor {
  implMethodX()
  implMethodY()
}

Abstraction <|-- RefinedAbstraction
Implementor <--o Abstraction
Implementor <|-- ConcreteImplementor

특징

클래스 계층의 분리

  Bridge 패턴의 특징은 기능의 클래스 계층구현의 클래스 계층을 분리하는 것이다. 이 두 개의 클래스 계층을 분리해둠으로써 각각의 클래스 계층을 독립적으로 확장할 수 있다.
  기능을 추가하고 싶으면 기능의 클래스 계층에서 새로운 클래스를 추가하면 되기 때문에, 구현의 클래스 계층은 전혀 수정할 필요가 없다. 또한 새로 추가한 기능은 ‘모든 구현’에서 사용할 수 있다.

  예시로 이해해보자. 어떤 프로그램은 컴퓨터나 운영체제와 같은 실행 환경에 의존하는 부분이 있어, 실행 환경에 따라 A, B, C 버전으로 프로그램이 나뉠 수 있을 것이다. 이 때 실행 환경에 의존하는 부분을 Bridge 패턴의 구현의 클래스 계층으로 나타내는 것이다.
  즉, 실행 환경에 공통된 인터페이스(API)를 정해서 Implementor 역으로 하고, ConcreteImplementor 역으로 A, B, C 버전 세 개의 클래스를 만들어 구현하도록 한다. 이를 통해 기능의 클래스 계층 쪽에서 아무리 기능을 추가하더라도 세 가지 실행 환경에 동시에 대응할 수 있게 된다.

약한 결합

  상속은 클래스를 확장하는 편리한 방법이지만, 클래스 간의 연결을 강하게 고정시킨다. 아래와 같이 소스 코드를 작성하면, SomethingGood 클래스는 Something 클래스의 하위클래스가 된다.

class SomethingGood extends Something {
  ...
}

  위 관계는 소스 코드를 다시 쓰지 않는 한 바꿀 수 없다. 즉, 매우 강하게 결합된다는 것을 의미한다. 상황에 따라 클래스 간의 관계를 전환하고자 할 대 상속을 사용하는 것은 적절하지 않다. 전환할 때 마다 소스 코드를 변경하고 있을 수는 없기 때문이다. 이럴 때는 상속이 아닌 위임을 사용한다.
  예제에서 Display 클래스 안에서 위임을 사용한다. Display 클래스의 impl 필드에 구현의 인스턴스가 저장되어 있어, 아래와 같이 ‘일을 떠넘기고’ 있다. 이것이 바로 위임이다.

class Display {
 private DisplayImpl impl;

 public Display(DisplayImpl impl) {
  this.impl = impl;
 }

 public void open() {
  impl.rawOpen();
 }

 public void print() {
  impl.rawPrint();
 }

 public void close() {
  impl.rawClose();
 }

  ...
}

  상속은 견고한 연결이지만, 위임은 느슨한 연결이다. Display 클래스의 인스턴스를 만드는 단계에서 인수로 넘어온 것과 연결되기 때문이다. 아래와 같이 예제에서는 Main 클래스에서 Display 및 CountDisplay의 인스턴스를 만들고, 그 때 StringDisplayImpl의 인스턴스를 인수로 넘겼다.

public class Main {
 public static void main(String[] args) {
  Display d1 = new Display(new StringDisplayImpl("Hello, Korea."));
    Display d2 = new CountDisplay(new StringDisplayImpl("Hello, world."));
  CountDisplay d3 = new CountDisplay(new StringDisplayImpl("Hello, universe."));
  }
}

  만약 StringDisplayImpl 클래스 이외에 ConcreteImplementor 역이 있어서 그 인스턴스를 Display나 CountDisplay에 넘겨 주면 그것으로 구현이 확실하게 전환이 된다. 전환할 때 수정한 것은 Main 클래스뿐이고, Display나 DisplayImpl 등의 소스 코드는 전혀 건드릴 필요가 없다.


Reference

  • 유키 히로시, 2022, JAVA 언어로 배우는 디자인 패턴 입문: 쉽게 배우는 GoF의 23가지 디자인 패턴