본문 바로가기
프로그래밍/kotlin

[디자인 패턴] 프록시 패턴이란

by 뜨끔쓰 2022. 9. 19.
728x90
728x90
인프런의 김영한님의 강의 스프링 핵심 원리 - 고급편을 학습하며 정리한 글입니다.

 

스프링 핵심 원리 - 고급편 (인프런)

 

스프링 핵심 원리 - 고급편 - 인프런 | 강의

스프링의 핵심 원리와 고급 기술들을 깊이있게 학습하고, 스프링을 자신있게 사용할 수 있습니다., - 강의 소개 | 인프런...

www.inflearn.com

 

프록시 패턴이란?

일반적으로 프록시는 클라이언트에서 서버에 요청할때 직접 요청하는 것이 아니라 어떤 대리자를 통해 대신 간접적으로 서버에 요청할 수 있도록 하는 대리자를 영어로 프록시(Proxy)라고한다. 그중에서도 접근의 제어가 목적인 경우가 프록시 패턴이다.

 

그림으로 한번 알아보자

Client -> Server 직접호출

위의 그림은 클라이언트에서 서버로 직접 호출 하는 예시이다.

 

여기서 클라이언트와 서버라는 것은 우리가 흔히 알고 있는 client, server가 아닌 요청하는 객체(client)와 요청을 처리하는 객체(server)라고 생각해야한다.

 

여기에서 중간에 대리자(Proxy)를 추가 하여 보자.

Proxy를 이용한 간접호출

이렇게 프록시를 이용하여 간접 호출을 하면 클라이언트 코드를 수정하지 않고 여러가지 일을 할 수 있다.

주로 GOF 디자인패턴에서는 이 둘의 의도에 따라서 프록시패턴과 데코레이터 패턴으로 구분한다.

  • 프록시패턴: 접근 제어가 목적
  • 데코레이터 패턴: 새로운 기능 추가가 목적

여기서는 프록시 패턴에 대하여 알아보는 것이기 때문에 예시 코드를 활용하여 접근제어를 프록시로 어떻게 처리하는지 확인해봅시다.

 

공통적으로 사용할 Subject라는 인터페이스를 간단하게 작성합니다.

Subject
interface Subject {
    fun operation():String?
}

매우 간단한 코드입니다.

 

이제 Subject 인터페이스를 상속받아 사용할 RealSubject를 작성합니다.

RealSubject
class RealSubject: Subject {
    private val log = LoggerFactory.getLogger(RealSubject::class.java)
    override fun operation(): String {
        log.info("실제 객체 호출")
        sleep(1000)
        return "data"
    }

    private fun sleep(millis: Long) {
        try {
            Thread.sleep(millis)
        }catch (e: InterruptedException){
            e.printStackTrace()
        }

    }
}

실제로 Subject를 상속받아 operation 함수를 override하여 log를 찍도록 작성하였습니다.

 

여기까지는 프록시 패턴을 적용하지 않고 일반적으로 사용할때 작성한 코드들입니다.

 

이제 클라이언트 코드를 작성해봅시다.

ProxyPatternClient
class ProxyPatternClient(
    private val subject: Subject,
) {

    fun execute(){
        subject.operation()
    }
}

Subject를 의존성으로 주입받아서 operation을 실행해주는 간단한 코드입니다.

 

이제 첫번째 경우인 Client -> Server로 호출하는 테스트 코드를 작성하여 결과를 확인해봅시다.

 

noProxyTest
    @Test
    fun noProxyTest(){
        val realSubject = RealSubject()
        val client = ProxyPatternClient(realSubject)

        client.execute()
        client.execute()
        client.execute()
    }

 

결과

noProxyTest 결과화면

대략 1초간격으로 실제 객체가 호출되는것을 볼 수 있습니다.

 

첫번째 요청이나 두번째, 세번째 요청이나 같은 값이 나온다는 것을 아는데 1초간격의 시간이 걸리는 걸 클라이언트의 코드를 수정하지 않고 해결할 방법이 없을까?

 

생각해보면 프록시패턴을 이용하여 Client와 Server의 요청 사이에 만약 요청하여 결과 값을 받은 적이 있으면 그 값을 가지고 있다가 다음번 호출때 결과를 내려주면 효율적인 코드가 작성 될 것입니다.

 

이것이 바로 접근제어를 이용한 프록시패턴입니다.

 

그럼 코드로 확인해보도롭합시다!

CacheProxy라는 클래스를 작성합니다.

 

CacheProxy
class CacheProxy(
    private val target: Subject,
): Subject {
    private val log = LoggerFactory.getLogger(CacheProxy::class.java)
    private var cacheValue: String? = null
    override fun operation(): String? {
        log.info("프록시 호출")
        if(cacheValue == null){
            cacheValue = target.operation()
        }

        return cacheValue
    }

}

코드를 확인해보면 복잡한 로직은 아니며, 단지 Subject를 의존성으로 받아 target 인스턴스의 operation 함수를 호출한 결과값을 cacheValue 변수에 담아주는 로직이 추가되었습니다.

 

첫번째 호출시에는 cacheValue가 null이기 때문에 실제 target인스턴스의 operation을 호출하지만 값이 있는 경우

 

실제 target 인스턴스의 operation을 호출하지 않고 미리 저장해놓은 cacheValue를 결과값을 반환합니다.

 

테스트코드로 확인해봅시다.

 

cacheProxyTest
    @Test
    fun cacheProxyTest() {
        val realSubject = RealSubject()
        val cacheProxy = CacheProxy(realSubject)
        val client = ProxyPatternClient(cacheProxy)

        client.execute()
        client.execute()
        client.execute()
    }

Client -> Proxy -> Server 형태의 호출이 필요하기 때문에 기존 noProxyTest와는 다르게 중간에 CacheProxy클래스가 추가되었습니다.

 

 

결과

 

cacheProxyTest 결과

처음 실제 객체 호출시에 1초가 필요하며 나머지는 바로바로 호출되는 것을 볼 수 있습니다.

 

이처럼 프록시패턴의 핵심은 RealSubject코드와 클라이언트 코드를 전혀 변경하지 않고, 중간에 프록시를 도입해서 접근제어를 할 수 있게 만드는 것입니다.

 

하지만 이런 장점만 있는 것은 아닙니다. 이렇게 프록시패턴을 사용하면 편리하지만 다음과 같은 단점이 있습니다.

  • 객체를 생성할때 중간에서 한번 더 객체를 생성하기 때문에 성능저하가 있을 수 있습니다.
  • 프록시 패턴을 사용하는 곳이 많아지면 많아질 수록 코드가 복잡하여 가독성이 떨어질 수 있습니다. 

 

배운 것을 정리한 글이기 때문에 틀리거나 잘못된 정보가 있을 수 있습니다.

 

언제든 댓글로 피드백 부탁드리겠습니다. 

 

감사합니다.

 

 

728x90
반응형

댓글