엔지니어로 가는 길

자바를 통해 다이나믹 프록시(Dynamic Proxy)를 구현하는 방법 본문

프로그래밍/Java

자바를 통해 다이나믹 프록시(Dynamic Proxy)를 구현하는 방법

탐p슨 2020. 10. 26. 15:33
728x90

지난 글에서 프록시란 무엇인지 알아보았고, 간단한 프록시 예제를 하나 살펴보았다.

 

프록시(Proxy)란 무엇인지 자바 코드로 보자

 

프록시(Proxy)란 무엇인지 자바 코드로 보자

Proxy Proxy의 사전적 정의는 대리인이다. 자바에서 프록시는 타겟의 기능을 확장하거나 타깃에 대한 접근을 제어하기 위한 목적으로 사용하는 클래스를 말한다. 여기서는 기능을 확장하는 프록시

live-everyday.tistory.com

 

이번에는 지난 시간의 예제를 발전시켜 자바를 통해 다이나믹하게 프록시를 구현하는 방법을 살펴보도록 한다. '다이나믹하게'라는 뜻은 코드로 일일이 프록시를 만드는 것이 아니라 런타임에 프록시가 생성되는 것을 의미한다. 먼저 왜 다이나믹하게 프록시를 구현해야 하는지 살펴보자.

 

프록시 구현의 문제점

 

지난 시간과 같이 구현하면 두 가지 문제가 있다.

 

1. 인터페이스를 직접 구현해야 한다.

 

 

Hello의 구현체에 대한 프록시를 만들려면 Hello라는 인터페이스를 구현해야 한다. 즉, Hello의 모든 메소드를 구현해야 한다. 만약 sayHello() 메소드만 기능을 확장하고 싶다고 하더라도 sayHi()와 sayThankYou()를 모두 override해주어야 한다. override 함수가 하는 일이라곤 타겟의 메소드에 위임하는 것 뿐인데도 말이다.

 

2. 프록시 클래스 내에 중복이 발생한다.

 

 

HelloUppercase라는 프록시를 직접 만들었다. 코드를 보면 세 개의 메소드 모두 리턴하는 문자열을 대문자로 바꿔주는 똑같은 일을 한다. 같은 일을 하는 건 좋은데 문제는 중복이 보인다는 것이다. 별도의 메소드로 분리함으로써 중복을 제거하는 것을 고려해볼 수 있다. 하지만 대문자로 바꿔주는 추가 기능이 Hello의 구현체 뿐만 아니라 다른 클래스에서도 필요한 경우라면 중복을 제거하기 곤란하다.

 

다이나믹 프록시

 

다이나믹 프록시는 위의 두 문제를 해결한다. 다이나믹 프록시를 자바 코드를 통해 살펴보자. 자바에서는 reflection API를 통해 다이나믹 프록시 구현할 수 있다. 먼저 사용하는 코드부터 보자.

 

 

Java.lang.reflect.Proxy 클래스의 newProxyInstance() 메소드를 이용한다.

 

https://docs.oracle.com/javase/7/docs/api/java/lang/reflect/Proxy.html

 

첫 번째 인자: 프록시를 만들 클래스 로더

두 번째 인자: 어떤 인터페이스에 대해 프록시를 만들 것인지 명시

세 번째 인자: InvocationHandler 인터페이스의 구현체

리턴 값: 동적으로 만든 프록시 객체

 

일일이 구현해야한다는 문제는 reflection API가 해결해주고, 중복은 InvocationHandler가 해결해준다. InvocationHandler에 대해 살펴보자.

 

InvocationHandler

 

InvocationHandler는 invoke()라는 메소드 하나만 가지고 있는 인터페이스이다. invoke() 메소드는 다이나믹하게 생성될 프록시의 어떤 메소드든 호출됐을 때 호출되는 메소드로, 여기서 어떤 메소드에 기능을 확장할지 결정할 수 있고, 확장된 기능을 구현할 수도 있다. 그러니까 클라이언트가 sayHi() 메소드를 호출하든 sayHello()를 호출하든 sayThankYou()를 호출하든 invoke() 메소드에 걸리고 사용자가 어떤 메소드를 호출했는지에 대한 정보와 메소드에 전달한 인자는 invoke() 메소드의 인자로 전달된다.

 

 

첫 번째 인자: 프록시 객체

두 번째 인자: 메소드 객체(클라이언트가 호출한 메소드)

세 번째 인자: 메소드의 인자(클라이언트가 메소드에 전달한 인자)

 

본 예제에서는 먼저 타겟의 진짜 메소드를 호출하여 그 결과를 ret에 담아두고, ret이 문자열인 동시에 메소드의 이름이 'say'로 시작하면 ret을 대문자로 변환하여 리턴하고 그렇지 않은 경우라면 그냥 리턴하도록 구현하였다. 테스트해보자.

 

 

전체 과정을 다시 살펴보자.

 

 

1. 클라이언트는 Hello 타입의 객체에게 sayHi()를 요청한다. (클라이언트는 프록시인지 타겟인지 모른다.)

 

2. 다이나믹하게 만들어진 프록시 객체는 사용자가 요청한 메소드의 정보와 메소드의 인자를 InvocationHandler 구현체의 invoke() 메소드의 인자로 전달하여 호출한다.

 

3. InvocationHandler의 invoke() 메소드에서 타겟의 어떤 메소드에 적용할지 검사하고, 적용해야 하는 메소드라면 타겟의 원래 메소드를 호출하여 결과를 담아둔 뒤 추가 기능을 적용하여 리턴한다. 적용하지 말아야 하는 메소드라면 타겟의 원래 메소드의 리턴을 그대로 리턴한다.

 


 

이로써 프록시를 만들고 싶은 인터페이스를 일일이 구현해야하는 문제를 해결했다. Hello라는 인터페이스가 오만 개의 메소드를 가지고 있고, 이 중에 하나의 메소드만 기능을 확장하고 싶은 경우라도 문제 없다. 나머지 49999개의 메소드는 reflection API를 이용하여 알아서 구현해주기 때문이다.

 

또한 Hello에 적용한 기능 확장(대문자로 만들어 리턴하기)을 Yellow라는 인터페이스의 구현체에 적용하고 싶은 경우라도 문제 없다.

 

 

InvocationHandler가 중복을 해결하기 때문이다. 프록시의 newInstance()에 두 번째 인자로 Yellow 타입을 주고, 세 번째 인자로 Hello 프록시를 만들 때 사용했던 InvocationHandler 구현체 UppercaseHandler를 넘겨주면 Hello에서와 마찬가지로 Yellow에서 'say'로 시작하는 메소드만 대문자 문자열을 리턴하게 된다.

728x90
Comments