(Kotlin/코틀린) 프록시 패턴(Proxy Pattern) 완전 정리
09 May 2026 -
15 mins read time
Tags:
Kotlin
Android
개요
- 구조 패턴(Structural Pattern) 중 프록시 패턴(Proxy Pattern) 을 다룹니다.
- 프록시 패턴은 실제 객체에 대한 접근을 대리(Proxy) 객체가 제어 하는 패턴입니다.
- 같은 인터페이스를 구현하므로 클라이언트는 프록시와 실제 객체를 구별하지 않아도 됩니다.
- 이 글에서는 다음을 설명합니다.
- 프록시 패턴이 필요한 이유
- 프록시 종류 (가상, 보호, 캐싱, 원격)
- Kotlin
by 위임을 활용한 프록시
- Android 실전 예제 (Lazy 로딩, 권한 제어, 캐싱)
1. 왜 프록시 패턴이 필요한가
// 실제 객체 생성 비용이 큼
class HeavyImageProcessor {
init {
println("HeavyImageProcessor 초기화 — 머신러닝 모델 로딩 중... (5초)")
Thread.sleep(5000)
}
fun processImage(bitmap: Bitmap): Bitmap = bitmap // 실제 처리
}
// ❌ 직접 사용 — 사용 여부와 관계없이 앱 시작 시 5초 대기
class MainActivity : AppCompatActivity() {
private val processor = HeavyImageProcessor() // 즉시 초기화
}
// ✅ 프록시로 해결 — 실제 사용 시점에 초기화
class MainActivity : AppCompatActivity() {
private val processor = LazyImageProcessorProxy() // 즉시 반환, 초기화 안 함
fun onEditPhoto(bitmap: Bitmap) {
processor.processImage(bitmap) // 이 시점에 HeavyImageProcessor 초기화
}
}
2. 프록시 종류
| 종류 |
목적 |
사용 시점 |
| 가상 프록시 |
생성 비용이 큰 객체의 초기화 지연 |
무거운 객체, 지연 로딩 |
| 보호 프록시 |
접근 권한 제어 |
로그인 여부, 역할 기반 접근 |
| 캐싱 프록시 |
결과를 캐싱해 중복 요청 방지 |
네트워크, DB 조회 |
| 원격 프록시 |
원격 객체에 대한 로컬 대리 |
네트워크 통신 추상화 |
3. 가상 프록시 (Virtual Proxy) — 지연 초기화
interface ImageProcessor {
fun processImage(bitmap: Bitmap): Bitmap
fun applyFilter(bitmap: Bitmap, filter: String): Bitmap
}
// 실제 구현 — 초기화 비용이 큼
class HeavyImageProcessor : ImageProcessor {
init { println("ML 모델 로딩 중... (3초)"); Thread.sleep(3000) }
override fun processImage(bitmap: Bitmap) = bitmap
override fun applyFilter(bitmap: Bitmap, filter: String) = bitmap
}
// 가상 프록시 — 처음 사용 시 실제 객체 생성
class LazyImageProcessorProxy : ImageProcessor {
private val real: ImageProcessor by lazy {
println("처음 사용 — HeavyImageProcessor 초기화")
HeavyImageProcessor()
}
override fun processImage(bitmap: Bitmap): Bitmap {
println("processImage 호출 → 실제 객체에 위임")
return real.processImage(bitmap)
}
override fun applyFilter(bitmap: Bitmap, filter: String): Bitmap {
println("applyFilter 호출 → 실제 객체에 위임")
return real.applyFilter(bitmap, filter)
}
}
// 사용
val processor: ImageProcessor = LazyImageProcessorProxy()
println("프록시 생성 완료 — 아직 ML 모델 안 로딩")
// 실제 사용 시점에만 초기화
val result = processor.processImage(bitmap) // 이 시점에 HeavyImageProcessor 초기화
4. 보호 프록시 (Protection Proxy) — 권한 제어
interface AdminService {
fun deleteUser(userId: Long)
fun getSystemLogs(): List<String>
fun updateSystemConfig(config: Map<String, String>)
}
class RealAdminService : AdminService {
override fun deleteUser(userId: Long) = println("유저 $userId 삭제")
override fun getSystemLogs() = listOf("Log1", "Log2", "Log3")
override fun updateSystemConfig(config: Map<String, String>) = println("설정 업데이트: $config")
}
// 보호 프록시 — 접근 권한 확인 후 실제 객체에 위임
class AdminServiceProxy(
private val real: AdminService,
private val currentUser: () -> User?
) : AdminService {
private fun checkAdmin() {
val user = currentUser() ?: throw SecurityException("로그인이 필요합니다")
if (user.role != Role.ADMIN) throw SecurityException("관리자 권한이 필요합니다")
}
override fun deleteUser(userId: Long) {
checkAdmin()
println("[Proxy] deleteUser 권한 확인 통과")
real.deleteUser(userId)
}
override fun getSystemLogs(): List<String> {
checkAdmin()
return real.getSystemLogs()
}
override fun updateSystemConfig(config: Map<String, String>) {
checkAdmin()
println("[Proxy] 설정 변경 감사 로그 기록")
real.updateSystemConfig(config)
}
}
// 사용
val adminService: AdminService = AdminServiceProxy(
real = RealAdminService(),
currentUser = { sessionManager.getCurrentUser() }
)
try {
adminService.deleteUser(42L) // 관리자면 성공, 아니면 SecurityException
} catch (e: SecurityException) {
showError(e.message)
}
5. 캐싱 프록시 (Caching Proxy)
interface WeatherRepository {
suspend fun getWeather(cityCode: String): Weather
}
class RemoteWeatherRepository(private val api: WeatherApi) : WeatherRepository {
override suspend fun getWeather(cityCode: String): Weather = api.fetchWeather(cityCode)
}
// 캐싱 프록시 — 동일 요청은 캐시에서 반환
class CachingWeatherProxy(
private val real: WeatherRepository,
private val ttlMs: Long = 10 * 60 * 1000L // 10분 캐시
) : WeatherRepository {
data class CacheEntry(val weather: Weather, val timestamp: Long)
private val cache = mutableMapOf<String, CacheEntry>()
override suspend fun getWeather(cityCode: String): Weather {
val cached = cache[cityCode]
val now = System.currentTimeMillis()
if (cached != null && now - cached.timestamp < ttlMs) {
println("[Cache] 캐시 반환: $cityCode")
return cached.weather
}
println("[Cache] 원격 조회: $cityCode")
return real.getWeather(cityCode).also {
cache[cityCode] = CacheEntry(it, now)
}
}
fun invalidate(cityCode: String) { cache.remove(cityCode) }
fun invalidateAll() { cache.clear() }
}
// 조합
val weatherRepo: WeatherRepository = CachingWeatherProxy(
RemoteWeatherRepository(weatherApi)
)
6. Kotlin by 위임 — 간결한 프록시
Kotlin의 by 키워드로 보일러플레이트 없이 프록시를 구현합니다.
interface Analytics {
fun trackEvent(name: String, params: Map<String, Any> = emptyMap())
fun trackScreen(screenName: String)
fun setUserId(userId: String)
}
class FirebaseAnalytics : Analytics {
override fun trackEvent(name: String, params: Map<String, Any>) =
println("[Firebase] Event: $name, $params")
override fun trackScreen(screenName: String) =
println("[Firebase] Screen: $screenName")
override fun setUserId(userId: String) =
println("[Firebase] UserId: $userId")
}
// by로 실제 구현에 위임 — 오버라이드가 필요한 메서드만 재정의
class DebugAnalyticsProxy(
private val real: Analytics
) : Analytics by real {
// trackEvent만 오버라이드 (로그 추가), 나머지는 real에 자동 위임
override fun trackEvent(name: String, params: Map<String, Any>) {
println("[DEBUG] trackEvent 호출: $name") // 디버그 로그 추가
real.trackEvent(name, params)
}
}
// 환경별 선택
val analytics: Analytics = if (BuildConfig.DEBUG)
DebugAnalyticsProxy(FirebaseAnalytics())
else
FirebaseAnalytics()
analytics.trackEvent("button_click", mapOf("screen" to "home"))
analytics.trackScreen("HomeScreen") // real에 자동 위임
7. Android 실전 예제 ① — Repository 프록시
interface UserRepository {
suspend fun getUser(id: Long): User?
suspend fun updateUser(user: User): Boolean
suspend fun deleteUser(id: Long): Boolean
}
// 로깅 + 에러 핸들링 프록시
class SafeUserRepositoryProxy(
private val real: UserRepository
) : UserRepository {
override suspend fun getUser(id: Long): User? =
runCatching { real.getUser(id) }
.onFailure { println("[Error] getUser($id) 실패: ${it.message}") }
.getOrNull()
override suspend fun updateUser(user: User): Boolean =
runCatching { real.updateUser(user) }
.onSuccess { println("[Log] 유저 업데이트: ${user.id}") }
.onFailure { println("[Error] updateUser 실패: ${it.message}") }
.getOrDefault(false)
override suspend fun deleteUser(id: Long): Boolean =
runCatching { real.deleteUser(id) }
.onSuccess { println("[Log] 유저 삭제: $id") }
.onFailure { println("[Error] deleteUser 실패: ${it.message}") }
.getOrDefault(false)
}
8. Android 실전 예제 ② — SharedPreferences 프록시
interface AppSettings {
var isDarkMode: Boolean
var fontSize: Int
var language: String
fun reset()
}
class SharedPrefsSettings(context: Context) : AppSettings {
private val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
override var isDarkMode: Boolean
get() = prefs.getBoolean("dark_mode", false)
set(v) = prefs.edit().putBoolean("dark_mode", v).apply()
override var fontSize: Int
get() = prefs.getInt("font_size", 14)
set(v) = prefs.edit().putInt("font_size", v).apply()
override var language: String
get() = prefs.getString("language", "ko") ?: "ko"
set(v) = prefs.edit().putString("language", v).apply()
override fun reset() = prefs.edit().clear().apply()
}
// 변경 감지 프록시 — 값이 바뀔 때마다 콜백 호출
class ObservableSettingsProxy(
private val real: AppSettings,
private val onChange: (String, Any) -> Unit
) : AppSettings by real {
override var isDarkMode: Boolean
get() = real.isDarkMode
set(v) { real.isDarkMode = v; onChange("isDarkMode", v) }
override var fontSize: Int
get() = real.fontSize
set(v) { real.fontSize = v; onChange("fontSize", v) }
override var language: String
get() = real.language
set(v) { real.language = v; onChange("language", v) }
}
// 사용
val settings: AppSettings = ObservableSettingsProxy(
real = SharedPrefsSettings(context),
onChange = { key, value -> println("설정 변경: $key = $value") }
)
settings.isDarkMode = true // 설정 변경: isDarkMode = true
settings.fontSize = 18 // 설정 변경: fontSize = 18
9. 프록시 vs 데코레이터
두 패턴은 구조가 유사하지만 목적이 다릅니다.
| 항목 |
프록시 |
데코레이터 |
| 목적 |
접근 제어 (권한, 캐싱, 지연) |
기능 추가 |
| 실제 객체 생성 |
프록시가 직접 관리 |
외부에서 주입 |
| 체이닝 |
일반적으로 단일 |
여러 개 중첩 가능 |
| 클라이언트 인지 |
프록시 존재를 모름 |
데코레이터 존재를 알 수 있음 |
10. 정리
| 항목 |
내용 |
| 목적 |
실제 객체 접근을 대리 객체가 제어 |
| 가상 프록시 |
by lazy로 지연 초기화 |
| 보호 프록시 |
권한 체크 후 실제 객체에 위임 |
| 캐싱 프록시 |
결과를 저장해 중복 요청 방지 |
| Kotlin 도구 |
by 위임으로 필요한 메서드만 오버라이드 |
| Android 사례 |
Repository 에러 처리, 설정 변경 감지, Analytics 래퍼 |
- 프록시 패턴은 “실제 객체 앞에 문지기를 세우는 것” 입니다.
- Kotlin
by lazy가 가상 프록시의 가장 자연스러운 구현이며, by 위임으로 보일러플레이트 없이 간결하게 작성할 수 있습니다.
참고
Related Posts