(Kotlin/코틀린) 브리지 패턴(Bridge Pattern) 완전 정리

개요


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 사례 알림×채널, 테마×컴포넌트, 로거×출력 대상
확장 방법 추상화 축 또는 구현 축 중 하나만 추가해도 전체 조합 증가

참고



Related Posts