[Java] 다이나믹 프록시

궁금증 : JpaRepository 인터페이스는 어떻게 구현이 되는 걸까?

JPA를 사용할 때 @Repository 인터페이스에 JpaRepository<T, ID>만 상속받으면 우리는 findById, findAll , save, delete 등 같은 엔티티를 변경하는 메서드들을 사용할 수 있다. 애플리케이션 안에 코드상에는 구현 코드가 없는데 어떻게 생성되고 사용할 수 있게 되는 걸까?

 

@Entity
public class EntityA {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
}

public interface InterfaceA {
    EntityA getEntityById(Long id);
}

@Service
public class AImpl implements InterfaceA {

    private final JpaRepositoryExample exampleRepository;

    public AService(JpaRepositoryExample exampleRepository) {
        this.exampleRepository = exampleRepository;
    }

    @Override
    public EntityA getEntityById(Long id) {
        return exampleRepository.findById(id).orElseThrow(
                () -> new RuntimeException("inValid Id, Empty EntityA")
        );
    }
}


@Repository
public interface JpaRepositoryExample extends JpaRepository<EntityA, Long> {
}

 

디버깅을 해보자
자바에서 $Proxy가 붙은 객체는 프록시 객체를 의미한다. (주로 동적 프록시 패턴을 사용할 때 나타남) 

 

프록시 패턴이란

Proxy는 '대리, 대리자'라는 뜻을 갖고 있다.

프로그래밍에서 프록시는 어떤 것에 대한 대리자일까?

https://www.inflearn.com/course/the-java-code-manipulation/dashboard

위 그림은 프록시 패턴의 구조이다. 서브젝트라는 인터페이스가 있고 프록시와 리얼 서브젝트 클래스가 서브젝트 인터페이스를 구현하고 있다. 그리고 프록시 클래스는 리얼 서브젝트 클래스를 참조한다.

https://www.inflearn.com/course/the-java-code-manipulation/dashboard

시퀀스 다이어그램을 보면은 프록시는 리얼서브젝트의 대리인이 되고 있는 상황이다.

public class AProxy implements InterfaceA {

    private final AImpl aImpl;

    public AProxy(AImpl aImpl) {
        this.aImpl = aImpl;
    }
    
    @Override
    public EntityA getEntityById(Long id) {
    	... 부가적인 기능 ...
        EntityA result = aImpl.getEntityById(id);
        ... 부가적인 기능 ...
        return result;
    }
}

위 예제 코드의 프록시 클래스

 

서브젝트에 대한 책임을 리얼 서브젝트에서 구현하고, 해당 책임 외의 부가적인 기능은 프록시 클래스가 담당한다. 프록시 패턴을 사용함으로써 SRP(단일 책임 원칙), OCP(개방 폐쇄 원칙)을 모두 지킬 수 있게 된다.

 

프록시 패턴의 단점은 부가적인 기능을 추가할 때 마다 프록시 클래스를 추가해야 한다. (복잡성이 증가) 이러한 단점을 보완하기 위해 다이나믹 프록시가 등장했다.

 

 

사진: Unsplash 의 Wajih Ghali

 

다이나믹 프록시란

런타임 환경에서 특정 인터페이스들을 구현하는 클래스 또는 인스턴스를 만드는 기술

 

다이나믹 프록시 구현 방법

Java Reflection 패키지 Proxy 클래스 사용해서 구현방법

public class Example1 {

    public static void main(String[] args) {
        AInterface aInterface = (AInterface) Proxy.newProxyInstance(
                AInterface.class.getClassLoader(),
                new Class[]{AInterface.class},
                new InvocationHandler() {

                    private final Aimpl aimpl = new Aimpl();

                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        System.out.println("AInterface start");
                        Object invoke = method.invoke(aimpl, args);
                        System.out.println("AInterface end");
                        return invoke;
                    }
                }
        );
        System.out.println(aInterface.call());
    }
}

 

  • Proxy.newProxyInstance(구현할 인터페이스의 클래스로더, 클래스 배열(구현할 인터페이스 목록), InvocationHandler 
  • 프록시 인스턴스가 어떤 인터페이스의 구현체인지 알려줘야 한다.
  • InvocationHandler : 프록시에 어떤 메서드가 호출 될 때 그 메소드 호출을 어떻게 처리할 것인지에 대한 핸들러
  • 리얼 서브젝트에 해당하는 클래스를 InvocationHandler 안에 참조하여 사용한다.
  • 리얼 서브젝트의 메서드가 실행되는 전 후로 start , end print하는 부가적인 기능이 실행된다.

특정 메서드에만 부가적인 기능을 추가하거나, 다른 부가적인 기능들을 감싸려고 할 때 위 코드량이 방대해진다. (유연한 구조가 아닌 상태) -> 이 상황을 스프링 인터페이스로 뜯어고친 것이 스프링 AOP(추후 학습 필요, 프록시 기반)

 

단점 : 자바의 다이나믹 프록시 방법은 클래스 기반의 프록시를 만들어내지 못한다. 무조건 인터페이스만 전달해줘야 한다

 

클래스만 있을 때 다이나믹 프록시 구현 방법 : CGlib , ByteBuddy

클래스만 있을 때 서브 클래스를 만들 수 있는 라이브러리(CGlib)를 사용해서 프록시를 만들 수 있다.

 

CGlib

  • 스프링 그리고 하이버네이트가 사용하는 라이브러리
public class CallService {

    public void call() {
        System.out.println("call method");
    }

    public void readMessage() {
        System.out.println("read Message method");
    }
}


@Test
void test2() {
    MethodInterceptor handler = new MethodInterceptor() {
        final CallService callService = new CallService();

        @Override
        public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
            if (method.getName().equals("call")) {
                System.out.println("start");
                Object result = method.invoke(callService, args);
                System.out.println("end");
                return result;
            }
            return method.invoke(callService, args);
        }
    };
    CallService callService = (CallService) Enhancer.create(CallService.class, handler);
    callService.call();
    callService.readMessage();
}

테스트 결과값
start
call method
end
read Message method
  • Enhancer 클래스 : CGlib의 핵심클래스
  • Enhancer.create 메서드로 프록시 인스턴스를 만들 수 있다.
  • Enhancer.create(프록시 인스턴스를 만들 클래스 객체, 메서드 handler)
  • Java Reflection Proxy 클래스와 동일하게 MethodInterceptor 핸들러 안에 리얼 서브젝트 클래스를 넣고, 메서드 실행 전 후로 부가적인 기능 작업을 할 수 있다.

 

ByteBuddy

    @Test
    void test3() throws Exception {

        Class<? extends CallService> proxyClass = new ByteBuddy().subclass(CallService.class)
                .method(ElementMatchers.named("call")).intercept(InvocationHandlerAdapter.of(new InvocationHandler() {
                    CallService callService = new CallService();
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        System.out.println("call start");
                        Object result = method.invoke(callService, args);
                        System.out.println("call end");
                        return result;
                    }
                }))
                .make().load(CallService.class.getClassLoader()).getLoaded();

        CallService callService = proxyClass.getConstructor(null).newInstance();
        callService.call();
        callService.readMessage();
    }
    
테스트 결과값
call start
call method
call end
read Message method
  • 바로 인스턴스를 만들어주진 않고 프록시 클래스를 생성 후 인스턴스를 만들어 준다.
  • ByteBuddy().subclass(클래스 객체).method(특정 메서드 지정)).intercepter( InvocationHandler)
    • 자바 프록시 클래스에서 사용했던 InvocationHandler를 넣어준다.
  • call 메서드일 때만 InvocationHandler에 있는 로그가 찍히도록 코드가 구현되었다.

 

클래스에서 프록시 서브 클래스를 만들어서 하는 방법은 해당 클래스가 final이거나, private한 디폴트 생성자만 있을 경우에는 사용이 불가능하다. (상속을 통해 프록시 클래스를 만드는데 상속이 불가능한 클래스에는 적용 불가능) 

 

 

참조

더 자바, 코드를 조작하는 다양한 방법

 

더 자바, 코드를 조작하는 다양한 방법 - 인프런 | 강의

여러분이 사용하고 있는 많은 자바 라이브러리와 프레임워크가 "어떻게" 이런 기능을 제공할 지 궁금한적 있으신가요? 이번 강좌를 통해 자바가 제공하는 다양한 코드 또는 객체를 조작하는 방

www.inflearn.com