Home [Java] 프록시
Post
Cancel

[Java] 프록시

2월 2주차 스터디 발표 자료📖
2주차 기술면접 주제 중 세번 째 주제입니다!
프록시 개념에 대해 정리하고자 합니다.
참고(10분 테코톡): https://www.youtube.com/watch?v=MFckVKrJLRQ&t=345s

프록시란?


proxy

  • 클라이언트로부터 타깃(Real Object)을 대신해서 요청을 받는 대리인
  • 실제 오브젝트인 타깃은 프록시를 통해 요청을 받아 처리함
  • 타깃은 자신의 기능에만 집중하고 프록시에게 부가기능을 위임한다.
  • 프록시 패턴을 통해 Proxy를 직접 구현할 수 있다.

프록시 사용 목적

  1. 클라이언트가 타깃에 접근하는 방법을 제어하기위해
    ex. 지연로딩, 접근 제어(권한)
  2. 타깃에 부가적인 기능을 부여해줄 때
    ex. 시간을 측정하는 로직을 추가한다, Transactional 기능을 부여한다.

프록시 패턴의 장점

  1. OCP(개방폐쇠의 원칙)
    기존 코드를 변경하지 않고 새로운 기능을 추가할 수 있다.
  2. SRP(단일 책임의 원칙)
    기존 코드가 해야 하는 일만 유지할 수 있다. 즉, 부가적인 기능은 프록시에게 맡기면 된다.

프록시 패턴의 단점

  1. 코드의 복잡도가 증가한다.
  2. 중복 코드가 발생한다.

이러한 문제를 해결해주는 것이 동적 프록시이다.

프록시 패턴의 구조(예시)


proxy2

  • 프록시 패턴의 흐름
    1. 인터페이스를 선언하고
    2. 해당 인터페이스를 구현한 proxy 객체와 타깃(Real Object)객체를 생성한 뒤
    3. Client의 요청을 HelloProxy가 대신 처리하도록 구현한다.

동적 프록시의 종류?


1. JDK Dynamic Proxy

  • 프록시 클래스를 직접 구현하지 않아도 된다.
    코드 복잡도 해소
  • Invocation Handler
    Invocation Handler를 통해 중복 코드를 제거할 수 있다. proxy3
  • Invocation Handler 예시
    밑의 코드를 보면 Proxy 객체에서 toUpperCase()라는 중복 코드가 발생하고 있다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    
    public class HelloProxy implements Hello {
      private Hello hello;
        
      public HelloProxy(Hello hello){
          this.hello = hello;
      }
    
      @Override
      public String sayHello(String name){
          // 1. 기존 sayHello 메서드에 대문자로 출력하는 부가 기능을 추가
          return hello.sayHello(name).toUpperCase();
      }
    
      @Override
      public String sayHi(String name){
          // 2. toUpperCase() 라는 중복 코드 발생
          return hello.sayHi(name).toUpperCase();
      }
    
      @Override
      public String sayThankYou(String name){
          // 3. toUpperCase() 라는 중복 코드 발생
          return hello.sayThankYou(name).toUpperCase();
      }
    }
    

    위의 문제점은 InvocationHandler를 재정의하여 해결할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.lang.reflect.Method;

public class UpperCaseHandler implements InvocationHandler {
    
    // 부가 기능을 제공할 타킷(Real Object)
    private final Object target;
    
    // 다이나믹 프록시로부터 전달받은 요청을 
    // 다시 타깃 오브젝트로 위임해야하므로, 타깃 오브젝트 주입받는다.
    public UpperCaseHandler(Object target)}{
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if(method.getName().startsWith("say")){
            return ((String) method.invoke(target,args)).toUpperCase(); //타깃에게 위임
        }
        return method.invoke(target,args); //타깃에게 위임
    }
}

JDK Dynamic Proxy의 동작과정

proxy4

  1. 클라이언트가 메소드를 요청한다.
  2. JDK Dynamic Proxy는 Invocation Handler에 메소드 처리를 위임한다.
  3. Invocation Handler는 부가 기능을 수행 후 Target에게 메소드 처리를 위임한다.

JDK Dynamic Proxy의 특징

  • JDK에서 지원하는 프록시 생성 방법
  • Reflection API를 사용하기때문에 느리다.
  • 인터페이스가 반드시 있어야한다.(프록시 생성 시, 반드시 필요함)
  • ⭐ Invocation Handler를 재정의한 invoke를 구현해줘야 부가기능이 추가된다.

  • JDK Dynamic Proxy 구현 예시
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    
    class ProxyTest{
      @Test
      void jdkProxy(){
          OrderService orderService = (OrderService) Proxy.newProxyInstance(
              ProxyTest.class.getClassLoader(), // 프록시 로딩에 사용할 클래스로더
              new Class[]{OrderService.class}, //타깃의 interface
              new LoggingHandler(new OrderServiceImpl()); // 부가 기능과 위임할 타깃(RealObject)
          );
    
          orderService.getOne(1L);
      }
    }
    

2. CGLIB

  • 스프링의 동적 프록시 생성 방법
    spring-proxy
    1. Client가 메소드를 요청하면
    2. Proxy Factory Bean에서 메서드에 대한 인터페이스 유무를 판단하고
    3. Interface가 존재하면 JDK Dynamic Proxy 방식으로 프록시 생성
    4. Interface가 없다CGLIB 방식으로 프록시 생성(클래스만 있어도 작동)

  • 🤔 근데 Spring은 Interface가 존재해도 CGLIB 방식으로 작동한다. 그 이유는?
    스프링에서 default로 proxy-target-class 옵션을 true로 설정해놨기때문.
    false로 변경하면 위의 방법처럼 작동한다.

CGLIB의 특징

  • ⭐ 상속을 통한 프록시 구현 ➔ 클래스에 final을 붙이면 정상동작하지 않는다.
  • 바이트 코드를 조작해서 프록시 생성 - Reflection Api보다 빠르다.
  • MethodInterceptor를 재정의한 Intercept를 구현해야 부가기능이 추가된다.
  • 메소드에 final을 붙이면 오버라이딩이 불가능하다.

  • MethodInterceptor 예시
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
    public class LoggingMethodInterceptor implements MethodInterceptor{
        
      // 부가 기능을 제공할 타킷(Real Object)
      private final Object target;
      private static final Logger log = LoggerFactory.getLogger(LoggingMethodInterceptor.class);
    
      public LoggingMethodInterceptor(Object target){
          this.target = target;
      }
    
      @Override
      public Object intercept(Object obj, Method method, Object[] args, MethodProxy methodProxy){
          if(method.getName().startsWith("get")){
              log.info("Excuting {} method : {}",method.getName(),"조회 메서드 호출!!");
              return methodProxy.invoke(target, args);
          }
          log.info("Executing {} method: {}",method.getName(),"조회가 아닌 메서드 호출!!");
          return methodProxy.invoke(target,args);
      }
    }
    
  • CGLIB 구현 예시 코드
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
    public class CglibTest {
      @Test
      void cglibProxyWhenGet(){
          ProductService productService = (ProductService)Enhancer.create(
              ProductService.class,
              new LoggingMEthodInterceptor(new ProductService()) // 부가기능과 위임할 타깃
          );
    
          productService.getOne(1L);
      }
    }
    

CGLIB의 동작과정

CGLIB

  1. 클라이언트가 메소드를 요청한다.
  2. CGLIB은 Method Interceptor에 메소드 처리를 위임한다.
  3. Method Interceptor는 부가 기능을 수행 후 Target에게 메소드 처리를 위임한다.

Spring의 프록시 구현?

  • Spring에서는 프록시를 Bean으로 만들어주는 ProxyFactoryBean을 제공한다.
  • ProxyFactoryBean을 통해 Proxy를 생성할 수 있음.

ProxyFactoryBean의 주요 특징

  • 스프링에서 지원하는 프록시 생성 방법
  • 프록시 빈을 생성해준다
  • 프록시 생성 시, 타깃의 인터페이스 정보가 필요없다..
  • 부가기능을 MethondInterceptor를 재정의하여 구현한다.CGLIB의 MethodInterceptor와는 다른 개념

ProxyFactoryBean의 프록시 생성 과정

CGLIB

  1. 프록시를 생성한다.
  2. 프록시가 Method Interceptor에게 메소드 처리를 위임한다.
  3. MethodInterceptor는 부가기능을 수행하고 Target에게 메소드 처리를 위임한다.

🤔스프링에선 왜 Invocation Handler가 아닌 MethodInterceptor를 사용할까?

JDK-Target

  1. Invocation Handler?
    타겟(실제 객체)을 필드로 직접 가지고있기때문에, 부가 기능을 독립적으로 유지할 수 없다(타겟에 의존적임)
    ➔ 타겟이 늘어난다면, 같은 기능을 사용하더라도 그 개수만큼 InvocationHandler를 빈으로 매번 등록하고 객체로 생성해야한다.
  2. Method Interceptor?
    타겟을 직접 가지고있지않고 Proxy가 가지고있다.
    따라서 부가기능을 독립적으로 유지할 수 있다.(싱글톤으로 공유하여 사용이 가능함)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    
     @Test
     public void proxyFactoryBean() {
            
         ProxyFactoryBean pfBean = new ProxyFactoryBean();
         pfBean.setTarget(new HelloTarget());
            
         NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
         pointcut.addMethodName("sayH*");
            
         pfBean.addAdvisor(new DefaultPointcutAdvisor(pointcut, new UppercaseAdvice()));
            
         Hello proxiedHello = (Hello)pfBean.getObject();
            
         assertThat(proxiedHello.sayHello("Toby"), is("HELLO, TOBY"));
         assertThat(proxiedHello.sayHi("Mindy"), is("HI, MINDY"));
         assertThat(proxiedHello.sayThankyou("Jack"), is("Thank you, Jack"));        
     }
        
     static class UppercaseAdvice implements MethodInterceptor {
            
         public Object invoke(MethodInvocation invocation) throws Throwable {
                
             String ret = (String)invocation.proceed();
             return ret.toUpperCase();
         }
     }
    
This post is licensed under CC BY 4.0 by the author.