Bridge 패턴: 추상화와 구현을 독립적으로 확장하는 법
Bridge 패턴이 상속 대신 위임으로 추상화와 구현을 분리해 클래스 폭증을 막는 원리를 Java 예제와 함께 정리한다.
2026-08-13 · 최초 발행 2025-10-14
기능이 늘고 구현도 늘 때, 상속은 어디서 무너지는가
같은 기능을 여러 플랫폼에서 지원해야 하는 순간이 오면 상속 하나로는 버티기 어려워진다. 기능 유형이 늘어나는 축과 플랫폼·드라이버·프로토콜이 늘어나는 축이 동시에 움직이면, 상속 기반 계층은 두 축의 곱만큼 클래스를 만들어내야 한다. Bridge 패턴은 이 두 축을 각각 독립된 계층으로 떼어내 조합(Composition)으로 연결한다. 추상화(Abstraction)는 안정적인 API를 유지하고, 구현(Implementation)은 플랫폼별 실제 동작을 담당하며, 둘은 상속이 아니라 위임으로 이어진다.
계층이 나뉘는 방식
Abstraction과 RefinedAbstraction은 클라이언트에 노출되는 안정된 API 계층이다. 구현 세부사항은 은닉하고, RefinedAbstraction이 이를 확장해 도메인 특화 기능을 얹는다.
Implementor와 ConcreteImplementor는 Abstraction이 위임 호출하는 최소 기능 인터페이스와, 플랫폼·드라이버·프로토콜별 실제 동작을 제공하는 구현체다.
둘 사이는 위임으로 연결된다. Abstraction은 생성자나 DI를 통해 Implementor 인스턴스를 주입받아 호출을 넘기고, 이 덕분에 런타임에 구현체를 교체할 수 있다 — 무중단 교체나 다중 조합 테스트가 가능해지는 지점이다.
이렇게 나누면 상속 조합에서 m×n으로 불어나던 클래스 수가 Bridge에서는 m+n으로 줄어든다. 예를 들어 기능 5종, 구현 4종이라면 상속으로는 20개 클래스가 필요하지만 Bridge로는 9개면 된다. 통합 테스트 행렬도 20에서 9로 함께 줄어드니 빌드·리그레션 시간에도 영향을 준다.
물론 공짜는 아니다. 위임 호출이 한 번 더 걸리는 만큼 성능에 민감한 경로에서는 미세한 오버헤드를 감안해야 하고, 설계 초기에 "무엇이 추상화이고 무엇이 구현인가"의 경계를 잘못 잡으면 인터페이스가 과도하게 쪼개질 위험도 있다.
요청이 계층을 오가는 흐름
클라이언트가 Abstraction의 API를 호출하면, Abstraction은 입력을 검증한 뒤 Implementor에 위임하고 필요하면 재시도나 폴백을 수행한다. Implementor에서 오류가 나면 Abstraction 쪽에서 표준 예외로 변환하며, 애초에 Implementor가 주입되지 않은 상태를 막기 위한 생성자 가드도 필요하다.
리모컨 예제로 확인하는 런타임 교체
리모컨(Abstraction)이 TV나 라디오(Implementor) 중 무엇을 다루는지 몰라도 되는 예제다. JDK 17 이상에서 javac BridgeDemo.java && java BridgeDemo로 바로 실행할 수 있다.
// BridgeDemo.java
public class BridgeDemo {
// Implementor
interface Device {
boolean isEnabled();
void enable();
void disable();
int getVolume();
void setVolume(int percent);
String name();
}
static class Tv implements Device {
private boolean on;
private int volume = 30;
public boolean isEnabled() { return on; }
public void enable() { on = true; }
public void disable() { on = false; }
public int getVolume() { return volume; }
public void setVolume(int percent) { volume = Math.max(0, Math.min(100, percent)); }
public String name() { return "TV"; }
}
static class Radio implements Device {
private boolean on;
private int volume = 50;
public boolean isEnabled() { return on; }
public void enable() { on = true; }
public void disable() { on = false; }
public int getVolume() { return volume; }
public void setVolume(int percent) { volume = Math.max(0, Math.min(100, percent)); }
public String name() { return "Radio"; }
}
// Abstraction
static class Remote {
protected final Device device;
Remote(Device device) {
if (device == null) throw new IllegalArgumentException("device null");
this.device = device;
}
void togglePower() {
if (device.isEnabled()) device.disable(); else device.enable();
System.out.println(device.name() + " power=" + device.isEnabled());
}
void volumeUp() {
device.setVolume(device.getVolume() + 10);
System.out.println(device.name() + " volume=" + device.getVolume());
}
void volumeDown() {
device.setVolume(device.getVolume() - 10);
System.out.println(device.name() + " volume=" + device.getVolume());
}
}
// RefinedAbstraction
static class AdvancedRemote extends Remote {
AdvancedRemote(Device device) { super(device); }
void mute() {
device.setVolume(0);
System.out.println(device.name() + " muted");
}
}
public static void main(String[] args) {
Device tv = new Tv();
Remote remote = new AdvancedRemote(tv);
remote.togglePower();
remote.volumeUp();
// 런타임 구현체 교체
Device radio = new Radio();
remote = new AdvancedRemote(radio);
remote.togglePower();
remote.volumeDown();
((AdvancedRemote) remote).mute();
}
}
AdvancedRemote가 Remote를 상속하는 건 RefinedAbstraction 쪽 확장일 뿐이고, Device 구현체(TV·Radio)는 상속 트리와 무관하게 교체된다는 점이 이 예제의 핵심이다.
UI 위젯부터 전송 채널까지, 구현을 갈아 끼우는 자리
- UI 위젯 렌더링 — Button·Dialog 같은 위젯 API(추상화)와 Win32·Cocoa·GTK 렌더러(구현)를 분리해 크로스플랫폼 렌더링 교체를 쉽게 한다
- 스토리지·드라이버 다중화 — KeyValueStore·BlobStore 추상화 아래 S3·GCS·Azure·로컬 FS를 구현으로 붙여 장애 시 폴백이나 멀티라이터 전략을 적용한다
- 전송 채널·프로토콜 — MessageSender 추상화와 HTTP·gRPC·Kafka·AMQP 구현을 나눠 QoS 정책과 전송 매체를 독립적으로 구성한다
- 문서 포맷과 출력 장치 — Document·Report 추상화 하나로 PDF·HTML·프린터·화면 출력을 지원한다
- 결제 수단과 게이트웨이 — PaymentMethod 추상화 아래 Stripe·PayPal·국내 VAN을 구현으로 붙여 리스크 정책과 게이트웨이를 독립적으로 운영한다
상속 확장과 비교하면
| 지표 | 상속 확장 | Bridge 패턴 |
|---|---|---|
| 성능 | 직접 호출로 미세 이점 존재 | 위임 1회 오버헤드 발생(대부분 무시 가능) |
| 확장성 | m×n 클래스 폭증 위험 | m+n로 선형 확장 가능 |
| 일관성 | 구현별 분기 누적, 정책 일관성 저하 가능 | Abstraction에서 정책 일원화 용이 |
| 안정성 | 구현 교체 시 상위 영향 가능 | 구현 교체의 파급 최소화 |
| 운영 편의 | 배포·테스트 행렬 증가 | 구성 변경 중심 운영, 무중단 교체 용이 |
도입할 때 챙길 것들
구현 인터페이스는 최소한으로 유지하고 시그니처를 안정적으로 지키는 편이 낫다. 버전을 올릴 때는 기존 인터페이스를 건드리기보다 확장 인터페이스를 병행 운영하는 쪽이 안전하다. Implementor 주입은 DI나 팩토리 패턴과 묶어 구성 루트에서 관리하고, 예외 변환·재시도·폴백 정책은 Abstraction 레이어 한 곳으로 모으는 게 좋다. 로깅·메트릭·트레이싱 같은 관측성도 이 레이어에 심어두면 구현체가 바뀌어도 관측 지점은 그대로 유지된다.
곱셈적으로 늘어나는 조합 문제가 보이는 영역 — UI, 스토리지, 전송, 결제 게이트웨이 같은 — 부터 Bridge를 단계적으로 적용해보는 편이 리스크가 적다.