////
Search
👥

Proxy pattern(프록시 패턴)

프록시 패턴이란? 위키백과
일반적으로 프록시는 다른 무언가와 이어지는 인터페이스의 역할을 하는 클래스이다. 프록시는 어떠한 것(이를테면 네트워크 연결, 메모리 안의 커다란 객체, 파일, 또 복제할 수 없거나 수요가 많은 리소스)과도 인터페이스의 역할을 수행할 수 있다. 특징
기본, 가상, 보호, 동적, 원격 등의 프록시가 존재한다.
클라이언트는 프록시를 사용하고 있는지 모른다.

프록시의 개략적 느낌

프록시 패턴의 경우 원본 객체를 대신하는 역할을 한다. 어디서 많이 본 것 같지 않은가? 그는 바로 데코레이터 패턴과 유사하다. 하지만 다른 점은 데코레이터는 특정 객체에 기능을 추가했다면, 프록시의 경우에는 기능에 초점을 맞추기 보다 접근에 초점을 맞춘다. 그래서 이름도 프록시(대리인)이라는 이름을 사용하는 것이다. 위의 위키백과 설명에서 알 수 있듯, 대리인이 필요한 이유는 다양하다. 네트워크 연결, 무거운 객체 등을 우회하기 위하여 탄생했다.
프록시 패턴
Client- Subject를 사용하는 객체 Subject- 특정 추상 RealSubject- 실제 Client가 필요로하는 정보를 담은 객체 Proxy- RealSubject를 내부에 포함하고 Client에 실제로 전달되는 객체
데코레이터 패턴

프록시는 언제 쓸까?

위에서 언급했듯, 우회라는 키워드를 통하여 기억하면 좋을 것 같다. 우회하는 경우가 뭐가 있을까? 당연히 특정 클래스를 수정하는 상황을 할 수 없는 것이다. 예를 들어 계산기를 만든다고 가정해보자. 우리의 사칙연산 계산기는 아주아주 유명해져서 많은 곳에서 의존성을 가지고 있다.
code 패키지 구조 그대로 되어있다.
근데 문제가 발생해버렸다. 또 IntegerCalculator를 사용하는 미친 클라이언트가 0 / 0을 넣어버린 것이다. 이 때문에 자바에 java.lang.ArithmeticException: / by zero 예외가 발생했다. 너무 당연한 이야기지만 생각해보니 처음부터 0이 들어오면 Calculator에 접근할 수 없도록 만들어야 한다는 것을 알았다. 그래서 Calculator를 고쳐보려고 하니 너무 많은 클래스들이 생겨버려서 고치려고 한다면 버그를 유발 할 수 있겠다는 생각을 하게 된 것이다. 이 때문에 우리는 프록시를 적용하여 이 Calculator를 고쳐보려고 한다.
code 패키지 구조 그대로 되어있다.
기존 클래스를 고친 것이 아니라 프록시를 하나 추가해서 if (next == 0) throw new UnsupportedOperationException("0으로 나눌 수 없습니다."); 구문을 추가했다. 덕분에 예외가 연산 도중에 발생하는 것이 아닌 0이 들어오면 알 수 있게 되었다. 근데 추가가 되었음에도 불구하고 클라이언트와 내가 만든 계산기를 건드린 부분은 하나도 없다. 이처럼 원본 객체(IntegerCalculator,DoubleCalculator, LongCalculator)를 수정할 수 없을 경우, 또는 원본 객체에 직접적인 접근이 민감할 경우 사용하는 것이다.
프록시 사용 전
프록시 사용 후
근데 신기한 점을 여기서 찾아볼 수 있다. 잘 찾아보자. 아래를 보면 사용 전에는직접적인 구현체, 프록시 적용 후에 예외에서 프록시 객체예외 스택에 나온다. 이처럼 예외가 발생하더라도 프록시 객체가 예외에서 나오기 때문에 원본 객체가 보호가 되는 것이다. 이 예제에서 기준으로는 0div()의 두 번째 파라미터로 들어올 수 없다. 즉, 권한이 없는 것이다. 이를 프록시에서 미리 감지하고 킥 시키거나 우회시킬 수 있는 것이다.
프록시 사용 전
프록시 사용 후
프록시 사용 전 예외 목록
프록시 사용 후 예외 목록

여기서 살짝의 디테일

이건 필자의 코딩 경험이고 순전히 내 생각이다. 그냥 “오 그럴 수도 있구나” 정도로만 받아들이도록 하자. 프록시를 사용할 때 공개되고 싶지 않은 직접적인 구현체들(IntegerCalculator,DoubleCalculator, LongCalculator)은 한 패키지에 몰아 놓고 생성자의 접근 제한자를 default, protected 로 만들면 더욱 구현을 숨길 수 있다. 왜냐하면 내 라이브러리 또는 “내 모듈을 사용하는 사람들은 전혀 직접적인 구현체를 만들 수 없기 때문이다.” DoubleCalculator() {} | LongCalculator() {} | IntegerCalculator() {} 일 때, 같은 패키지인 Proxy Classnew XXXXCalculator()가 가능하지만 Client Class에서는 전혀 못 만든다.
Client Class에서 IntegerCalculator 만들어 보기

프록시의 종류

기본 프록시

여기서 토마토 개발자의 증명사진.png를 불러온다. 하지만 이를 프록시 구조로 만든 것이다.
가장 기본적인 프록시 구조

가상 프록시

기본 프록시에서는 토마토 개발자의 증명사진.png를 불러온다. 근데 만약 초고화질 8K NMIXX 릴리 사진을 들고 와야 하는 경우에는 어떨까? 아무런 장치를 하지 않더라도 사진의 크기가 크기 때문에 2초 정도 걸린다고 가정해보자. 근데 만약 빌드 시간에 NMIXX 멤버 전원의 8K 사진을 들고 온다고 가정해보자. 근데 이게 무슨 문제가 있을까? main() 내부에서는 mem1render하고 싶은데, 전원의 사진을 로딩하기 전까진 main이 실행할 수 없게 되는 것이다. 이 때문에 가상 프록시를 이용하여 실제 사용될 때만 이미지를 불러오게 게으른 초기화를 할 수 있게 된다.
필자의 개인적인 생각으로는 이 부분을 확실하게 알아두면 좋다고 생각한다. 왜냐하면 우리는 개발을 할 때 가장 중요하게 생각해야 하는 것이 바로 생산성이기 때문이다. 갑자기 “생산성과 가상 프록시가 무슨 관계야?” 라고 생각할 수 있다. 근데 이 예제에서 우리가 main() 을 테스트라고 생각해보자. 그럼 테스트 코드를 돌리는데 필요한 시간은 최소 12초가 걸린다. 근데 게으른 초기화를 알고 있다면 최소 2초 만에 테스트가 가능해진다. 실질적으로 실무에서는 비대하고, 많은 연결과 연산이 존재하는 서비스를 만드는 작업을 하는게 개발자다. 그렇다면 이를 알아두면 좋지 않을까?
가상 프록시 구조
한명의 8K 사진을 로딩
멤버 전원의 사진을 로딩
가상 프록시 적용

보호 프록시

보호 프록시는 앞선 가상 프록시 예제를 통해 알아보자. 아래와 같은 구조가 있다. PoCaMarket - 우리가 운영중인 서비스 User - 우리 서비스에 등록된 회원 Responsibility - 멤버십 가입 여부 정도가 추가 되었다. 그리고 User 객체는 image를 렌더링 할 수 있다. 그런데 우리의 서비스에서 8K 사진을 들고 오는 작업은 고성능의 CPU작업을 요구한다. 그래서 멤버십을 가입한 사람만 8K사진을 렌더링 할 수 있다. 이럴때 보호 프록시를 사용하여 다운로드 기능 자체를 User에 심을 수 있다. 이렇듯 기존 기능(RealImageRender)에는 아무런 변화가 없다.
보호 프록시 구조
클래스 다이어그램
실행 화면

동적 프록시

동적 프록시는 앞서 나온 계산기 예제를 다시 들고 와서 생각해보고 개선해보자. 계산기 예제에서 프록시 패턴을 적용했던 이유는 0이 들어올 때 미리 감지하고 이를 걸러내기 위한 하나의 장치로 이용했다. 그로 근데 문제는 프록시로 구현해야 하는 코드가 계산기의 타입마다 생겨버리는 것이다(Integer, Long, Double Proxy). 더 큰 문제는 만약 Calculator<Integer, Double>의 구현체가 생긴다면 그에 맞춰서 프록시를 계속 만들어서 제공해야 한다. 이 문제는 프록시 패턴의 단점이기도 하다. 그래서 나온 것이 동적 프록시 이다. 동적 프록시는 하늘색 부분을 자동으로 생성해준다.
동적 프록시가 작용해야 하는 부분 표시

동적 프록시를 어떻게 만들어? Dynamic Proxy | CGLIB

Dynamic ProxyCGLIB두 가지 방식으로 계산기를 바꿔보겠다.
본 예제에서 Integer, Double, Long 클래스를 개별적으로 정의해서 사용하고 있다. 이는 프록시를 설명하기 위하여 수정이 불가능한 경우를 나타내려고 각각의 클래스를 개별적으로 정의해서 사용한 것이다. 실제로는 조금 다르게 만드는 방법이 더 좋을 수 있다. 또한 패키지 구조를 잘 살펴보면 더 생각해 볼거리가 많아 질 것 같다. 실제 구현체들(Integer, Double, Long Calculator)의 생성자를 default로 만들고 이를 factory에서 접근하여 만들고 clientfactory를 통해서 Calculator interface만 사용한다. 이를 살펴보면서 예제를 본다면 좀 더 재미있는 예제가 될 것 같다.
문제 부분

Calculator with Dynamic Proxy

Calculator with CGLIB

Calculator 예제로 본 패키지 고찰(이 부분은 필자의 생각입니다.)

가상프록시가 적용된 PoCaMarketDynamic Proxy | CGLIB 적용하기 with Lazy Initialization

여기는 그냥 재미로 해본 겁니다.

뇌절의 삼절의 사절에 고찰 : Modern JAVA Style Calculator with Dynamic | CGLIB Proxy

애국가 5절 CGLIBLazyLoader, Interceptor의 이상한 사용법

원격 프록시

원격 프록시는 말 그대로 아래와 같은 아키텍처를 기준으로 설명하자면 원격 객체에 대한 대리자 역할을 하는 것을 의미한다. 즉. 아래의 아키텍처에서는 Stub을 원격 프록시라고 할 수 있다. 이 클라이언트는 스텁을 통하여 이용하여 java Server에 요청을 보내게 된다. 그럼 Skeleton이라는 java Server를 보조하는 객체가 이 요청을 해석하고 java Server에 특정 메서드를 실행시킬 수 있다. 이렇듯 프록시를 통하여 원격지의 메서드를 실행할 수 있다. 이보다 더 저수준의 제어가 Socket이다. 요기 아래의 참조를 통해 발전 과정을 알 수 있다.
처음에는 RMIGRPC이렇게 두가지 예제를 만들어 볼려고 했다. 그런데 RMI가 가지는 장점들이 GRPC가 가지는 장점들에 못미친다. 또한 RMI가 할 수 없는 일은 Socket이 할 수 있다고 한다. 그럼 RMI의 장점은 없는 것 같다(상대적으로). 그럼 기술을 선택할 때 고수준지원을 위해서는 GRPC를 선택하고, 저수준의 제어에서는 Socket을 선택하는 것이 더 좋은 선택지 일 것 같다. 그래서 원격 프록시에서 RMI라는 기술이 있구나. 정도만 하고 차라리 Socket을 이용한 코딩과 GRPC를 이용한 방식을 통하여 예제를 만들어 볼까 한다.

프록시 패턴 이해하기

프록시 패턴이란? 위키백과
일반적으로 프록시는 다른 무언가와 이어지는 인터페이스의 역할을 하는 클래스이다. 프록시는 어떠한 것(이를테면 네트워크 연결, 메모리 안의 커다란 객체, 파일, 또 복제할 수 없거나 수요가 많은 리소스)과도 인터페이스의 역할을 수행할 수 있다.
프록시라는게 원본객체를 감싸서 기능을 제공해주는 역할을 하구나. 특징
기본, 가상, 보호, 동적, 원격 등의 프록시가 존재한다.
기본적으로 감싼다는 개념을 통해서 보호와 가상을 이루고 있구나.
클라이언트는 프록시를 사용하고 있는지 모른다.
동일한 인터페이스인 Subject로 접근하니깐 뭘하는지 모르구나.