브리지 패턴
개요
일반 알림과 긴급 알림을 문자와 메일로 보낸다고 하자. 조합마다 클래스를 만들면 일반문자·긴급문자·일반메일·긴급메일이 생기고, 채널을 추가할 때마다 알림 종류만큼 구현이 늘어난다.
여기서 채널은 문자·메일처럼 메시지를 전달하는 경로다. 긴급 표시를 붙일지는 알림의 규칙이고, 그 문장을 어떻게 보낼지는 채널의 책임이다. 브리지(Bridge)는 이렇게 따로 바뀌는 두 역할을 나누고, 알림 객체가 채널 객체를 참조하도록 연결한다.
UML
Notice는 IChannel을 참조한다. NormalNotice와 UrgentNotice는 알림 쪽을, SmsChannel과 MailChannel은 전송 쪽을 확장한다. 연결은 Notice의 생성자에서 정한다.
역사
GoF는 1994년 Design Patterns에서 Bridge를 구조 패턴으로 정리했다. 추상화와 구현을 분리해 각각 확장할 수 있게 하는 구조다.1
왜 사용하는가?
긴급 문구를 바꾸려고 문자·메일 구현을 모두 수정하거나, 푸시 알림을 추가하려고 일반·긴급 규칙을 다시 쓰고 싶지는 않다. Notice는 채널에 메시지를 건네는 공통 약속인 IChannel만 사용하므로, 알림 규칙과 전달 방식을 각각 한쪽에서 바꿀 수 있다.
어댑터가 이미 다른 약속을 가진 대상을 맞추는 데 초점을 둔다면, 여기서는 처음부터 두 축을 독립적으로 설계한다. 둘 다 객체를 감쌀 수 있으므로 모양보다 해결하려는 문제로 구별한다.
예제
일반 알림을 문자 채널에 연결한다. 전송 클래스는 실제 통신 대신 결과 문자열을 반환한다.
using System;
using System.Collections.Generic;
using System.Linq;
Console.WriteLine(new NormalNotice(new SmsChannel()).Send("ready"));
// 알림 쪽이 문자·메일의 구체 API를 몰라도 메시지를 건넬 수 있는 약속이다.
interface IChannel { string Deliver(string message); }
// 실제 전송 대신 어느 채널을 골랐는지 문자열로 보여 준다.
sealed class SmsChannel : IChannel
{
public string Deliver(string message) => "SMS: " + message;
}
sealed class MailChannel : IChannel
{
public string Deliver(string message) => "MAIL: " + message;
}
abstract class Notice(IChannel channel)
{
// 생성자로 받은 채널을 보관한다. 알림 종류와 전달 방식을 여기서 연결한다.
protected IChannel Channel { get; } = channel;
public abstract string Send(string message);
}
sealed class NormalNotice(IChannel channel) : Notice(channel)
{
public override string Send(string message) => Channel.Deliver(message);
}
sealed class UrgentNotice(IChannel channel) : Notice(channel)
{
// 긴급 문구는 알림 규칙이므로 채널마다 복사하지 않고 이곳에서 붙인다.
public override string Send(string message) => Channel.Deliver("URGENT " + message);
}
SMS: readynew NormalNotice(new SmsChannel())에서 바깥 객체는 알림 종류를, 안쪽 객체는 전달 경로를 정한다. NormalNotice는 문자 API를 직접 부르지 않고 IChannel.Deliver()에 메시지를 넘긴다.
범위 넓힌 예제
알림 두 종류와 채널 두 종류를 조합한다. 긴급 표시를 붙이는 규칙은 UrgentNotice 한곳에 있다.
using System;
using System.Collections.Generic;
using System.Linq;
// 채널만 교체하면서 같은 두 알림 종류를 재사용한다.
foreach (IChannel channel in new IChannel[] { new SmsChannel(), new MailChannel() })
{
Console.WriteLine(new NormalNotice(channel).Send("ready"));
Console.WriteLine(new UrgentNotice(channel).Send("ready"));
}
// 알림 쪽이 문자·메일의 구체 API를 몰라도 메시지를 건넬 수 있는 약속이다.
interface IChannel { string Deliver(string message); }
// 실제 전송 대신 어느 채널을 골랐는지 문자열로 보여 준다.
sealed class SmsChannel : IChannel
{
public string Deliver(string message) => "SMS: " + message;
}
sealed class MailChannel : IChannel
{
public string Deliver(string message) => "MAIL: " + message;
}
abstract class Notice(IChannel channel)
{
// 생성자로 받은 채널을 보관한다. 알림 종류와 전달 방식을 여기서 연결한다.
protected IChannel Channel { get; } = channel;
public abstract string Send(string message);
}
sealed class NormalNotice(IChannel channel) : Notice(channel)
{
public override string Send(string message) => Channel.Deliver(message);
}
sealed class UrgentNotice(IChannel channel) : Notice(channel)
{
// 긴급 문구는 알림 규칙이므로 채널마다 복사하지 않고 이곳에서 붙인다.
public override string Send(string message) => Channel.Deliver("URGENT " + message);
}
SMS: ready
SMS: URGENT ready
MAIL: ready
MAIL: URGENT ready네 조합을 표현하기 위해 네 전용 클래스를 만들지 않았다. 실제 전송에는 실패·취소·재시도 계약이 추가로 필요하며, 이 대역은 외부 메시지를 보내지 않는다.
TypeScript 예제
TypeScript에서도 같은 두 역할을 나눈다. 위 UML의 Send·Deliver는 아래 코드의 send·deliver에 대응하며, 생성자로 채널을 전달하는 구조는 같다.
// 알림 쪽이 문자·메일의 구체 API를 몰라도 메시지를 건넬 수 있는 약속이다.
interface IChannel {
deliver(message: string): string;
}
// 실제 전송 대신 어느 채널을 골랐는지 문자열로 보여 준다.
class SmsChannel implements IChannel {
deliver(message: string): string { return "SMS: " + message; }
}
class MailChannel implements IChannel {
deliver(message: string): string { return "MAIL: " + message; }
}
abstract class Notice {
protected readonly channel: IChannel;
constructor(channel: IChannel) {
// 생성자로 받은 채널을 보관한다. 알림 종류와 전달 방식을 여기서 연결한다.
this.channel = channel;
}
abstract send(message: string): string;
}
class NormalNotice extends Notice {
override send(message: string): string {
return this.channel.deliver(message);
}
}
class UrgentNotice extends Notice {
override send(message: string): string {
// 긴급 문구는 알림 규칙이므로 채널마다 복사하지 않고 이곳에서 붙인다.
return this.channel.deliver("URGENT " + message);
}
}
// 채널만 교체하면서 같은 두 알림 종류를 재사용한다.
for (const channel of [new SmsChannel(), new MailChannel()]) {
console.log(new NormalNotice(channel).send("ready"));
console.log(new UrgentNotice(channel).send("ready"));
}SMS: ready
SMS: URGENT ready
MAIL: ready
MAIL: URGENT ready새 채널이 deliver()를 구현하면 NormalNotice와 UrgentNotice에 그대로 연결할 수 있다. 반대로 알림 종류를 추가해도 기존 채널을 수정할 필요가 없다.
같이 보기
C# 예제는 각각 독립된 .NET 10 콘솔 프로그램이며, TypeScript 예제는 컴파일한 JavaScript를 Node.js에서 실행할 수 있다.