트렌드브리프
브리지 패턴: 추상과 구현을 나눠 클래스 폭발 막기

브리지 패턴: 추상과 구현을 나눠 클래스 폭발 막기

· 게시 · 약 4분 분량

브리지 패턴은 추상(무엇을 하는가)과 구현(어떻게 하는가)을 별도 계층으로 나눠 상속 대신 합성으로 잇는 GoF 구조 패턴이다. 두 축이 각각 늘 때 서브클래스가 곱으로 폭발하는 것을 막는 원리와, 자주 헷갈리는 어댑터 패턴과의 차이를 예제로 정리했다.

목차
  1. 상속만 쓰면 왜 클래스가 폭발하나
  2. 코드로 보는 최소 구조
  3. 어댑터 패턴과 무엇이 다른가
  4. 축이 하나뿐이면 브리지는 과하다

브리지 패턴은 '무엇을 하는가'를 담은 추상 계층과 '어떻게 하는가'를 담은 구현 계층을 서로 다른 클래스 계층으로 갈라놓고, 둘을 상속이 아니라 합성으로 잇는 구조 패턴이다. 알림 종류와 발송 채널처럼 각각 독립적으로 늘어나는 두 축을 상속 트리 하나에 욱여넣으면 클래스 수가 곱셈으로 불어나는데, 브리지 패턴은 이를 덧셈으로 되돌린다. GoF의 『디자인 패턴』이 밝힌 의도는 "추상을 구현에서 분리해 둘이 독립적으로 변할 수 있게 한다"이다. 이 글은 언어에 매이지 않는 개념과 최소 구조, 그리고 자주 헷갈리는 어댑터 패턴과의 차이만 다룬다. 여러 패턴을 엮어 쓰는 조합 설계는 범위 밖이다.

상속만 쓰면 왜 클래스가 폭발하나

독립된 두 축을 상속 하나로 표현하면 조합마다 클래스를 새로 만들어야 하기 때문이다. 알림 종류가 일반·긴급 2가지, 발송 채널이 SMS·이메일·슬랙 3가지라고 하자. 상속으로 풀면 일반SMS알림, 긴급슬랙알림처럼 2×3=6개의 클래스가 필요하다. 여기에 푸시 채널이 하나 붙으면 2×4=8개, 알림 종류가 하나 더 생기면 3×4=12개로, 축이 커질수록 곱으로 불어난다.

브리지 패턴은 이 두 축을 아예 갈라놓는다. 알림 종류를 추상 계층에, 발송 채널을 구현 계층에 두고, 알림이 채널을 필드로 들고 있게(합성) 만든다. 그러면 종류 2개와 채널 4개를 더한 6개 클래스로 끝난다. 새 채널은 채널 계층에만, 새 종류는 종류 계층에만 추가하면 되니 M×N이 M+N으로 바뀐다. 이 덧셈 구조가 브리지 패턴이 주는 가장 큰 이득이다.

코드로 보는 최소 구조

핵심은 추상 쪽이 구현을 상속받지 않고 참조로 들고 있다는 점이다. 아래는 스위프트로 쓴 최소 골격이다. 구현 계층인 MessageChannel은 발송 방법만 정의하고, 추상 계층인 Notification은 채널을 프로퍼티로 주입받아 일을 넘긴다.

// 구현 계층: 발송 채널
protocol MessageChannel {
 func send(_ text: String)
}

struct SMSChannel: MessageChannel {
 func send(_ text: String) { /* SMS 발송 */ }
}

struct SlackChannel: MessageChannel {
 func send(_ text: String) { /* 슬랙 발송 */ }
}

// 추상 계층: 알림 종류 — 채널을 참조로 소유(합성)
class Notification {
 let channel: MessageChannel
 init(channel: MessageChannel) { self.channel = channel }
 func notify(_ text: String) { channel.send(text) }
}

class UrgentNotification: Notification {
 override func notify(_ text: String) {
  channel.send("[긴급] " + text)
 }
}

// 조합은 생성 시점에 결정
let alert = UrgentNotification(channel: SlackChannel())
alert.notify("서버 응답 없음")

긴급 알림은 notify만 재정의해 접두어를 붙일 뿐, 뒤에 있는 채널이 SMS인지 슬랙인지는 전혀 모른다. 채널을 교체하려면 생성할 때 다른 구현체를 넘기면 되고, 새 채널을 추가해도 Notification 계층은 한 줄도 바뀌지 않는다. 추상과 구현이 각자의 속도로 자라게 두는 것이 이 구조의 목적이다.

왼쪽 추상(모양) 열과 오른쪽 구현(채움) 열을 다리 하나가 잇고, 구석에 조합마다 클래스를 만든 격자가 지워진 선화 일러스트
추상(모양)과 구현(채움)이 각자 변하고 다리 하나로 만난다 — 조합마다 클래스를 만들지 않는다.

어댑터 패턴과 무엇이 다른가

의도가 다르다. 어댑터는 이미 존재하고 고칠 수 없는 클래스의 인터페이스를 다른 형태에 맞추려고 사후에 끼워 넣는 패턴이고, 브리지는 설계 단계에서 처음부터 추상과 구현을 갈라두는 패턴이다. 구조가 비슷해 보여도 어댑터는 맞지 않는 두 인터페이스를 이어 붙이는 사후 대응이고, 브리지는 앞으로 각자 자랄 두 축을 미리 나누는 선제 설계다.

그래서 선택 기준은 시점이다. 남이 만든 라이브러리나 레거시를 감싸 쓸 때는 어댑터, 내가 설계하는 도메인에서 축이 둘 이상 독립적으로 늘어날 조짐이 보일 때는 브리지를 고른다. 두 패턴의 구조 차이는 리팩터링 구루의 브리지 패턴 문서가 그림으로 잘 정리해 두었으니 함께 보면 헷갈림이 준다.

축이 하나뿐이면 브리지는 과하다

변하는 축이 하나뿐이라면 브리지는 오히려 짐이 된다. 추상과 구현을 나누는 순간 계층이 둘로 늘고 호출이 한 단계를 더 거치기 때문에, 조합이 늘 일이 없는 곳에 미리 넣으면 읽기만 어려워진다. 실무에서는 지금 구현이 하나여도 곧 두세 가지로 갈릴 것이 분명할 때, 또는 이미 if나 switch로 구현을 분기하는 코드가 여러 곳에 흩어져 있을 때 도입하는 편이 안전하다.

도입 전에 축이 정말 둘로 독립해서 늘어나는지부터 확인한다. 한쪽 축만 변한다면 구현 하나를 갈아 끼우는 전략 패턴이나 단순한 프로토콜 하나로 충분한 경우가 많다. 브리지는 두 축이 모두 자랄 때 값을 하는 도구다.

더 많은 글 보기 RSS 구독