[TIL] 리플렉션,기본 프록시,JDK Dynamic Proxy 그리고 CGLIb Proxy

SlowAnd·2024년 1월 3일
0

Today I Learned

목록 보기
2/17
post-thumbnail

리플렉션

리플렉션의 핵심은 메타 데이터!

중복코드가 많은 반면, 핵심 로직은 간단한다.
중복코드를 뽑아낼 수 없을까?

호출하는 메서드인 target.callA() , target.callB() 이 부분만 동적으로 처리할 수 있다면 문제를 해 결할 수 있을 듯 하다.
마치


이럴 때 사용하는 기술이 바로 리플렉션이다. 리플렉션은 클래스나 메서드의 메타정보를 사용해서 동적으로 호출하는 메서드를 변경할 수 있다.

기존의 callA() , callB() 메서드를 직접 호출하는 부분이 Method 로 대체되었다.

정리

정적인 target.callA() , target.callB() 코드를 리플렉션을 사용해서 Method 라는 메타정보로 추상화했 다. 덕분에 공통 로직을 만들 수 있게 되었다.


기본 프록시

원래 순수 프록시는 각 클래스마다 전용 프록시를 만들어냈었다.
ex) controller,service,repository 각각 전부 Proxy로 감싼 클래스 만듬

구조

문제

프록시를 적용하기 위해 적용 대상의 숫자 만큼 많은 프록시 클래스를 만들어야 한다.
적용 대상이 100개면 프록시 클래스도 100개 만들어야한다..

이러한 프록시 클래스의 100개를 살펴보면
프록시의 로직은 같은데(중복 발생), 적용 대상만 차이가 있는 것이다.


동적 프록시의 등장

이 문제를 해결하는 것이 바로 동적 프록시 기술이다!

동적 프록시 기술을 사용하면 개발자가 직접 프록시 클래스를 만들지 않아도 된다.
이름 그대로 프록시 객체를 동적으로 런타임에 개발자 대신 만들어 준다.
그리고 동적 프록시에 원하는 실행 로직을 지정할 수 있다.

JDK 동적 프록시

자바 언어가 기본으로 제공하는 JDK 동적 프록시를 알아보자.

주의 : JDK 동적 프록시는 인터페이스를 기반으로 프록시를 동적으로 만들어 주기 때문에 인터페이스가 필수다.

JDK 동적 프록시에 적용할 로직은 InvocationHandler 인터페이스를 구현 및 작성한다.

JDK 동적 프록시가 제공하는 InvocationHandler

package java.lang.reflect;
 public interface InvocationHandler {
      public Object invoke(Object proxy, Method method, Object[] args)
         throws Throwable;
}

** 제공되는 파라미터 **
`Object proxy` : 프록시 자신
`Method method` : 호출한 메서드
`Object[] args` : 메서드를 호출할 때 전달한 인수

예시 코드

1. JDK 동적 프록시에 적용할 로직 구현

  1. 인터페이스와 구현체 A,B
  2. 구현체에 적용할 핸들러 제작. TimeInvocationHandler 만듬.

Object target : 동적 프록시가 호출할 대상
method.invoke(target, args) : 리플렉션을 사용해서 target 인스턴스의 메서드를 실행한다.
args 는 메서드 호출시 넘겨줄 인수이다.

2. 동적 프록시 사용(테스트) 하기

실행 코드

  • new TimeInvocationHandler(target) : 동적 프록시에 적용할 핸들러 로직이다.

  • Proxy.newProxyInstance( AInterface.class.getClassLoader(), new Class[] {AInterface.class}, handler)

    • 동적 프록시는 java.lang.reflect.Proxy 를 통해서 생성할 수 있다.
    • 클래스 로더 정보, 인터페이스, 그리고 핸들러 로직을 넣어주면 된다. 그러면 해당 인터페이스를 기반으로 동적 프록시를 생성하고 그 결과를 반환한다.

출력 결과

생성된 JDK 동적 프록시
proxyClass=class com.sun.proxy.$Proxy9 이 부분이 동적으로 생성된 프록시 클래스 정보이다.
이것은 우리가 만든 클래스가 아니라 JDK 동적 프록시가 이름 그대로 동적으로 만들어준 프록시이다. 이 프록시는 TimeInvocationHandler 로직을 실행한다.

3. 실행 순서

  1. 클라이언트는 JDK 동적 프록시의 call() 을 실행한다.

  2. JDK 동적 프록시는 InvocationHandler.invoke() 를 호출한다. TimeInvocationHandler 가 구현체로 있으로 TimeInvocationHandler.invoke() 가 호출된다.

  3. TimeInvocationHandler 가 내부 로직을 수행하고, method.invoke(target, args) 를 호출해 서 target 인 실제 객체( AImpl )를 호출한다.

  4. AImpl 인스턴스의 call() 이 실행된다.

  5. AImpl 인스턴스의 call() 의 실행이 끝나면 TimeInvocationHandler 로 응답이 돌아온다. 시간 로그를 출력하고 결과를 반환한다.

4. 실행 순서 그림

5. 동적 프록시 클래스 정보

dynamicA()dynamicB() 둘을 동시에 함께 실행하면 JDK 동적 프록시가 각각 다른 동적 프록시 클래스를 만들어주는 것을 확인할 수 있다.

정리

예제를 보면 AImpl , BImpl 각각 프록시를 만들지 않았다. 프록시는 JDK 동적 프록시를 사용해서 동적으로 만들고
TimeInvocationHandler 는 공통으로 사용했다.
JDK 동적 프록시 기술 덕분에 적용 대상 만큼 프록시 객체를 만들지 않아도 된다. 그리고 같은 부가 기능 로직을 한번만 개발해서 공통으로 적용할 수 있다. 만약 적용 대상이 100개여도 동적 프록시를 통해서 생성하고, 각각 필요한
InvocationHandler 만 만들어서 넣어주면 된다.
결과적으로 프록시 클래스를 수 없이 만들어야 하는 문제도 해결하고, 부가 기능 로직도 하나의 클래스에 모아서 단일 책임 원칙(SRP)도 지킬 수 있게 되었다.
JDK 동적 프록시 없이 직접 프록시를 만들어서 사용할 때와 JDK 동적 프록시를 사용할 때의 차이를 그림으로 비교해 보자.

(전 vs 후)JDK 동적 프록시 등록

JDK 동적 프록시 도입 후 전체 코드


JDK 동적 프록시 - 한계

JDK 동적 프록시는 인터페이스가 필수다.
그렇다면 인터페이스 없이 클래스만 있는 경우에는 어떻게 동적 프록시를 적용할 수 있을까?
이것은 일반적인 방법으로는 어렵고 CGLIB라는 바이트 코드를 조작하는 특별한 라이브러리르 사용해야한다.

CGLIB 소개

CGLIB: Code Generator Library

CGLIB는 바이트코드를 조작해서 동적으로 클래스를 생성하는 기술을 제공하는 라이브러리이다.
CGLIB를 사용하면 인터페이스가 없어도 구체 클래스만 가지고 동적 프록시를 만들어낼 수 있다.

CGLIB는 원래는 외부 라이브러리인데, 스프링 프레임워크가 스프링 내부 소스 코드에 포함했다.
따라서 스프링 을 사용한다면 별도의 외부 라이브러리를 추가하지 않아도 사용할 수 있다.

참고로 우리가 CGLIB를 직접 사용하는 경우는 거의 없다.
이후에 설명할 스프링의 ProxyFactory 라는 것이 이 기 술을 편리하게 사용하게 도와주기 때문에, 너무 깊이있게 파기 보다는 CGLIB가 무엇인지 대략 개념만 잡으면 된다.
예제 코드로 CGLIB를 간단히 이해해보자.

CGLIB 코드

JDK 동적 프록시에서 실행 로직을 위해 InvocationHandler 를 제공했듯이, CGLIB는 MethodInterceptor 를 제공한다.

package org.springframework.cglib.proxy;


  public interface MethodInterceptor extends Callback{
  
     Object intercept(Object obj, Method method, Object[] args, MethodProxy
 							proxy) throws Throwable;

 }
 
`obj` : CGLIB가 적용된 객체
`method` : 호출된 메서드
`args` : 메서드를 호출하면서 전달된 인수 `proxy` : 메서드 호출에 사용

예시 코드

  • TimeMethodInterceptorMethodInterceptor 인터페이스를 구현해서 CGLIB 프록시의 실행 로직 을 정의한다.
  • JDK 동적 프록시를 설명할 때 예제와 거의 같은 코드이다.
  • Object target : 프록시가 호출할 실제 대상
  • proxy.invoke(target, args) : 실제 대상을 동적으로 호출한다.
    • 참고로 method 를 사용해도 되지만, CGLIB는 성능상 MethodProxy proxy 를 사용하는 것을 권장한 다.

테스트 코드


ConcreteService 는 인터페이스가 없는 구체 클래스이다. 여기에 CGLIB를 사용해서 프록시를 생성해보자.

  • Enhancer : CGLIB는 Enhancer 를 사용해서 프록시를 생성한다.
  • enhancer.setSuperclass(ConcreteService.class) : CGLIB는 구체 클래스를 상속 받아서 프록시 를 생성할 수 있다. 어떤 구체 클래스를 상속 받을지 지정한다.
  • enhancer.setCallback(new TimeMethodInterceptor(target))
    • 프록시에 적용할 실행 로직을 할당한다.
  • enhancer.create() : 프록시를 생성한다. 앞서 설정한 enhancer.setSuperclass(ConcreteService.class) 에서 지정한 클래스를 상속 받아서 프록시가 만들어진다.
    JDK 동적 프록시는 인터페이스를 구현(implement)해서 프록시를 만든다. CGLIB는 구체 클래스를 상속(extends)해서 프록시를 만든다.

실행 결과

실행 결과를 보면 프록시가 정상 적용된 것을 확인할 수 있다.

  • CGLIB가 생성한 프록시 클래스 이름
    CGLIB를 통해서 생성된 클래스의 이름을 확인해보자.
    ConcreteService$$EnhancerByCGLIB$$25d6b0e3
    CGLIB가 동적으로 생성하는 클래스 이름은 다음과 같은 규칙으로 생성된다.
    대상클래스$$EnhancerByCGLIB$$임의코드
    참고로 다음은 JDK Proxy가 생성한 클래스 이름이다.
    proxyClass=class com.sun.proxy.$Proxy1

그림 정리

CGLIB 제약

  • 클래스 기반 프록시는 상속을 사용하기 때문에 몇가지 제약이 있다.
    • 부모 클래스의 생성자를 체크해야 한다. CGLIB는 자식 클래스를 동적으로 생성하기 때문에 기본 생성자 가 필요하다.
    • 클래스에 final 키워드가 붙으면 상속이 불가능하다. CGLIB에서는 예외가 발생한다.
    • 메서드에 final 키워드가 붙으면 해당 메서드를 오버라이딩 할 수 없다. CGLIB에서는 프록시 로직이 동작하지 않는다.

남은 문제

인터페이스가 있는 경우에는 JDK 동적 프록시를 적용하고, 그렇지 않은 경우에는 CGLIB를 적용하려면 어떻게 해야할까?

두 기술을 함께 사용할 때 부가 기능을 제공하기 위해서 JDK 동적 프록시가 제공하는 InvocationHandler 와 CGLIB가 제공하는 MethodInterceptor 를 각각 중복으로 만들어서 관리해야 할까?

특정 조건에 맞을 때 프록시 로직을 적용하는 기능도 공통으로 제공되었으면?

이 문제는 스프링의 프록시 팩토리가 해결해준다.

프록시 팩토리는 따로 학습글을 작성하겠다.

참조
스프링 핵심 원리 - 고급편 - 인프런

0개의 댓글