본문 바로가기
개발 이론/Spring

프록시??? 스프링 AOP?? 트랜젝션?? (2)

by dal_been 2024. 3. 2.
728x90

지난 블로그에서 프록시와 프록시 패턴에 대해서 알아보았다.
간단하게 정리해보자면 프록시를 사용하는 이유는 원본 객체를 수정할 수 없는 상황을 극복하기 위해서였다.
예를 들어 A라는 클래스를 수정할 수 없다고 해보자. 근데 A라는 클래스 앞에 x라는 기능을 넣고 싶다. 이럴때 프록시라는 것을 통해 A수정없이 x라는 기능을 추가할 수 있다.
 
그러니까 프록시를 이용해 부가적인 기능을 부여(트랜젝션, 시간측정등), 또는 타깃에 대한 접근방법 제어(지연로딩)일때 사용된다.
 
다만 프록시 패턴을 사용하면 
- 인터페이스를 구현해서 프록시 객체를 생성해야하는 코드 복잡도 증가
- 모든 메서드에 부가기능 구현 해야하는 중복코드 발생
과 같은 단점이 존재한다.
 
그래서 나온게 동적 프록시이다
 


동적 프록시

 
프록시의 단점 해결하고 
스프링에서 클라이언트가 메서드를 요청하면 ProxyFactoryBean에서 인터페이스 유무를 확인하여 JDK Dynamic Proxy를 호출하여 없으면 CGLIB방식으로 프록시를 생성한다.

 

JDK Dynamic Proxy

  • JDK에서 지원하는 프록시 생성 방법
    • 외부 라이브러리에 의존하지 않는다
  • 프록시 팩토리에 의해 런타임 시 다이나믹하게 만들어지는 오브젝트
  • 프록시 팩토리에게 인터페이스 정보만 제공해주면 해당 인터페이스를 구현한 클래스 오브젝트를 자동으로 생성
  • Reflection API를 사용한다. (느리다)
  • 인터페이스가 반드시 존재해야한다
  • Invocation Hanlder를 재정의한 invoke 코드를 직접 구현해줘야 부가기능이 추가된다

프록시 단점 해소

  • 프록시 클래스를 직접 구현하지 않아도 된다
    • 코드 복잡도 해소
  • InvocationHandler
    • 중복 코드 제거

구현해보자면 클라이언트가 Sleep의 wantSleep을 요청한다고 해보자

public interface Sleep {
  void wantSleep();
}

public class SleepImpl implements Sleep{
  @Override
  public void wantSleep() {
    System.out.println("자고 싶어!!!!!!");
  }
}

 
InvocationHandler 구현

public class Invocation implements InvocationHandler {

  Object target;

  public Invocation(Object target) {
    this.target = target;
  }

  @Override
  public Object invoke(Object proxy , Method method , Object[] args) throws Throwable {
    if(method.getName().contains("wantSleep")){
      System.out.println("sleep");
    }
    return method.invoke(target,args);
  }
}

 
테스트코드 Jdk Dynamic Proxy 사용해서

public class ProxyTest {

  @Test
  void test(){
    Sleep sleep = (Sleep) Proxy.newProxyInstance(
        Sleep.class.getClassLoader(),
        new Class[]{Sleep.class},
        new Invocation(new SleepImpl())
    );

    sleep.wantSleep();
    System.out.println(sleep.getClass());
  }
}
  • Proxy.newProxyInstance를 사용해서 프록시 로딩에 사용할 클래스 로더, 타켓 인터페이스, 부가기능과 위임할 타겟 인자로 생성하여 JDK Proxy 사용 가능함
  • 만약 인터페이스가 아닌 인터페이스 구현체를 넣으면 런타임 에러가 발생한다

보면 Invocation 에 있는 System.out -> SleepImpl에 있는 System.out이 나오는 것을 볼 수 있다.
 
 

CGLIB Proxy

  • 프록시 팩토리에 의해 런타임 시 다이나믹하게 만들어지는 오브젝트
  • 클래스 상속을 이용하여 프록시 구현. 인터페이스가 존재하지 않아도 가능
  • Dynamic Proxy 보다 약 3배 가까이 빠르다
    • Reflection Api자체가 느리다고함
  • MethodInterceptor를 재정의한 intercept를 구현해야 부가 기능이 추가된다
  • 메서드에 final을 붙이면 오버라이딩이 불가능
  • 만약 클래스 상속을 통해 이라면 클래스에 final키워드 불가(상속안됨) + 부모 클래스의 생성자를 체크해야한다
public class MethodInter implements MethodInterceptor {

  Object target;

  public MethodInter(Object target) {
    this.target = target;
  }

  @Override
  public Object intercept(Object obj , Method method , Object[] args , MethodProxy proxy)
      throws Throwable {
    if(method.getName().contains("sleep")){
      System.out.println("자고 싶다고!!!!!!1");
    }
    return method.invoke(target,args);
  }
}
  @Test
  void test2(){
    SleepImpl sleep = (SleepImpl) Enhancer.create(
        SleepImpl.class,
        new MethodInter(new SleepImpl())
    );
    sleep.wantSleep();
    System.out.println(sleep.getClass());
  }

 

 

현재 스프링 부트

 
앞서 말했드시 인터페이스 유무에 따라 어떤 proxy인지 결정되는데
스프링에서는 기본으로 CGLIB가 작동한다
그이유를 찾아보니 인터페이스 기반 프록시의 경우 ClassCastException이 발생하는데 추적하기 어렵다고 한다.
 
 


간단하게 Jdk Dynamic Proxy와 CGLIB에 대해 말해봤다.
사실 여기서 ProxyFactory에 대해 더 얘기해야한다. 앞서 얘기했듯이 Proxyfactory가 인터페이스 유무에 따라 어떤 프록시를 생성할지 결정한다.다만 이부분은 아직 공부가 덜되서 다음에 다룰 예정이당
오늘은 이만 총총총..