(Kotlin/코틀린) 브리지 패턴(Bridge Pattern) 완전 정리
개요
- 구조 패턴(Structural Pattern) 중 브리지 패턴(Bridge Pattern) 을 다룹니다.
- 브리지 패턴은 추상화(Abstraction)와 구현(Implementation)을 분리해 각각 독립적으로 확장 할 수 있게 하는 패턴입니다.
- 이 글에서는 다음을 설명합니다.
- 브리지 패턴이 필요한 이유
- 추상화 계층과 구현 계층의 분리
- Kotlin 실전 구현
- Android 실전 예제 (테마·렌더러 분리, 알림 채널, 로거)
1. 왜 브리지 패턴이 필요한가
❌ 상속만으로 두 축을 조합 — 클래스 폭발
// "도형" × "렌더링 방식" 두 축을 상속으로 조합
open class Shape
class Circle : Shape()
class Rectangle : Shape()
// 렌더링 방식이 추가될 때마다 조합이 폭발
class CanvasCircle : Circle()
class CanvasRectangle : Rectangle()
class OpenGLCircle : Circle()
class OpenGLRectangle : Rectangle()
class VulkanCircle : Circle() // 새 렌더러 추가 시
class VulkanRectangle : Rectangle() // 도형 수 × 렌더러 수만큼 클래스 필요
도형 N개 × 렌더러 M개 = N×M 클래스
✅ 브리지로 해결 — 두 축을 독립적으로 확장
// 도형(추상화)과 렌더러(구현)를 분리
// 도형 N개 + 렌더러 M개 = N+M 클래스
val circle = Circle(radius = 50f, renderer = CanvasRenderer())
val rectangle = Rectangle(width = 100f, height = 60f, renderer = OpenGLRenderer())
2. 기본 구조
Abstraction (추상화 계층)
│ has-a
└── Implementor (구현 인터페이스)
├── ConcreteImplementorA
└── ConcreteImplementorB
RefinedAbstraction (추상화 확장)
└── 같은 Implementor 인터페이스 사용
3. 기본 구현 — 도형 × 렌더러
// 구현 인터페이스 (Implementor)
interface Renderer {
fun renderCircle(x: Float, y: Float, radius: Float)
fun renderRectangle(x: Float, y: Float, width: Float, height: Float)
}
// 구체 구현체
class CanvasRenderer : Renderer {
override fun renderCircle(x: Float, y: Float, radius: Float) =
println("[Canvas] 원 그리기: 중심($x,$y) 반지름=$radius")
override fun renderRectangle(x: Float, y: Float, width: Float, height: Float) =
println("[Canvas] 사각형 그리기: ($x,$y) ${width}×$height")
}
class OpenGLRenderer : Renderer {
override fun renderCircle(x: Float, y: Float, radius: Float) =
println("[OpenGL] circle(x=$x, y=$y, r=$radius)")
override fun renderRectangle(x: Float, y: Float, width: Float, height: Float) =
println("[OpenGL] rect(x=$x, y=$y, w=$width, h=$height)")
}
// 새 렌더러 추가 — 도형 코드 수정 없음 ✅
class SVGRenderer : Renderer {
override fun renderCircle(x: Float, y: Float, radius: Float) =
println("""[SVG] <circle cx="$x" cy="$y" r="$radius"/>""")
override fun renderRectangle(x: Float, y: Float, width: Float, height: Float) =
println("""[SVG] <rect x="$x" y="$y" width="$width" height="$height"/>""")
}
// 추상화 계층 — Renderer를 주입받아 사용
abstract class Shape(protected val renderer: Renderer) {
abstract fun draw()
abstract fun resize(factor: Float): Shape
}
// 추상화 확장
class Circle(
private val x: Float = 0f,
private val y: Float = 0f,
private val radius: Float,
renderer: Renderer
) : Shape(renderer) {
override fun draw() = renderer.renderCircle(x, y, radius)
override fun resize(factor: Float) = Circle(x, y, radius * factor, renderer)
}
class Rectangle(
private val x: Float = 0f,
private val y: Float = 0f,
private val width: Float,
private val height: Float,
renderer: Renderer
) : Shape(renderer) {
override fun draw() = renderer.renderRectangle(x, y, width, height)
override fun resize(factor: Float) = Rectangle(x, y, width * factor, height * factor, renderer)
}
// 사용 — 도형과 렌더러 조합이 자유로움
val canvasCircle = Circle(radius = 50f, renderer = CanvasRenderer())
val glRectangle = Rectangle(width = 100f, height = 60f, renderer = OpenGLRenderer())
val svgCircle = Circle(radius = 30f, renderer = SVGRenderer())
canvasCircle.draw() // [Canvas] 원 그리기: 중심(0.0,0.0) 반지름=50.0
glRectangle.draw() // [OpenGL] rect(x=0.0, y=0.0, w=100.0, h=60.0)
svgCircle.draw() // [SVG] <circle cx="0.0" cy="0.0" r="30.0"/>
// 렌더러 교체 — 도형 코드 수정 없음 ✅
val svgRectangle = Rectangle(width = 100f, height = 60f, renderer = SVGRenderer())
svgRectangle.draw() // [SVG] <rect .../>
4. Android 실전 예제 ① — 알림 × 채널 분리
// 구현 인터페이스 — 알림 채널
interface NotificationChannel {
fun send(title: String, body: String, targetId: String)
val channelName: String
}
class PushChannel(private val context: Context) : NotificationChannel {
override val channelName = "PUSH"
override fun send(title: String, body: String, targetId: String) {
// FCM 발송
println("[FCM] to=$targetId | $title: $body")
}
}
class EmailChannel : NotificationChannel {
override val channelName = "EMAIL"
override fun send(title: String, body: String, targetId: String) {
println("[EMAIL] to=$targetId | $title: $body")
}
}
class SmsChannel : NotificationChannel {
override val channelName = "SMS"
override fun send(title: String, body: String, targetId: String) {
println("[SMS] to=$targetId | $title: $body")
}
}
// 추상화 계층 — 알림 종류
abstract class AppNotification(protected val channel: NotificationChannel) {
abstract fun notify(targetId: String)
}
// 추상화 확장 — 주문 알림
class OrderNotification(
private val orderId: String,
private val status: String,
channel: NotificationChannel
) : AppNotification(channel) {
override fun notify(targetId: String) =
channel.send(
title = "주문 상태 업데이트",
body = "주문 #$orderId: $status",
targetId = targetId
)
}
// 추상화 확장 — 마케팅 알림
class MarketingNotification(
private val campaignId: String,
private val message: String,
channel: NotificationChannel
) : AppNotification(channel) {
override fun notify(targetId: String) =
channel.send(
title = "특별 혜택 안내",
body = message,
targetId = targetId
)
}
// 사용 — 알림 × 채널 자유롭게 조합
val pushOrderNotif = OrderNotification("12345", "배송 중", PushChannel(context))
val emailOrderNotif = OrderNotification("12345", "배송 중", EmailChannel())
val smsMarketing = MarketingNotification("SUMMER_2026", "여름 세일 30% 할인!", SmsChannel())
pushOrderNotif.notify("user_fcm_token")
emailOrderNotif.notify("user@example.com")
smsMarketing.notify("010-1234-5678")
// 새 채널 추가 (카카오) — 기존 알림 코드 수정 없음 ✅
class KakaoChannel : NotificationChannel {
override val channelName = "KAKAO"
override fun send(title: String, body: String, targetId: String) =
println("[KAKAO] to=$targetId | $title: $body")
}
val kakaoOrderNotif = OrderNotification("12345", "배송 완료", KakaoChannel())
kakaoOrderNotif.notify("kakao_user_id")
5. Android 실전 예제 ② — UI 테마 × 컴포넌트
// 구현 인터페이스 — 테마
interface Theme {
val primaryColor: Int
val backgroundColor: Int
val textColor: Int
val cornerRadius: Float
}
class LightTheme : Theme {
override val primaryColor = Color.parseColor("#6200EE")
override val backgroundColor = Color.WHITE
override val textColor = Color.BLACK
override val cornerRadius = 8f
}
class DarkTheme : Theme {
override val primaryColor = Color.parseColor("#BB86FC")
override val backgroundColor = Color.parseColor("#121212")
override val textColor = Color.WHITE
override val cornerRadius = 8f
}
class HighContrastTheme : Theme {
override val primaryColor = Color.YELLOW
override val backgroundColor = Color.BLACK
override val textColor = Color.WHITE
override val cornerRadius = 0f
}
// 추상화 계층 — UI 컴포넌트
abstract class ThemedView(
protected val context: Context,
protected val theme: Theme
) {
abstract fun render(): View
}
// 추상화 확장 — 버튼
class ThemedButton(
context: Context,
theme: Theme,
private val label: String,
private val onClick: () -> Unit
) : ThemedView(context, theme) {
override fun render(): View = MaterialButton(context).apply {
text = label
setBackgroundColor(theme.primaryColor)
setTextColor(theme.textColor)
cornerRadius = theme.cornerRadius.toInt()
setOnClickListener { onClick() }
}
}
// 추상화 확장 — 카드
class ThemedCard(
context: Context,
theme: Theme,
private val content: String
) : ThemedView(context, theme) {
override fun render(): View = MaterialCardView(context).apply {
setCardBackgroundColor(theme.backgroundColor)
radius = theme.cornerRadius
addView(TextView(context).apply {
text = content
setTextColor(theme.textColor)
})
}
}
// 런타임에 테마 교체 가능
val currentTheme: Theme = if (isDarkMode) DarkTheme() else LightTheme()
val loginButton = ThemedButton(context, currentTheme, "로그인") { viewModel.login() }
val profileCard = ThemedCard(context, currentTheme, "홍길동")
binding.container.addView(loginButton.render())
binding.container.addView(profileCard.render())
6. Android 실전 예제 ③ — 로거 × 출력 대상
// 구현 인터페이스 — 로그 출력 대상
interface LogOutput {
fun write(level: String, tag: String, message: String)
}
class ConsoleOutput : LogOutput {
override fun write(level: String, tag: String, message: String) =
Log.println(levelToAndroidLevel(level), tag, message)
private fun levelToAndroidLevel(level: String) = when (level) {
"DEBUG" -> Log.DEBUG
"WARN" -> Log.WARN
"ERROR" -> Log.ERROR
else -> Log.INFO
}
}
class FileOutput(private val file: File) : LogOutput {
override fun write(level: String, tag: String, message: String) {
file.appendText("[$level][$tag] $message\n")
}
}
class RemoteOutput(private val endpoint: String) : LogOutput {
override fun write(level: String, tag: String, message: String) {
// 원격 서버로 로그 전송
println("[Remote→$endpoint] [$level][$tag] $message")
}
}
// 추상화 계층 — 로거
abstract class AppLogger(protected val output: LogOutput) {
abstract val tag: String
fun debug(msg: String) = output.write("DEBUG", tag, msg)
fun warn(msg: String) = output.write("WARN", tag, msg)
fun error(msg: String) = output.write("ERROR", tag, msg)
}
class NetworkLogger(output: LogOutput) : AppLogger(output) {
override val tag = "Network"
}
class DatabaseLogger(output: LogOutput) : AppLogger(output) {
override val tag = "Database"
}
// 조합 — 로거 × 출력 대상 자유롭게 결합
val networkLogger = NetworkLogger(if (BuildConfig.DEBUG) ConsoleOutput() else RemoteOutput("https://logs.example.com"))
val databaseLogger = DatabaseLogger(FileOutput(File(cacheDir, "db.log")))
networkLogger.debug("GET /api/products 요청")
databaseLogger.error("쿼리 실패: SELECT * FROM users")
7. 브리지 vs 전략 패턴
두 패턴은 구조가 유사하지만 목적이 다릅니다.
| 항목 | 브리지 | 전략 |
|---|---|---|
| 목적 | 추상화와 구현을 영구적으로 분리 | 알고리즘을 런타임에 교체 |
| 설계 시점 | 설계 초기에 두 계층을 분리 | 행동 변경이 필요할 때 |
| 관계 | 추상화가 구현을 항상 보유 | 컨텍스트가 전략을 교체 가능 |
8. 정리
| 항목 | 내용 |
|---|---|
| 목적 | 추상화와 구현을 분리해 각각 독립적으로 확장 |
| 문제 해결 | 두 축의 조합에서 발생하는 클래스 폭발 방지 |
| 핵심 구조 | 추상화 클래스가 구현 인터페이스를 합성(has-a)으로 보유 |
| Android 사례 | 알림×채널, 테마×컴포넌트, 로거×출력 대상 |
| 확장 방법 | 추상화 축 또는 구현 축 중 하나만 추가해도 전체 조합 증가 |
- 브리지 패턴은 “두 개의 독립적인 변화 축이 있을 때 이를 분리하는 것” 이 핵심입니다.
- “도형 × 렌더러”, “알림 × 채널”, “로거 × 출력 대상”처럼 두 가지 관점에서 독립적으로 확장해야 할 때 적용하세요.
참고
- Design Patterns — GoF (Gang of Four)
- 어댑터 패턴 포스팅 보기
- 데코레이터 패턴 포스팅 보기
- 파사드 패턴 포스팅 보기