프록시라는 말이 나오면 '대리자'를 떠올려 보자.
어떤 서버에 직접 요청을 보내는 것이아니라 대리자를 통해 요청을 가져와달라고 간접 요청을 보낼 수 가 있다.
이 순서가 중요한 이유는 proxy 객체가 개발중간에 추가되거나 빠져도 클라이언트와 원본 객체에는 아무런 수정이 필요하지 않다는 점에 있다.
이렇게 중간에서 대리자 역할을 하는 프록시 객체는 다음과 같은 기능을 할 수 있다.
GOF 디자인 패턴에서는 이 둘을 의도에 따라서 프록시 패턴과, 데코레이터 패턴으로 구분하여 소개한다.
그럼 접근 제어를 주 목적으로 하는 프록시 패턴에 대한 간단한 예제를 살펴보자.
interface Subject {
fun operation(): String
}
class RealSubject : Subject {
override fun operation(): String {
log.info("실제 객체 호출")
sleep(1000)
return ""
}
private fun sleep(millis: Int) {
try {
Thread.sleep(millis.toLong())
} catch (e: InterruptedException) {
e.printStackTrace()
}
}
}
class CacheProxy(
private val target: Subject,
private var cacheValue: String? = null,
) : Subject {
override fun operation(): String {
log.info("프록시 호출")
if (cacheValue == null) {
cacheValue = target.operation()
}
return cacheValue!!
}
}
class ProxyPatternClient(
private val subject: Subject,
) {
fun execute() {
subject.operation()
}
}
---
@Test
fun nonProxyTest() {
val realSubject = RealSubject()
val client = ProxyPatternClient(realSubject)
client.execute() // 프록시 호출 + 실제 객체 호출
client.execute() // 프록시 호출
client.execute() // 프록시 호출
}
@Test
fun proxyTest() {
val realSubject = RealSubject()
val cacheProxy = CacheProxy(realSubject)
val client = ProxyPatternClient(cacheProxy)
client.execute() // 프록시 호출 + 실제 객체 호출
client.execute() // 프록시 호출
client.execute() // 프록시 호출
}
위와 같은 예제 코드에서 프록시 객체를 의존하는 ProxyPatternClient 인스턴스를 생성하여 execute 함수를 여러번 호출 해보자.
처음에는 cacheValue가 null 이기 때문에 실제 객체를 호출하여 cacheValue를 생성하고 그 이후로는 실제 객체 호출 없이 프록시에서 값 반환을 끝내고 있다. 즉, 요청에 대한 실제 객체로의 접근을 제거하는 모습을 확인할 수 있다.
프록시 객체의 역할과 GOF에서 소개하는 프록시 패턴의 구분된 의미에 대해서 학습했다.
중요한 점은 프록시를 사용했을 때와 사용하지 않았을 때 클라이언트 코드가 전혀 변하지 않았다는 점이고, 유지보수 뿐만아니라 성능적인 이점을 확보하기 위해서도 프록시 객체가 유용하게 사용된다는 점을 이해할 수 있다.