프록시는 클라이언트나 대상 사이의 대리 또는 접근 방법의 제어를 말하는 오브젝트를 일컫을 때 사용하는 단어입니다.
디자인 패턴 중 프록시 패턴이 있습니다.
디자인 패턴의 프록시는 타켓의 기능을 추가하지 않습니다. Spring을 다루어 보신 분이라면 AOP, intercepter, filter 등을 사용해보셨을 텐데요.
이 기능들의 공통점은 오브젝트에 접근할 때 먼저 처리해야할 점 또는 나중에 처리해야할 점의 설정을 도와준다는 점이죠,
타켓에 접근하는 방식을 변경해주는 역할을 합니다.
프록시 패턴이 이와 비슷합니다.
프록시 패턴을 이해하기 위해서는 먼저 리플렉션에 대한 이해가 필요합니다.
리플렉션이란, 오브젝트에 대한 구체적인 클래스 정보 없이도 그 오브젝트가 가진 변수, 메소드 등에 접근할 수 있게 하는 자바 API를 말합니다.
예제 코드를 한번 볼까요
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class Point {
public static class Point2 {
int x = 1;
public int getX() {
return x;
}
}
public static void main(String[] args)
throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Object point2 = new Point2();
// point2.getX(); - 호출 불가
Method getX = Point2.class.getMethod("getX"); // 호출 가능
System.out.println(getX.invoke(point2)); // return 값: 1
}
}
저번에 사용했던 클래스 재활용이라 깔끔하지는 않습니다.
main 메소드를 보시면 Point2 객체를 Object 형식으로 저장하고 있습니다.
모든 객체는 Object를 상속하니 다형성으로 인해 이 부분은 문제가 없습니다.
하지만 그 Object 값으로 getX 메소드를 호출할 수는 없습니다. Object 입장에서는 클래스 타입이 뭔지 알 수가 없으니까요.
이 부분을 구체적인 메소드 정보를 찾아서 부여함으로써
메소드를 실행시킬 수 있습니다.
Point2.class.getMethod("getX"); 를 통해 메소드 정보를 찾아온 후
메소드의 invoke(Object 변수) 메소드를 통해 메소드를 유발(?) 시킬 수 있습니다.
이처럼 구체적 클래스 정보를 몰라도, 실제 정보를 대입함으로서 우회적으로 정보를 사용할 수 있습니다.
리플렉션은 이러한 점 때문에 편리하지만, 그 사용이 주의되는 API입니다.
throw에 굉장히 간단한 코드임에도 처리하는 Exception이 많은 걸 보시면 알 수 있으실겁니다.
더 자세한 리플렉션 정보는 다음 링크를 참조하도록 합시다.
http://gyrfalcon.tistory.com/entry/Java-Reflection
https://docs.oracle.com/javase/tutorial/reflect/
다시 프록시 패턴으로 돌아가봅시다.
인터넷에 맘에 드는 그림이 없어서 만들어봤습니다..
프록시는 클라이언트가 특정 개체에 접근할 때의 행동에 영향을 줍니다.
예를 들어 단순히 숫자를 리턴하는 메소드가 있다고 해봅시다.
1, 2, 3 4 ...
이 메소드를 핸들러를 이용해서 리턴값을 처리해주면
1번, 2번, 3번, 4번 ...
으로 결과값을 조정해줄 수가 있습니다.
이 핸들러는 리플렉션 API를 사용하고요.
예제코드를 한번 볼까요.
예제코드의 패키지는 이렇게 생겼습니다.
Hello는 타겟을 묶어줄 인터페이스입니다.
HelloTarget은 Hello를 구현한 구체적 정보를 담고 있는 클래스입니다.
ProxyField는 프록시 처리를 한 변수를 관리하는 클래스입니다.
TestHandler는 리플렉션API를 사용해 정보 조정을 돕는 InvocationHandler를 구현한 핸들러 클래스입니다.
public interface Hello {
String sayHello(String name);
String sayHi(String name);
String others(String others);
String others2(String others2);
}
package proxy;
public class HelloTarget implements Hello {
@Override
public String sayHello(String name) {
return "Hello " + name;
}
@Override
public String sayHi(String name) {
return "Hi " + name;
}
@Override
public String others(String others) {
return "others " + others;
}
@Override
public String others2(String others2) {
return "others2 " + others2;
}
}
package proxy;
import java.lang.reflect.Proxy;
public class ProxyField {
// 매개변수 3개
Hello proxiedHello = (Hello) Proxy.newProxyInstance( // 구현할 인터페이스에 정보 있으니 캐스팅 OK
getClass().getClassLoader(), // 동적으로 생성되는 다이나믹 프록시 클래스의 로딩에 사용될 클래스 로더
new Class[] {Hello.class}, // 구현할 인터페이스
new TestHandler(new HelloTarget()) ); // InvocationHandler 구현 + 부가기능 (Target 클래스)
public Hello getProxiedHello() {
return proxiedHello;
}
}
살펴보면 보면 별거 아닌데, 좀 복잡하네요.
인터페이스와 타겟은 보시면 아시겠지만 굉장히 간단합니다.
프록시 필드는 위에서 보셨던 Proxy 원을 만드는 작업을 합니다.
즉, 타겟 메소드의 변수에 접근할 때 거쳐가는 프록시 오브젝트를 만드는 곳입니다.
이 다이나믹 프록시는 프록시 팩토리에 의해 다이나믹하게 생성되는 객체입니다.
타겟의 인터페이스와 같은 타입으로 만들어지며
클라이언트는 타겟 인터페이스를 통해 이 오브젝트에 접근이 가능합니다
프록시 오브젝트를 만들 때는 몇몇 정보가 필요합니다.
매개변수로 사용될 정보들이 그것인데요.
객체를 생성할 때 필요한 클래스 로더, 구현할 인터페이스 정보, 타겟 객체를 포함한 InvocationHandler(TestHandler) 입니다.
TestHandler를 보시면 InvocationHandler를 구현하고 있습니다.
이 InvacationHandler가 가지고 있는 메소드는 Invoke 메소드 하나인데
이 메소드는 나중에 프록시 오브젝트가 타겟오브젝트가 사용할 정보를 전달해주는 역할을 합니다.
package proxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class TestHandler implements InvocationHandler {
Hello target;
public TestHandler(Hello target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if(method.getName().equals("sayHi")){
String plus2 = (String) method.invoke(target, args);
// target의 메소드(sayHi)에 args(매개변수들)를 넣고 invoke(유발) 한다
return plus2+ " 2"; // 부가작업( 2 추가 )
}
if(method.getName().startsWith("sayHell")){
String plus5 = (String) method.invoke(target, args);
return plus5+" 5";
}
String others = (String)method.invoke(target, args);
return others + " 그 외 메소드";
}
}
Invoke를 보시면 매개변수는 proxy, method, args 세가지 입니다.
proxy는 proxy 자기자신을 매개변수르 넘겨줍니다. method는 프록시 객체를 통해 들어온 메소드 정보를 넘겨주며, args 배열은
그 메소드의 매개변수들을 의미합니다.
메소드는 문자열을 기준으로 위와 같이 if문 처리를 통해 개별처리가 가능하며
맨 아래처럼 그 외의 메소들을 모두 처리하는 방식도 가능합니다.
테스트를 한번 해볼까요
Hello는 5자니 5를 붙이고
Hi는 2자니 2를 뒤에 붙여보겠습니다.
테스트 코드는 아래와 같습니다.
package proxy;
public class ClientTest {
public static void main(String[] args){
ProxyField field = new ProxyField();
System.out.println(field.getProxiedHello().sayHello("헬로오으우"));
System.out.println(field.getProxiedHello().sayHi("하이"));
System.out.println(field.getProxiedHello().others("||"));
System.out.println(field.getProxiedHello().others2("&&"));
}
}
잘 되는 모습을 확인할 수 있습니다.
마지막 sysout은 invoke에 구체적 메소드 정보가 없어도
일괄적으로 처리될 수 있음을 보여주기 위해 테스트 해봤습니다.
'프로그래밍 > Java' 카테고리의 다른 글
이펙티브 자바 규칙 15 - 변경 가능성을 최소화하라 (0) | 2018.02.12 |
---|---|
디자인 패턴 - 데코레이터 패턴(Decorator Pattern) (0) | 2018.02.12 |
이펙티브 자바 규칙 13 - 클래스와 멤버의 접근 권한은 최소화하라 (0) | 2018.02.10 |