ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 프록시 패턴
    BackEnd/자바 2020. 12. 20. 20:05

    실제 기능을 수행하는 객체 Real Object 대신 가상의 객체 Proxy Object를 사용해 로직의 흐름을 제어하는 디자인 패턴.

     

    • 원래 하려던 기능을 수행하며 그외의 부가적인 작업(로깅, 인증, 네트워크 통신 등)을 수행하기에 좋다. 
    • 비용이 많이 드는 연산(DB 쿼리, 대용량 텍스트 파일 등)을 실제로 필요한 시점에 수행할 수 있다. 
    • 사용자 입장에서는 프록시 객체나 실제 객체나 사용법은 유사하므로 사용성이 좋다. 

     

    만약 책을 대여하고 반납하는 메서드에서 다른 부가 작업을 수행하고 싶다고 하면, 

    BookService 인터페이스를 이용해 정적인 방식, 동적인 방식으로 프록시 클래스를 생성할 수 있다.

    public class Book {
    	private String title;
    	public String getTitle() {
    		return title;
    	}
    	public void setTitle(String title) {
    		this.title = title;
    	}
    }
    
    public interface BookService {
    	void rent(Book book);
    	void returnBook(Book book);
    }
    

     

    1. 정적으로 프록시 클래스를 구현 

    BookService 인터페이스를 상속한 Proxy 클래스 직접 생성 . 

    이 방법은 클래스마다 프록시 클래스를 구현해야하기 때문에 비효율적이다.

    public class BookServiceProxy implements BookService {
    	BookService bookService;
    	
    	public BookServiceProxy(BookService bookService) {
    		this.bookService = bookService;
    	}
    	
    	@Override
    	public void rent(Book book) {
    		System.out.println("rent book");
    		System.out.println("do another job");
    		bookService.rent(book);		
    	}
    
    	@Override
    	public void returnBook(Book book) {
    		System.out.println("return book");
    		System.out.println("do another job");
    		bookService.returnBook(book);
    	}
    }
    

     

    2.  동적으로 프록시 클래스를 구현 

    BookService 인터페이스를 상속한 service 클래스 생성.

    자바 reflect패키지에 있는 Proxy 와 InvocationHandler 를 이용해 동적으로 프록시 클래스를 생성한다.

    public class DefaultBookService implements BookService {
    
    	@Override
    	public void rent(Book book) {
    		System.out.println("defualt rent " + book.getTitle());
    	}
    
    	@Override
    	public void returnBook(Book book) {
    		System.out.println("defualt return " + book.getTitle());
    	}
    }
    
    Proxy.newProxyInstance( ClassLoader loader, // 프록시 클래스를 정의하는 클래스 로더
                           Class[] interfaces, // 프록시 클래스가 수현하는 인터페이스 리스트
                           InvocationHandler handler // 메서드 호출을 처리하는 핸들러 
                          );
    
    BookService bookService = (BookService) Proxy.newProxyInstance(
                BookService.class.getClassLoader()
                , new Class[] { BookService.class }
                , new InvocationHandler() {...})
    
    /* 
    Interface InvocationHandler
    
    invoke(Object proxy, Method method, Object[] args)
    Processes a method invocation on a proxy instance and returns the result.
    */ 
               
    /*            
    Parameters:
    proxy - the proxy instance that the method was invoked on
    메서드가 호출된 프록시 인스턴스
    
    method - the Method instance corresponding to the interface method invoked on the proxy instance. 
    The declaring class of the Method object will be the interface that the method was declared in, 
    which may be a superinterface of the proxy interface that the proxy class inherits the method through.
    프록시 인스턴스에 호출된 인터페이스의 메서드에 해당하는 메서드 인스턴스
    
    args - an array of objects containing the values of the arguments passed in the method invocation on the proxy instance, 
    or null if interface method takes no arguments. Arguments of primitive types are wrapped in instances of the appropriate primitive wrapper class, 
    such as java.lang.Integer or java.lang.Boolean.
    객체의 매개변수를 배열로 담고 있음
    */

                                     

    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Method;
    import java.lang.reflect.Proxy;
    
    import org.junit.Test;
    
    public class BookServiceTest {
    //2.런타임에 동적으로 프록시 클래스 구현하는 방법 
    	BookService bookService = (BookService) Proxy.newProxyInstance(
                BookService.class.getClassLoader()
                , new Class[] {BookService.class}
                , new InvocationHandler() {
    		
                    BookService bookservice = new DefaultBookService();
    			
    		@Override
    		public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    			
    			if(method.getName().equals("rent")) {
    				System.out.println("dynamic rent");
    				System.out.println("do another job");
    				Object invoke = method.invoke(bookservice, args);			
    				return invoke;
    			}
    			
    			return method.invoke(bookservice, args);
    		}
    	});
    	
    	@Test
    	public void di() {
    		Book book = new Book();
    		book.setTitle("lala");
    		bookService.rent(book);
    		bookService.returnBook(book);
    	}
    }

     

    자바 프록시의 단점

    1. 유연성이 떨어진다. method.getName()로 분기처리를 통해 할 수 있다. 이는 코드의 양이 방대해질 수 있다는 것을 의미한다. 

     

    2. 정적, 동적인 방법 모두 인터페이스인 BookService 를 통해 프록시 클래스를 생성했다.

     이는 자바 프록시 생성이 클래스 기반의 프록시를 만들지 못하기 때문에 반드시 인터페이스로 만들어야하기 때문이다.

     인터페이스가 없는 경우 클래스 기반의 프록시를 만들기 위해 CGlib 나 ByteBuddy 를 이용할 수 있다.


    public class BookServiceClass {	
    	public void rent(Book book) {
    		System.out.println("rent " + book.getTitle());
    	}
    
    	public void returnBook(Book book) {
    		System.out.println("return " + book.getTitle());
    	}	
    }

     

    1. CGlib (스프링, 하이버네이트에서 사용 ) 

        <dependency> 
              <groupId>cglib</groupId> 
              <artifactId>cglib</artifactId> 
              <version>3.3.0</version> 
       </dependency> 
    MethodInterceptor handler = new MethodInterceptor(){ 
          BookService bookService = new BookService(); 
          @Override 
          public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { 
              return method.invoke(bookService,   objects); 
          } 
     }; 
    
    BookService bookService = (BookService) Enhancer.create(BookService.class, handler); 

     

    2. ByteBuddy 이용   
    인스턴스를 바로 만들어주지 않고 클래스를 만들어야한다.
    자바가 제공하는 다이나믹 프록시에 프록시 클래스를 만드는 방법과 비슷하다.

    <dependency>
    	<groupId>net.bytebuddy</groupId>
    	<artifactId>byte-buddy</artifactId>
    	<version>1.10.1</version>
    </dependency>
    import net.bytebuddy.ByteBuddy;
    import net.bytebuddy.implementation.InvocationHandlerAdapter;
    import net.bytebuddy.matcher.ElementMatchers;
    public class BookServiceTest2 {
    
    	//byteBuddy 사용해 프록시 서버 만들기 
    	@Test
    	public void di() throws Exception {
    		//프록시 클래스 만들어 가져오기 
    		Class<? extends BookServiceClass> proxyClass =
            		new ByteBuddy()
            		.subclass(BookServiceClass.class)
            		.make()
            		.load(BookServiceClass.class.getClassLoader()).getLoaded();
    		
    		//클래스 object 에서 instance 생성하기 
    		BookServiceClass bookService = proxyClass.getConstructor(null).newInstance();
    		
    		Book book = new Book();
    		book.setTitle("lala");
    		bookService.rent(book);
    		bookService.returnBook(book);
    		
    	}
    }
    

    이렇게만 하면 원래의 메세지만 나온다.

    아래와 같이  make() 이전에  method 지정과 intercept 를 통해 할 일을 지정한다.

    import net.bytebuddy.ByteBuddy;
    import net.bytebuddy.implementation.InvocationHandlerAdapter;
    import net.bytebuddy.matcher.ElementMatchers;
    public class BookServiceTest2 {
    
    	//byteBuddy 사용해 프록시 서버 만들기 
    	@Test
    	public void di() throws Exception {
    		//프록시 클래스 만들어 가져오기 
    		Class<? extends BookServiceClass> proxyClass =
                     new ByteBuddy()
                     .subclass(BookServiceClass.class)
                     .method(ElementMatchers.named("rent"))
                     .intercept(InvocationHandlerAdapter.of(new InvocationHandler() {
                        BookServiceClass bookservice = new BookServiceClass();
    					
                        @Override
                        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                            System.out.println("rent + another job");
                            Object invoke = method.invoke(bookservice, args);                        
                            return invoke;
                        }
                    }))                
                    .make()
                    .load(BookServiceClass.class.getClassLoader()).getLoaded();
    		
                    BookServiceClass bookService = proxyClass.getConstructor(null).newInstance();
    		
                    Book book = new Book();
                    book.setTitle("lala");
                    bookService.rent(book);
                    bookService.returnBook(book);		
    	}
    }
    

     

    상속을 통해 프록시를 만들 때 단점
    일부 클래스는 상속을 허용하지 않기 때문에 그럴 경우 사용이 불가하다.
    - final 클래스 
    - 부모의 생성자가 private 생성자만 있다면 자식클래스에서 부모의 생성자를 호출 못한다.

    * 가능하다면 제약사항 없는 인터페이스를 만들어 사용하기를 권한다.

    * 스프링 AOP 는 자바 다이나믹 프록시  + CGlib로 상속을 이용한 프록시 만들어 사용해준다.

     

     


    https://devidea.tistory.com/40

    반응형
Designed by Tistory.