(Kotlin/코틀린) 파사드 패턴(Facade Pattern) 완전 정리

개요


1. 왜 파사드 패턴이 필요한가

❌ 서브시스템을 호출부에서 직접 조합

// 동영상 재생 — 복잡한 초기화 과정을 호출부가 직접 처리
class VideoActivity : AppCompatActivity() {
    private lateinit var mediaPlayer: MediaPlayer
    private lateinit var audioManager: AudioManager
    private lateinit var wifiLock: WifiManager.WifiLock
    private lateinit var wakeLock: PowerManager.WakeLock

    fun playVideo(url: String) {
        // 1. AudioFocus 요청
        val audioManager = getSystemService(AUDIO_SERVICE) as AudioManager
        audioManager.requestAudioFocus(null, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN)

        // 2. WakeLock 획득
        val pm = getSystemService(POWER_SERVICE) as PowerManager
        wakeLock = pm.newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK, "app:video")
        wakeLock.acquire()

        // 3. WifiLock 획득
        val wm = getSystemService(WIFI_SERVICE) as WifiManager
        wifiLock = wm.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, "app:video")
        wifiLock.acquire()

        // 4. MediaPlayer 초기화 및 재생
        mediaPlayer = MediaPlayer().apply {
            setDataSource(url)
            prepare()
            start()
        }
        // 호출부가 너무 많은 것을 알아야 함 ❌
    }
}

✅ 파사드로 해결 — 단순한 창구 하나

// 호출부는 단 한 줄
videoPlayerFacade.play(url)
videoPlayerFacade.pause()
videoPlayerFacade.stop()

2. 기본 구조

// 서브시스템 ①
class AudioFocusManager(private val context: Context) {
    fun request(): Boolean {
        val am = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
        val result = am.requestAudioFocus(
            null, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN
        )
        return result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED
    }
    fun release() {
        val am = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
        am.abandonAudioFocus(null)
    }
}

// 서브시스템 ②
class WakeLockManager(private val context: Context) {
    private var wakeLock: PowerManager.WakeLock? = null
    fun acquire() {
        val pm = context.getSystemService(Context.POWER_SERVICE) as PowerManager
        wakeLock = pm.newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK, "app:video").apply { acquire() }
    }
    fun release() { wakeLock?.release(); wakeLock = null }
}

// 서브시스템 ③
class MediaSession(private val context: Context) {
    private var player: MediaPlayer? = null
    fun prepare(url: String) {
        player = MediaPlayer().apply { setDataSource(url); prepare() }
    }
    fun play()  { player?.start() }
    fun pause() { player?.pause() }
    fun stop()  { player?.stop(); player?.release(); player = null }
    val isPlaying get() = player?.isPlaying == true
}

// 파사드 — 세 서브시스템을 감싸 단순한 인터페이스 제공
class VideoPlayerFacade(private val context: Context) {
    private val audioFocus = AudioFocusManager(context)
    private val wakeLock   = WakeLockManager(context)
    private val session    = MediaSession(context)

    fun play(url: String) {
        if (!audioFocus.request()) return  // AudioFocus 실패 시 재생 안 함
        wakeLock.acquire()
        session.prepare(url)
        session.play()
    }

    fun pause() {
        session.pause()
        audioFocus.release()
        wakeLock.release()
    }

    fun stop() {
        session.stop()
        audioFocus.release()
        wakeLock.release()
    }

    val isPlaying get() = session.isPlaying
}
// 호출부 — 서브시스템을 전혀 모름
class VideoActivity : AppCompatActivity() {
    private val playerFacade by lazy { VideoPlayerFacade(this) }

    fun onPlayClicked(url: String) = playerFacade.play(url)
    fun onPauseClicked()           = playerFacade.pause()
    fun onStopClicked()            = playerFacade.stop()

    override fun onDestroy() {
        super.onDestroy()
        playerFacade.stop()
    }
}

3. Android 실전 예제 ① — 알림 파사드

class NotificationFacade(private val context: Context) {

    private val manager = context.getSystemService(Context.NOTIFICATION_SERVICE)
            as NotificationManager

    init { createChannels() }

    private fun createChannels() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            listOf(
                NotificationChannel("ORDER",    "주문 알림",  NotificationManager.IMPORTANCE_HIGH),
                NotificationChannel("CHAT",     "채팅 알림",  NotificationManager.IMPORTANCE_HIGH),
                NotificationChannel("PROMO",    "프로모션",   NotificationManager.IMPORTANCE_LOW)
            ).forEach { manager.createNotificationChannel(it) }
        }
    }

    fun showOrderNotification(orderId: String, status: String) {
        val notification = NotificationCompat.Builder(context, "ORDER")
            .setSmallIcon(R.drawable.ic_order)
            .setContentTitle("주문 상태 업데이트")
            .setContentText("주문 #$orderId — $status")
            .setPriority(NotificationCompat.PRIORITY_HIGH)
            .setAutoCancel(true)
            .build()
        manager.notify(orderId.hashCode(), notification)
    }

    fun showChatNotification(sender: String, message: String) {
        val notification = NotificationCompat.Builder(context, "CHAT")
            .setSmallIcon(R.drawable.ic_chat)
            .setContentTitle(sender)
            .setContentText(message)
            .setPriority(NotificationCompat.PRIORITY_HIGH)
            .setAutoCancel(true)
            .build()
        manager.notify(sender.hashCode(), notification)
    }

    fun showPromoNotification(title: String, body: String) {
        val notification = NotificationCompat.Builder(context, "PROMO")
            .setSmallIcon(R.drawable.ic_promo)
            .setContentTitle(title)
            .setContentText(body)
            .setPriority(NotificationCompat.PRIORITY_LOW)
            .setAutoCancel(true)
            .build()
        manager.notify(System.currentTimeMillis().toInt(), notification)
    }

    fun cancelAll() = manager.cancelAll()
}

// 사용
val notificationFacade = NotificationFacade(context)
notificationFacade.showOrderNotification("12345", "배송 중")
notificationFacade.showChatNotification("홍길동", "안녕하세요!")

4. Android 실전 예제 ② — 네트워크 파사드

class NetworkFacade(
    private val userRepository: UserRepository,
    private val productRepository: ProductRepository,
    private val orderRepository: OrderRepository
) {
    // 홈 화면에 필요한 데이터를 한 번에 로딩
    suspend fun loadHomeData(): HomeData {
        return coroutineScope {
            val userDeferred     = async { userRepository.getCurrentUser() }
            val productsDeferred = async { productRepository.getFeaturedProducts() }
            val ordersDeferred   = async { orderRepository.getRecentOrders(limit = 5) }

            HomeData(
                user     = userDeferred.await(),
                products = productsDeferred.await(),
                orders   = ordersDeferred.await()
            )
        }
    }

    // 주문 처리 — 여러 서브시스템 조율
    suspend fun placeOrder(cartItems: List<CartItem>): OrderResult {
        val user    = userRepository.getCurrentUser()
            ?: return OrderResult.Failure("로그인이 필요합니다")
        val order   = orderRepository.createOrder(user.id, cartItems)
        val payment = orderRepository.processPayment(order.id, order.totalPrice)
        return if (payment.isSuccess) {
            productRepository.decreaseStock(cartItems)
            OrderResult.Success(order.id)
        } else {
            orderRepository.cancelOrder(order.id)
            OrderResult.Failure(payment.errorMessage)
        }
    }
}

// ViewModel — 파사드만 알면 됨
@HiltViewModel
class HomeViewModel @Inject constructor(
    private val networkFacade: NetworkFacade
) : ViewModel() {

    fun loadHome() {
        viewModelScope.launch {
            _state.update { it.copy(isLoading = true) }
            runCatching { networkFacade.loadHomeData() }
                .onSuccess { data -> _state.update { it.copy(isLoading = false, data = data) } }
                .onFailure { e  -> _state.update { it.copy(isLoading = false, error = e.message) } }
        }
    }
}

5. Android 실전 예제 ③ — 카메라 파사드

class CameraFacade(private val activity: AppCompatActivity) {

    private val cameraProviderFuture = ProcessCameraProvider.getInstance(activity)
    private var imageCapture: ImageCapture? = null

    fun startPreview(previewView: PreviewView) {
        cameraProviderFuture.addListener({
            val cameraProvider = cameraProviderFuture.get()
            val preview = Preview.Builder().build().also {
                it.setSurfaceProvider(previewView.surfaceProvider)
            }
            imageCapture = ImageCapture.Builder()
                .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
                .build()

            cameraProvider.unbindAll()
            cameraProvider.bindToLifecycle(
                activity, CameraSelector.DEFAULT_BACK_CAMERA, preview, imageCapture
            )
        }, ContextCompat.getMainExecutor(activity))
    }

    fun takePicture(onSuccess: (Uri) -> Unit, onError: (String) -> Unit) {
        val capture = imageCapture ?: run { onError("카메라 초기화 안 됨"); return }
        val file    = createImageFile(activity)
        val options = ImageCapture.OutputFileOptions.Builder(file).build()

        capture.takePicture(
            options,
            ContextCompat.getMainExecutor(activity),
            object : ImageCapture.OnImageSavedCallback {
                override fun onImageSaved(output: ImageCapture.OutputFileResults) =
                    onSuccess(Uri.fromFile(file))
                override fun onError(exception: ImageCaptureException) =
                    onError(exception.message ?: "촬영 실패")
            }
        )
    }

    private fun createImageFile(context: Context): File {
        val dir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
        return File(dir, "IMG_${System.currentTimeMillis()}.jpg")
    }
}

// Activity — 카메라 복잡성은 파사드 내부에 숨겨짐
class CameraActivity : AppCompatActivity() {
    private val cameraFacade by lazy { CameraFacade(this) }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        cameraFacade.startPreview(binding.previewView)

        binding.btnCapture.setOnClickListener {
            cameraFacade.takePicture(
                onSuccess = { uri -> showPreview(uri) },
                onError   = { msg -> showError(msg) }
            )
        }
    }
}

6. 파사드 vs 관련 패턴

패턴 목적 인터페이스 수
파사드 복잡한 서브시스템을 단순화 여러 → 하나
어댑터 호환되지 않는 인터페이스 변환 하나 → 다른 하나
데코레이터 기능 추가 (인터페이스 동일 유지) 동일 유지
미디에이터 객체 간 통신을 중재자가 처리 여러 ↔ 여러

7. 정리

항목 내용
목적 복잡한 서브시스템을 단순한 인터페이스로 감춤
핵심 호출부는 파사드만 알면 되고, 내부 복잡성은 숨겨짐
장점 서브시스템과 호출부의 결합도 감소, 사용 편의성 향상
단점 파사드가 서브시스템 모두를 참조 → God Object 위험
Android 사례 미디어 플레이어, 알림, 카메라, 네트워크 조율

참고



Related Posts