
Facade 패턴은 복잡한 SDK나 서브시스템을 다룰 때 특히 실전적인 구조 패턴입니다. Kotlin에서도 초기화 순서, 객체 조합, 예외 번역이 반복되기 시작하면 단순한 진입점 하나를 두는 편이 훨씬 읽기 쉽고 안전해집니다.
이번 글에서는 Facade가 정확히 어떤 문제를 푸는지 먼저 짚고, SDK 초기화 매니저 형태의 Kotlin 예제로 감을 잡아보겠습니다. 그리고 Adapter, Proxy와 무엇이 다른지도 짧고 분명하게 정리하겠습니다.
Facade 패턴을 먼저 감으로 이해하기
Facade를 어렵게 보면 클래스 하나 더 만드는 패턴처럼 보입니다. 하지만 핵심은 클래스 수가 아니라 진입점의 단순화입니다.
예를 들어 외부 미디어 SDK를 앱에서 쓰려면 설정 로드, API 키 검증, 로거 준비, 썸네일 생성, 업로드 실행 같은 절차가 필요할 수 있습니다. 이 순서를 화면이나 서비스마다 직접 적기 시작하면 호출부는 금방 비대해집니다.
Facade는 이때 uploadVideo() 같은 단순한 입구를 제공합니다. 클라이언트는 내부 부품이 몇 개인지, 어떤 순서로 조립해야 하는지 몰라도 됩니다.
Facade의 핵심은 여러 부품을 더 똑똑하게 만드는 것이 아니라, 클라이언트가 복잡한 조립 순서를 몰라도 되게 만드는 데 있습니다.
Facade 패턴이 필요한 순간
Facade는 기능 부족 때문에 쓰는 패턴이 아닙니다. 대부분 기능은 이미 있고, 문제는 그 기능이 너무 잘게 나뉘어 있어서 올바른 사용 순서를 매번 클라이언트가 기억해야 한다는 점입니다.
- 같은 초기화 순서가 여러 곳에서 반복된다
- 서브시스템 객체가 너무 많이 노출된다
- 클라이언트가 성공·실패 규칙까지 직접 조합해야 한다
- 외부 SDK 교체 시 손댈 곳이 너무 많아질 것 같다
특히 팀 안에서 “이 기능 쓰려면 먼저 A 만들고, 그다음 B 연결하고, 마지막에 C를 호출해야 해” 같은 설명이 자주 나온다면 이미 Facade 후보일 가능성이 큽니다.
Kotlin 예제로 보는 Facade 패턴
예시는 동영상 업로드 SDK를 감싼다고 가정하겠습니다. 설정 로더, 토큰 검증기, 업로더, 썸네일 생성기가 따로 있고, 클라이언트는 최종 업로드만 원한다고 보겠습니다.
먼저 복잡한 서브시스템 쪽 클래스부터 간단히 정의하겠습니다.
class SdkConfigLoader {
fun load(): MediaSdkConfig {
return MediaSdkConfig(
apiKey = "demo-key",
endpoint = "https://api.media.example",
)
}
}
data class MediaSdkConfig(
val apiKey: String,
val endpoint: String,
)
class ApiKeyValidator {
fun validate(apiKey: String) {
require(apiKey.isNotBlank()) { "API 키가 비어 있습니다." }
}
}
class MediaLogger {
fun info(message: String) {
println("[media] $message")
}
}
class ThumbnailGenerator {
fun generate(videoPath: String): String {
return videoPath.replace(".mp4", "-thumb.png")
}
}
class VideoUploader(
private val endpoint: String,
private val logger: MediaLogger,
) {
fun upload(videoPath: String, thumbnailPath: String): String {
logger.info("upload to $endpoint")
return "media_${videoPath.hashCode()}_${thumbnailPath.hashCode()}"
}
}이 상태에서 클라이언트가 직접 모든 객체를 조립하면 아래처럼 됩니다.
class VideoPublishService {
fun publish(videoPath: String): String {
val config = SdkConfigLoader().load()
ApiKeyValidator().validate(config.apiKey)
val logger = MediaLogger()
val thumbnailGenerator = ThumbnailGenerator()
val uploader = VideoUploader(
endpoint = config.endpoint,
logger = logger,
)
val thumbnailPath = thumbnailGenerator.generate(videoPath)
return uploader.upload(videoPath, thumbnailPath)
}
}짧아 보이지만 실제 프로젝트에서는 여기에 재시도, 옵션 조합, 실패 변환, 메트릭, 기록 저장까지 붙기 쉽습니다. 그 순간부터 서비스는 핵심 역할보다 준비 절차를 더 많이 알게 됩니다.
Facade를 두면 무엇이 달라질까
이제 복잡한 절차를 Facade 안으로 모아보겠습니다.
class MediaPlatformFacade(
private val configLoader: SdkConfigLoader,
private val apiKeyValidator: ApiKeyValidator,
private val logger: MediaLogger,
private val thumbnailGenerator: ThumbnailGenerator,
) {
fun uploadVideo(videoPath: String): UploadResult {
return try {
val config = configLoader.load()
apiKeyValidator.validate(config.apiKey)
val uploader = VideoUploader(
endpoint = config.endpoint,
logger = logger,
)
val thumbnailPath = thumbnailGenerator.generate(videoPath)
val mediaId = uploader.upload(videoPath, thumbnailPath)
UploadResult.Success(mediaId = mediaId)
} catch (e: IllegalArgumentException) {
UploadResult.Failure(reason = e.message ?: "잘못된 설정입니다.")
} catch (e: Exception) {
UploadResult.Failure(reason = "미디어 업로드 중 오류가 발생했습니다.")
}
}
}
sealed interface UploadResult {
data class Success(val mediaId: String) : UploadResult
data class Failure(val reason: String) : UploadResult
}이제 클라이언트는 훨씬 단순해집니다.
class VideoPublishService(
private val mediaFacade: MediaPlatformFacade,
) {
fun publish(videoPath: String): UploadResult {
return mediaFacade.uploadVideo(videoPath)
}
}서비스는 이제 업로드를 위해 어떤 부품이 몇 개 필요한지 몰라도 됩니다. 그냥 업로드를 요청하고 결과만 받습니다.
이 예제에서 꼭 읽어야 할 포인트
진입점이 줄어든다
클라이언트 입장에서는 uploadVideo() 하나만 알면 됩니다. 서브시스템의 세부 클래스 이름을 전부 기억할 필요가 없습니다.
호출 순서가 한곳에 모인다
설정 로드, 검증, 썸네일 생성, 업로드 순서가 Facade 안에 모입니다. 순서를 바꿔야 할 때도 수정 지점이 분명합니다.
실패 규칙이 통일된다
예외가 밖으로 제멋대로 새지 않고 UploadResult라는 프로젝트 친화적인 결과 타입으로 정리됩니다. 이 지점은 실무에서 꽤 큽니다.
외부 SDK 교체 비용을 낮춘다
나중에 다른 미디어 SDK로 바꾸더라도 바깥 서비스들은 MediaPlatformFacade 진입점에만 기대고 있으면 됩니다.
Facade는 서브시스템을 없애는 패턴이 아니라, 서브시스템의 복잡도가 바깥으로 새지 않게 막는 패턴입니다.
Facade가 실제로 감추는 것
객체 생성 순서
특정 객체는 항상 설정 검증 뒤에만 만들어야 할 수 있습니다. 이런 규칙을 호출부마다 알게 두면 유지보수가 어려워집니다.
조합 규칙
썸네일 생성이 켜져 있을 때만 업로더 옵션을 바꾸는 식의 규칙이 자주 생깁니다. 이 조합 로직을 Facade 안으로 몰아두면 바깥은 훨씬 단순해집니다.
실패 번역
SDK 예외, 네트워크 예외, 설정 오류를 그대로 바깥으로 던지면 상위 계층이 세부사항에 묶입니다. Facade는 이 실패를 프로젝트 언어로 번역하는 위치가 될 수 있습니다.
사용 범위 제한
복잡한 라이브러리는 기능이 많아도 실제 프로젝트에서 쓰는 경로는 일부뿐일 때가 많습니다. Facade는 그중 자주 쓰는 경로만 안전하게 노출합니다.
Kotlin에서는 왜 더 실용적으로 느껴질까
Kotlin에서는 Facade가 자바보다 덜 무겁게 느껴질 때가 많습니다. data class로 설정과 결과를 짧게 정의할 수 있고, sealed interface로 성공과 실패를 읽기 좋게 표현할 수 있기 때문입니다.
또한 Kotlin 공식 문서가 설명하듯 delegation은 구현 상속의 대안이 될 수 있고, by 문법으로 전달 보일러플레이트를 줄여줍니다. 다만 이 문법이 곧 Facade 자체를 대신하는 것은 아닙니다. 핵심은 복잡한 진입 절차를 어디에 모을지에 대한 설계 판단입니다.
Facade와 Adapter 차이
Facade와 Adapter는 둘 다 중간 계층처럼 보여서 자주 헷갈립니다. 하지만 질문이 다릅니다.
- Adapter: 안 맞는 인터페이스를 어떻게 연결할까
- Facade: 복잡한 사용법을 어떻게 단순하게 감출까
외부 결제 SDK의 pay()를 우리 서비스의 PaymentGateway.charge()에 맞추면 Adapter에 가깝습니다. 반면 결제 승인, 영수증 발행, 정산 예약, 알림 발송을 PaymentFacade.completeOrder() 하나로 묶으면 Facade에 더 가깝습니다.
즉 Adapter는 호환성 문제를 풀고, Facade는 복잡도 문제를 풉니다.
Facade와 Proxy 차이
Facade와 Proxy도 모두 앞단에 서 있는 객체처럼 보일 수 있지만 의도가 다릅니다.
- Proxy: 같은 인터페이스를 유지하면서 접근을 제어하거나 대리한다
- Facade: 여러 객체를 묶어 더 쉬운 새 진입점을 만든다
무거운 객체를 지연 생성하거나 접근 권한을 확인하는 것은 Proxy 설명에 더 잘 맞습니다. 반면 여러 서브시스템을 묶어 “업로드 한 번 실행” 같은 단순 흐름을 만드는 것은 Facade 쪽입니다.
Facade를 쓸 때 주의할 점
너무 많은 기능을 한 Facade에 몰아넣는 문제
업로드, 다운로드, 인코딩, 보관, 삭제, 권한 체크를 전부 한 클래스에 넣으면 Facade가 다시 복잡한 서브시스템처럼 변합니다. 이럴 때는 역할별로 Facade를 나누는 편이 낫습니다.
도메인 로직까지 Facade가 먹어버리는 문제
Facade는 조립과 진입점 단순화에 강점이 있습니다. 할인 정책, 권한 정책, 주문 규칙 같은 핵심 도메인 판단까지 다 넣어버리면 책임이 흐려집니다.
내부 세부사항을 너무 숨겨서 확장성이 떨어지는 문제
Facade는 단순함을 주지만, 모든 상황에 단일 경로만 강제하면 고급 사용 사례를 막을 수 있습니다. 그래서 자주 쓰는 기본 경로만 Facade로 제공하고, 필요하면 하위 API도 열어두는 식의 균형이 필요합니다.
좋은 Facade는 복잡함을 숨기지만, 책임까지 뭉개지는 만능 클래스가 되면 안 됩니다.
이런 상황이면 Facade를 고려해볼 만하다
- 여러 객체를 정해진 순서로 호출해야 한다
- 사용 흐름은 자주 같지만 내부 부품은 많다
- 호출부가 외부 SDK 세부사항을 너무 많이 알고 있다
- 실패 규칙과 반환 형태를 통일하고 싶다
- 복잡한 라이브러리 기능 중 일부만 안전하게 노출하고 싶다
반대로 객체가 몇 개 없고 절차도 단순하다면 굳이 Facade까지 두지 않아도 됩니다. 패턴 이름보다 반복되는 복잡도를 줄이는 쪽이 먼저입니다.
마무리
Facade 패턴을 한 문장으로 줄이면 이렇습니다. 복잡한 서브시스템 앞에 단순한 진입점 하나를 두어, 클라이언트가 내부 조립 순서를 몰라도 되게 만드는 패턴입니다.
Kotlin에서는 data class, sealed interface, delegation 덕분에 이런 구조를 비교적 담백하게 만들 수 있습니다. 그래서 거창한 프레임워크 설계가 아니어도 SDK 초기화 매니저나 결제·업로드 조정 계층처럼 현실적인 문제에 바로 적용하기 좋습니다.
같이 보면 좋은 글은 코틀린 디자인 패턴 시리즈 (6) – Adapter 패턴으로 기존 코드를 새 인터페이스에 맞추기, 코틀린 디자인 패턴 시리즈 (9) – Decorator 패턴은 상속 대신 어떻게 확장할까, 상속보다 조합이 더 나은 순간: 객체지향 설계에서 조합을 고르는 기준입니다. 외부 참고 자료로는 Refactoring.Guru의 Facade 설명, Adapter 설명, Proxy 설명, Kotlin Delegation 문서, Kotlin Interfaces 문서를 함께 보면 좋습니다.