
코틀린 Proxy 패턴은 처음 보면 Decorator와 꽤 비슷해 보입니다. 둘 다 같은 인터페이스를 구현하고, 안쪽에 실제 객체를 들고, 호출을 위임하는 식으로 보이기 때문입니다. 그래서 구조만 보면 헷갈리기 쉽습니다.
하지만 목적은 꽤 다릅니다. Decorator는 기능을 덧붙이는 쪽이고, Proxy는 접근을 통제하거나 지연시키는 쪽에 더 가깝습니다. 이번 글에서는 이 차이를 lazy loading, permission check, caching 예시로 풀어보겠습니다.
Proxy의 기본 의도는 Refactoring.Guru Proxy 설명과 SourceMaking Proxy 설명에서 공통적으로 확인할 수 있습니다. 핵심은 원본 객체에 대한 접근을 같은 인터페이스 뒤에서 통제한다는 점입니다.
Proxy가 필요한 순간은 언제일까
Proxy는 원본 객체를 바로 드러내고 싶지 않을 때 등장합니다. 객체 생성 비용이 크거나, 접근 권한을 확인해야 하거나, 네트워크 호출이나 캐시처럼 요청 전후에 통제 로직을 넣어야 할 때가 대표적입니다.
- 생성 비용이 큰 객체를 실제로 필요할 때만 만들고 싶을 때
- 호출 전 권한을 검사하고 싶을 때
- 같은 요청 결과를 캐시하고 싶을 때
- 원격 객체를 로컬처럼 감싸고 싶을 때
즉 Proxy의 핵심은 “무언가를 더 예쁘게 꾸미는 일”보다, 원본 객체까지 가는 길목을 통제하는 일입니다.
가장 짧은 정의: 같은 인터페이스를 가진 대리인
SourceMaking은 Proxy를 다른 객체에 대한 surrogate 또는 placeholder라고 설명합니다. 쉽게 말해 원본 객체 앞에 서 있는 대리인입니다. 이 대리인은 같은 인터페이스를 갖기 때문에, 클라이언트는 진짜 객체를 쓰는지 Proxy를 쓰는지 크게 의식하지 않아도 됩니다.
이 “같은 인터페이스”가 아주 중요합니다. 그래야 기존 클라이언트 코드를 거의 건드리지 않고도 접근 통제 로직을 중간에 끼워 넣을 수 있기 때문입니다.
코드로 보면 더 쉽게 보인다: lazy loading proxy
예를 들어 이미지 로딩 객체가 아주 무겁다고 가정해보겠습니다. 매번 화면이 열릴 때 곧바로 큰 리소스를 읽고 싶지 않을 수 있습니다.
interface ImageSource {
fun display()
}
class RealImage(private val path: String) : ImageSource {
init {
println("load image from disk: $path")
}
override fun display() {
println("display image: $path")
}
}
class ImageProxy(private val path: String) : ImageSource {
private var realImage: RealImage? = null
override fun display() {
if (realImage == null) {
realImage = RealImage(path)
}
realImage!!.display()
}
}이 구조에서는 클라이언트가 display()를 실제로 호출하기 전까지 무거운 객체를 만들지 않습니다. 바로 이런 지연 생성이 virtual proxy의 대표 예시입니다.
이 예시가 중요한 이유는, lazy 초기화 코드를 클라이언트마다 중복해서 넣지 않고 Proxy 안에 모을 수 있다는 점입니다.
보호용 proxy는 권한 확인에 잘 맞는다
Proxy는 단순히 늦게 만드는 데만 쓰이지 않습니다. 보호용 proxy는 민감한 객체에 접근하기 전에 권한을 검사하는 데도 잘 맞습니다.
interface ReportService {
fun readFinancialReport(): String
}
class RealReportService : ReportService {
override fun readFinancialReport(): String = "sensitive financial report"
}
class ReportServiceProxy(
private val roleProvider: () -> String,
private val realService: ReportService = RealReportService(),
) : ReportService {
override fun readFinancialReport(): String {
check(roleProvider() == "ADMIN") { "access denied" }
return realService.readFinancialReport()
}
}이 경우 클라이언트는 여전히 ReportService만 바라보지만, 실제로는 proxy가 중간에서 접근을 통제합니다. 원본 서비스가 권한 로직까지 떠안지 않아도 된다는 점도 장점입니다.
캐싱 proxy도 실무에서 자주 보인다
네트워크 호출이나 외부 API처럼 비용이 큰 작업은 결과를 그대로 다시 쓰고 싶을 때가 많습니다. 이때 proxy는 “같은 인터페이스”를 유지한 채 캐시를 끼워 넣기 좋은 구조를 제공합니다.
interface VideoService {
fun loadVideo(id: String): String
}
class RemoteVideoService : VideoService {
override fun loadVideo(id: String): String {
println("call remote api: $id")
return "video:$id"
}
}
class CachingVideoProxy(
private val realService: VideoService = RemoteVideoService(),
) : VideoService {
private val cache = mutableMapOf<String, String>()
override fun loadVideo(id: String): String {
return cache.getOrPut(id) { realService.loadVideo(id) }
}
}이 코드는 단순하지만 실무 감각이 잘 드러납니다. 클라이언트는 같은 VideoService를 쓰고, proxy는 앞에서 비용과 속도를 조절합니다.
Proxy 패턴은 Decorator와 무엇이 다를까
Decorator와 왜 헷갈릴까
둘 다 래퍼 구조이기 때문입니다. 둘 다 원본 객체를 감싸고, 같은 인터페이스를 구현하고, 내부에 위임합니다. 그래서 코드 뼈대만 보면 거의 비슷해 보일 수 있습니다.
하지만 질문이 다릅니다.
- Decorator: 기능을 더 붙이고 싶은가?
- Proxy: 접근을 통제하거나 지연시키고 싶은가?
즉 Decorator는 “무엇을 더 하게 만들까”에 가깝고, Proxy는 “원본까지 가는 길을 어떻게 통제할까”에 가깝습니다.
Decorator와 비교하면 더 분명해진다
바로 앞 시리즈 글인 Decorator 패턴 글에서는 기능을 덧붙이는 목적이 중심이었습니다. 예를 들어 알림 전송 객체에 로깅, 압축, 포맷 변환 같은 기능을 차곡차곡 덧씌우는 식입니다.
반면 Proxy는 원본 기능을 바꾸기보다, 호출 시점과 접근 조건을 관리합니다. lazy loading, 권한 검사, 캐싱, 원격 호출 감춤이 대표적입니다. 그래서 둘 다 감싸는 구조이지만 의도는 꽤 다릅니다.
코틀린에서는 어떻게 더 간결해질까
코틀린에서는 Proxy 구현이 전통적인 자바 예제보다 조금 더 가볍게 느껴질 수 있습니다. 인터페이스 선언이 간결하고, nullable 참조와 getOrPut(), 고차 함수 등을 이용해 proxy의 의도를 더 짧게 표현할 수 있기 때문입니다.
- lazy loading은 nullable 참조나
lazy로 표현하기 쉽다 - 캐싱은
mutableMapOf와getOrPut()로 간단히 드러난다 - 권한 검사 proxy는 고차 함수로 정책 주입이 쉽다
즉 코틀린은 Proxy의 필요를 없애주지는 않지만, 의도를 더 짧고 또렷하게 드러내게 도와주는 편입니다.
remote proxy까지 보면 Proxy 패턴의 범위가 더 넓어진다
Proxy를 lazy loading과 권한 검사 정도로만 보면 패턴의 폭이 좁아집니다. 실제로 Proxy는 원격 객체를 로컬 객체처럼 보이게 만드는 데도 자주 연결됩니다. 즉 클라이언트는 같은 인터페이스를 호출하지만, 뒤에서는 네트워크 요청이나 IPC가 일어날 수 있습니다.
이런 구조를 이해하고 나면 Proxy는 단순 래퍼가 아니라, 원본 객체의 위치나 비용이나 접근 조건을 숨기는 인터페이스 장치라는 점이 더 선명해집니다.
smart proxy는 어디서 자주 보일까
SourceMaking은 smart proxy를 별도 용례로 설명합니다. 여기에는 접근 횟수 카운팅, 잠금 확인, 캐시, 지연 로딩처럼 원본 객체 접근 전후에 추가 행동을 끼워 넣는 경우가 포함됩니다.
실무에서는 “프록시 패턴을 쓴다”는 말을 직접 하지 않아도, connection wrapper, repository wrapper, client wrapper 형태로 비슷한 구조를 자주 만납니다. 중요한 것은 이름보다 같은 인터페이스 뒤에서 접근 정책을 조정하고 있는가입니다.
왜 Decorator와 계속 헷갈리는지 더 깊게 보자
둘 다 원본 객체를 감싸고 같은 인터페이스를 구현하니, “그냥 wrapper 아닌가?”라는 생각이 들기 쉽습니다. 실제 코드 리뷰에서도 Proxy와 Decorator를 구분하지 않고 넘어가는 경우가 많습니다.
하지만 목적을 질문으로 바꿔보면 꽤 다르게 보입니다.
- 이 래퍼는 원본 기능을 더 풍부하게 만들려는가?
- 아니면 원본에 도달하기 전 접근 조건을 조정하려는가?
- 클라이언트가 원본의 존재를 덜 의식하게 만들려는가?
- 원본 호출 자체를 늦추거나 막거나 캐시하려는가?
앞 질문에 yes가 많으면 Decorator 성격이 강하고, 뒤 질문에 yes가 많으면 Proxy 성격이 강해집니다. 즉 둘의 차이는 클래스 구조보다 래퍼를 도입한 이유에서 갈립니다.
AOP, 인터셉터, 프레임워크 래퍼와는 어떻게 다를까
여기서 또 한 번 헷갈리는 지점이 있습니다. 스프링 AOP, HTTP interceptor, DB client wrapper도 모두 “중간에서 뭔가를 한다”는 점에서 Proxy와 닮아 보입니다.
실제로 구조적으로 닮은 부분이 있습니다. 다만 글의 목적상 중요한 것은 프레임워크 용어 구분보다, 원본 객체를 그대로 노출하지 않고 중간 계층에서 접근 정책을 조정하는 사고를 이해하는 일입니다. Proxy 패턴은 바로 그 사고를 가장 기초적인 형태로 보여줍니다.
실무에서 더 쓸 만한 판단 기준
- 원본 객체 생성 비용이 커서 실제 사용 시점까지 늦추고 싶은가
- 권한, 인증, 로깅, 캐싱 같은 공통 접근 정책을 넣고 싶은가
- 클라이언트가 원본의 복잡성이나 위치를 몰라도 되게 만들고 싶은가
- 기능 추가가 아니라 접근 통제가 핵심 목표인가
이 질문에 여러 개가 yes라면 Proxy를 고려할 이유가 충분합니다. 반대로 “기능을 더 붙이고 싶은 것뿐”이라면 Decorator가 더 자연스러울 가능성이 큽니다.
언제 쓰면 좋고, 언제 과할까
- 원본 객체 접근 전에 공통 통제 로직이 필요한가
- 클라이언트 코드를 바꾸지 않고 lazy/caching/permission을 넣고 싶은가
- 원본 객체를 바로 노출하면 결합이나 비용이 커지는가
반대로 단순히 기능을 덧붙이고 싶은 것뿐이라면 Proxy보다 Decorator가 더 자연스러울 수 있습니다. 또 접근 통제가 아주 단순한데 굳이 별도 객체를 세우면 과한 추상화가 될 수도 있습니다.
마무리
코틀린 디자인 패턴 시리즈 12편의 핵심은 이렇습니다. Proxy 패턴은 구조가 Decorator와 비슷해 보여도, 실제 목적은 접근 제어, 지연 로딩, 캐싱처럼 원본 객체까지 가는 길을 통제하는 것에 더 가깝습니다.
그래서 둘을 구분할 때는 “같은 인터페이스를 구현하는가”보다, 왜 감싸고 있는가를 먼저 보세요. 기능 확장이라면 Decorator, 접근 통제라면 Proxy라는 구분이 가장 실용적입니다.
코틀린 디자인 패턴 시리즈
코틀린 디자인 패턴 시리즈(0) – 왜 아직도 디자인 패턴을 배워야 할까
코틀린 디자인 패턴(1) – Singleton 패턴은 언제 쓰고 object는 어떻게 다를까
코틀린 디자인 패턴(2) – Factory Method 패턴으로 생성 책임 나누기
코틀린 디자인 패턴(3) – Abstract Factory 패턴은 언제 필요할까
코틀린 디자인 패턴(4) – Builder 패턴과 named argument는 어떻게 다를까
코틀린 디자인 패턴(5) – Prototype 패턴과 data class copy 정리
코틀린 디자인 패턴(6) – Adapter 패턴으로 기존 코드를 새 인터페이스에 맞추기
코틀린 디자인 패턴(7) – Bridge 패턴은 상속 폭발을 어떻게 줄일까
코틀린 디자인 패턴(8) – Composite 패턴으로 트리 구조 다루기
코틀린 디자인 패턴(9) – Decorator 패턴은 상속 대신 어떻게 확장할까