cv.cm

코딩 · 디자인 패턴

예제로 이해하는 브리지 패턴

6분 분량

핵심 답변

브리지 패턴은 하나의 큰 클래스 계층을 추상(abstraction)과 구현(implementation)이라는 두 개의 별도 계층으로 나누고 합성으로 연결하는 구조 패턴입니다. 추상(예: 보고서의 종류)은 구현 객체(예: 출력 렌더러)에 대한 참조를 가지고 저수준 작업을 거기에 위임합니다. 그러면 양쪽이 독립적으로 늘어날 수 있어, 보고서 3종과 렌더러 3종이 서브클래스 조합 9개가 아니라 6개의 클래스로 해결됩니다.

의도

추상을 구현에서 분리해 두 쪽이 독립적으로 변할 수 있게 합니다. 브리지 패턴은 Gang of Four(GoF) 책의 일곱 가지 구조 패턴 중 하나이며, 책에서는 Handle/Body라고도 부릅니다.

해결하는 문제

경고와 알림이라는 두 종류의 메시지를 이메일과 SMS라는 두 채널로 보내는 앱을 떠올려 보세요. 이를 상속으로 모델링하면 EmailAlert, SmsAlert, EmailReminder, SmsReminder가 생깁니다. 푸시 채널과 요약 메시지를 추가하면 클래스가 아홉 개가 됩니다. 어느 쪽이든 새 종류가 하나 늘 때마다 전체 수가 곱절로 늘어납니다.

진짜 문제는 클래스 계층이 두 가지 독립적인 질문, 즉 메시지가 무엇인지와 어떻게 전달되는지를 섞어 놓았다는 점입니다. 브리지 패턴은 이 둘을 두 계층으로 분리하고 참조로 연결하여, 전체 수가 곱셈이 아니라 덧셈으로 늘어나게 합니다.

설계메시지 2종 × 채널 2종메시지 3종 × 채널 3종메시지 4종 × 채널 4종
조합마다 서브클래스클래스 4개클래스 9개클래스 16개
브리지클래스 4개클래스 6개클래스 8개

구조와 참여자

참여자역할예제에서
Abstraction클라이언트가 사용하는 상위 수준 인터페이스로, Implementor에 대한 참조를 가짐.Message
RefinedAbstraction변형된 동작으로 추상을 확장.Alert, Reminder
Implementor저수준 연산의 인터페이스.Sender
ConcreteImplementor하나의 플랫폼이나 기술에 대한 저수준 연산을 구현.EmailSender, SmsSender

TypeScript 구현

// Implementor: how a message is delivered.
interface Sender {
  deliver(to: string, subject: string, body: string): string;
}

class EmailSender implements Sender {
  deliver(to: string, subject: string, body: string) { return `email ${to} | ${subject} | ${body}`; }
}

class SmsSender implements Sender {
  deliver(to: string, subject: string, body: string) { return `sms ${to} | ${subject}: ${body}`.slice(0, 40); }
}

// Abstraction: what the message is. It holds the bridge to a Sender.
abstract class Message {
  constructor(protected sender: Sender) {}
  abstract send(to: string): string;
}

class Alert extends Message {
  constructor(sender: Sender, private problem: string) { super(sender); }
  send(to: string): string {
    return this.sender.deliver(to, "ALERT", this.problem.toUpperCase());
  }
}

class Reminder extends Message {
  constructor(sender: Sender, private task: string, private due: string) { super(sender); }
  send(to: string): string {
    return this.sender.deliver(to, "Reminder", `${this.task} by ${this.due}`);
  }
}

console.log(new Alert(new SmsSender(), "disk full").send("+100")); // sms +100 | ALERT: DISK FULL
console.log(new Reminder(new EmailSender(), "Pay invoice", "Friday").send("[email protected]"));
// email [email protected] | Reminder | Pay invoice by Friday

PushSender를 추가하는 것은 클래스 하나이며, 곧바로 경고와 알림 모두와 동작합니다. Digest 메시지를 추가하는 것도 클래스 하나이며, 곧바로 모든 sender와 동작합니다. sender는 단지 생성자 인수이므로 사용자 설정에 따라 런타임에 고를 수도 있습니다.

언제 쓰고 언제 쓰지 말아야 하나

다음과 같을 때 브리지 패턴을 사용합니다.

  • 클래스가 둘 이상의 독립적인 차원으로 변하며, 상속을 쓰면 조합마다 클래스가 생길 때.
  • 런타임에 구현을 바꾸고 싶거나 하나의 구현 객체를 여러 추상이 공유하게 하고 싶을 때.
  • 여러 플랫폼(운영체제, 렌더링 백엔드, 데이터베이스 엔진)에서 실행되어야 하는 코드를 만들며 플랫폼 세부 사항을 별도의 계층 하나에 모으고 싶을 때.

다음 경우에는 피하세요.

  • 변화의 차원이 하나뿐일 때. 구현이 여러 개인 단순한 인터페이스면 충분합니다.
  • 구현이 하나뿐인데 "나중을 위해" 추가할 때. 아직 이득이 없는데 추가된 계층 때문에 코드를 읽기만 어려워집니다.

실제 사용 사례

  • Java의 JDBC가 잘 알려진 예입니다. 애플리케이션 코드는 JDBC API를 기준으로 작성되고, 각 데이터베이스 벤더가 저수준 연산을 구현하는 드라이버를 제공합니다. 두 쪽이 독립적으로 발전합니다.
  • 그래픽과 UI 툴킷은 플랫폼에 의존하지 않는 위젯이나 도형 API를 플랫폼별 그리기 백엔드 위에 둡니다. GoF 책의 예제는 Window 추상과 윈도 시스템마다 별도로 있는 WindowImp 구현입니다.
  • 로깅 라이브러리는 코드가 호출하는 로거 API와 파일, 콘솔, 네트워크 서비스에 쓰는 핸들러(어펜더)를 흔히 분리합니다.

관련 패턴

패턴브리지 패턴과의 관계
어댑터 패턴모양은 비슷하지만, 서로 무관하게 설계된 기존 클래스들을 나중에 함께 동작하게 만듭니다. 브리지는 클래스를 작성하기 전에 계획합니다.
전략 패턴이 패턴도 교체 가능한 객체에 위임합니다. 전략은 알고리즘을 바꿔 끼우는 것이고, 브리지는 추상 계층 전체를 구현 계층에서 분리하는 것입니다.
추상 팩토리 패턴브리지에 맞는 구현 객체, 예를 들어 알맞은 플랫폼 백엔드를 만들고 설정할 수 있습니다.
컴포지트 패턴브리지도 상속보다 합성에 기반하지만, 컴포지트는 서로 독립적인 두 계층이 아니라 객체의 트리를 다룹니다.

자주 묻는 질문

브리지 패턴에서 "구현"은 무슨 뜻인가요?

클래스 안의 코드를 뜻하지 않습니다. 추상이 작업을 위임하는, 저수준의 플랫폼 의존적인 일을 하는 별도의 객체 계층을 뜻합니다.

브리지 패턴과 전략 패턴은 무엇이 다른가요?

구조는 거의 같습니다. 전략은 객체 안의 알고리즘 하나를 교체하는 데 초점을 두고, 브리지는 두 계층 전체를 분리해 각각 독립적으로 확장할 수 있게 합니다.

브리지 패턴과 어댑터 패턴은 무엇이 다른가요?

브리지는 양쪽이 변할 수 있도록 미리 설계합니다. 어댑터는 호환되지 않는 기존 클래스를 인터페이스에 맞추려고 나중에 추가합니다.

브리지 패턴은 의존성 주입과 같은가요?

아니요. 브리지는 두 계층을 구조화하는 방법이고, 의존성 주입은 구현 객체를 공급하는 방법입니다. 추상에 implementor를 건네는 자연스러운 방법이기도 합니다.

코딩 튜토리얼 전체