-
[Java] 프록시 패턴이란?Java 2022. 4. 8. 02:50728x90
프록시 패턴이란?
디자인 패턴 중 하나로, 어떤 클래스를 직접 사용하는 대신에 프록시 클래스를 통해 사용하는 것을 의미합니다.
사장님에게 바로 보고하지 않고 비서에게 보고하면 비서가 사장님에게 보고하는 것을 떠올리면 좋습니다.
프록시(Proxy)란?
프록시란 프록시 서버, 프록시 객체, 프록시 패턴 등등 소프트웨어 기술에서 종종 등장하는 용어입니다.
'대리', '중개'라는 의미를 가진 단어로써 예를 들어 프록시 서버라고 한다면 클라이언트와 서버 사이에 들어가 있어서 '클라이언트 -> 프록시 서버 -> 서버'처럼 중간에 대리인/중개인처럼 사용됩니다.
따라서 프록시 패턴이란 인터페이스를 사용하고 실행시킬 클래스에 대해 객체가 들어갈 자리에 프록시(대리자) 객체를 투입하여 클라이언트는 실제 실행시킬 클래스에 대해 메서드를 호출하여 받는지/ 프록시 객체의 메소드를 호출해서 반환 값을 받는지 모르게 하는 것을 의미합니다.
클라이언트는 Subject라는 인터페이스의 request() 메서드를 호출했을 때 RealSubject 클래스에서 해당 메서드가 호출되었는지 Proxy 클래스에서 해당 메서드가 호출되었는지 알 수 없습니다.
프록시 패턴 사용법
위의 예제처럼 실제로 코드를 구현해 보겠습니다.
Subject 인터페이스
public interface Subject { String request(); }
RealSubJect 클래스
public class RealSubject implements Subject { @Override public String request() { return "HelloWorld"; } }
Proxy 클래스
public class Proxy implements Subject { private final RealSubject realSubject = new RealSubject(); @Override public String request() { return realSubject.request(); //프록시가 실제의 메소드를 호출한다. } }
메인 클래스
public class Main { public static void main(String[] args) { // Subject클래스의 메소드를 호출하는것이아닌 프록시클래스의 메소드를 호출한다. Subject subject = new Proxy(); System.out.println(subject.request()); // 내부적으로 Subject의 메소드를 호출한다. } }
프록시 패턴을 사용하는 이유는?
굳이 불편하게 Proxy 클래스를 만들어서 거기에 멤버 변수로 RealSubJect 클래스를 만들고 메서드를 호출하는 이유가 뭘까요?
1. 흐름을 제어할 수 있다.
중요한 것은 흐름 제어만 할 뿐 결괏값을 조작하거나 변경시키면 안 됩니다.
2. 실제 메서드가 호출되기 이전에 필요한 기능을 구현 객체 변경 없이 추가할 수 있다. (AOP가 떠오르네요)
public class RealSubject implements Subject { @Override public String request() { return "HelloWorld"; } }
만약 request() 메서드의 수행 시간을 측정하고 싶은데 main 클래스 코드나 RealSubject 클래스를 수정하지 않고 측정하려면 프록시 패턴을 사용하여 해결할 수 있습니다.
public class Proxy implements Subject { private final Subject realSubject = new RealSubject(); @Override public String request() { long before = System.currentTimeMillis(); String temp = realSubject.request(); //프록시가 실제의 메소드를 호출한다. System.out.println(System.currentTimeMillis() - before); return temp; } }
프록시 클래스에서 realSubject.request()를 수행하는데 이때 수행시간을 측정함으로써 main 클래스나 코드나 RealSubject 클래스를 수정하지 않고 request() 메서드의 수행시간을 측정할 수 있습니다.
3. 캐싱을 사용할 수 있다.
public class Proxy implements Subject { private final subject realSubject; String cache; @Override public String request() { if(realSubject == null){ realSubject = new RealSubject(); } if(cache = null){ cache = realSubject.request(); return cache; } return cache; } }
cache를 활용하여 값이 존재하지 않으면 값을 받아오고 값이 존재한다면 그냥 그 값을 그대로 반환함으로써 캐시로 사용할 수 있습니다.
4. 생성 자원이 많이 드는 작업에 대해 백그라운드 처리
public class Proxy implements Subject { private final subject realSubject; @Override public String request() { if(realSubject == null){ realSubject = new RealSubject(); } return realSubject.request(); //프록시가 실제의 메소드를 호출한다. } }
실제로 사용되기 전에 미리 객체를 생성하기 않고 실제로 사용될 때 한번 생성함으로써 이점을 가질 수 있습니다.
5. 실제 오브젝트를 요청하기 전이나 후에 인가 처리(보안/보호)
public class Proxy implements Subject { private final Subject realSubject = new RealSubject(); @Override public String request() { //실제 메서드를 호출하기 직전에 인증/인가 확인 if(!isValid()){ return "CANT RUNNING"; } return realSubject.request(); //프록시가 실제의 메소드를 호출한다. } }
실제 메서드를 수행하기 전에 보안을 위하여 인증/인가를 확인하고 메서드를 수행할 수 있습니다.
프록시 패턴의 단점/문제점
- 로직이 난해해져 가독성이 떨어질 수 있습니다. (단순하게 코드가 복잡해짐)
- 인터페이스를 직접 구현해야 합니다. (예를 들어 1가지 메서드를 확장하고 싶더라도 인터페이스를 모두 구현해야 하기 때문에 3개의 메서드를 구현해야 할 수 있습니다.)
- 프록시 클래스 내에 중복이 발생합니다.
위의 코드는 세 개의 메서드 모두 리턴하는 문자열을 대문자로 바꾸어주는 똑같은 일을 합니다.
하지만 다이나믹 프록시를 사용한다면 인터페이스를 직접 구현해야 하는 단점과 프록시 클래스 내에 중복이 발생하는 문제점을 해결할 수 있습니다.
다이나믹 프록시
인터페이스의 구현체로 프록시를 구현할 수도 있지만 자바에서 런타임시에 인스턴스를 동적으로 만들 수 있는 리플렉션이라는 클래스를 제공합니다.
리플렉션의 Proxy 클래스가 동적으로 프록시 객체를 생성해 주므로 JDK Dynamic Proxy라는 이름이 붙여졌습니다.
프록시 객체 생성 과정
Object proxy = Proxy.newProxyInstance(ClassLoader // 클래스로더 , Class<?>[] // 타겟의 인터페이스 , InvocationHandler // 타겟의 정보가 포함된 Handler );
예제 코드
public interface MyInterface { void execute(String message); } public class MyClass implements MyInterface{ @Override public void execute(String message) { System.out.println(message); } } public class MyInvocationHandler implements InvocationHandler { private MyClass target; public MyInvocationHandler(MyClass target) { this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("Before"); method.invoke(target , args); System.out.println("After"); return null; } } public class ProxyInJava { public static void main(String[] args) { MyClass myClass = new MyClass(); MyInvocationHandler myInvocationHandler = new MyInvocationHandler(myClass); MyInterface myInterface = (MyInterface) Proxy.newProxyInstance(MyInterface.class.getClassLoader(), new Class[]{MyInterface.class} ,myInvocationHandler); myInterface.execute("JDK Dynamic Proxy"); // 프록시 객체 myClass.execute("Simple"); //출력 //Before //JDK Dynamic Proxy //After //Simple } }
프록시 객체로 실행하면 메서드와 before와 after가 수행되고 그냥 실행할 경우에는 메서드만 실행됩니다.
InvocationHandler란?
InvocationHandler는 invoke()라는 메서드 하나만 가지고 있는 인터페이스입니다.
invoke() 메서드는 다이나믹하게 생성될 프록시의 어떤 메서드든 호출되었을 때 호출되는 메서드입니다.
invoke() 메서드를 통해 확장된 기능을 구현할 수 있습니다.
여기서는 myInterface에 execute() 메서드만 존재했지만 만약에 execute1(), execute2() 메서드가 추가로 존재했을 때 어떤 메서드를 호출하던 invoke() 메서드가 수행되며 확장된 기능이 구현됩니다.
CGLib Proxy
순수 Java JDK 라이브러리를 이용하지 않고 CGLIB라는 외부 라이브러리를 추가해야만 사용할 수 있습니다.
CGLIB의 Enhancer 클래스를 바탕으로 Proxy를 생성하기 때문에 인터페이스가 없어도 Proxy를 생성할 수 있습니다.
하지만 상속을 이용하기 때문에 final, private와 같이 상속에 대해 오버라이딩을 지원하지 않는 경우에는 적용할 수 없습니다.
또한 바이트 코드를 조작해서 프록시 객체를 생성하므로 다이나믹 프록시보다 빠릅니다.
CGLib를 사용하여 프록시를 생성할 때 크게 두가지 작업으로 수행합니다.
- MethodInterceptor 방식(CGLib의 성능을 활용하기 위해서는 MethodInterceptor 일반적으로 사용)
- InvocationHandler 방식(다이내믹 프록시와 마찬가지로 리플랙션 활용)
어디에 쓰이나요?
이러한 기능을 추상화시켜서 스프링에서는 AOP(Aspect Oriented Programming)이라는 개념으로 제공합니다.
또한 JPA에서 지연 로딩할 때도 사용합니다.
출처
https://www.baeldung.com/java-proxy-pattern
https://esoongan.tistory.com/180
https://www.youtube.com/watch?v=tes_ekyB6U8(백기선님의 프록시 패턴 1부~4부)
https://live-everyday.tistory.com/217
https://steady-coding.tistory.com/608
https://velog.io/@suhongkim98/JDK-Dynamic-Proxy%EC%99%80-CGLib
'Java' 카테고리의 다른 글
객체지향 5원칙 : SOLID (0) 2022.04.22 [Java] 빌더 패턴 (0) 2022.04.09 [Java] 리플렉션이란? (0) 2022.04.07 [Java] 직렬화(Seralize)란? (0) 2022.03.24 [Java] Integer.parseInt() vs Integer.valueOf() (0) 2022.03.20