(Kotlin/코틀린) 파사드 패턴(Facade Pattern) 완전 정리
08 May 2026 -
14 mins read time
Tags:
Kotlin
Android
개요
- 구조 패턴(Structural Pattern) 중 파사드 패턴(Facade Pattern) 을 다룹니다.
- 파사드 패턴은 복잡한 서브시스템에 단순한 인터페이스(창구)를 제공 하는 패턴입니다.
- 이 글에서는 다음을 설명합니다.
- 파사드 패턴이 필요한 이유
- 기본 구조와 Kotlin 구현
- Android 실전 예제 (미디어 플레이어, 카메라, 알림, 네트워크)
- 파사드 패턴의 장단점
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 사례 |
미디어 플레이어, 알림, 카메라, 네트워크 조율 |
- 파사드 패턴은 “복잡한 내부를 감추고 쉬운 창구를 제공” 하는 것이 핵심입니다.
- Android에서 여러 시스템 서비스(AudioManager, WifiManager, PowerManager 등)를 함께 사용해야 할 때 파사드로 묶으면 코드가 훨씬 깔끔해집니다.
참고
Related Posts