[응용] 5. 동적 프록시 기술

kiwonkim·2021년 12월 1일
0

[ 이전 포스팅 ]

요청객체와 실제객체 사이에 프록시객체의 도입으로 부가기능을 수행하게 하였다. 다형성을 활용하기에 프록시객체는 실제객체와 같은 인터페이스를 구현하거나, 실제객체의 자식클래스이다. 프록시객체의 도입으로 요청객체와 실제객체의 코드를 수정하지 않고 의존성주입 변경만으로 부가기능을 추가할 수 있게 되었다. 그런데 실제객체마다 각각 프록시클래스를 만들어줘야 하는 한계가 존재한다. 이는 프록시클래스를 자동생성해주는 동적 프록시 기술로 해결할 수 있다.

공통 핸들러를 생성해놓고, 실행할 때 실제객체의 정보를 넘겨주면 프록시객체를 생성해주는 동적 프록시 기술에 대해 알아보자.


[ 리플렉션 ]

리플렉션이란

자바의 리플렉션 기술은 클래스나 메서드의 메타데이터를 획득하여, 메서드를 동적으로 호출할 수 있는 기능이다.

// 기존 - 정적 호출함수 정하기 가능
Temp target = new Temp(); // 객체생성
target.method1(); // 객체.함수명으로 함수호출

기존 정적호출은 객체.함수명으로 함수를 호출한다. 이 때 함수명은 문자열이 아니므로 동적으로 넣어줄 수 없고, 하드코딩처럼 입력해줘야한다.

// 리플렉션 - 동적으로 호출함수 정할 수 있음
Temp target = new Temp();
Class classTemp = Class.forName("hello.proxy.jdkdynamic.Temp"); // 클래스정보 추출
Method method1 = classTemp.getMethod("method1"); // 메서드 추출
method1.invoke(target); // 추출한 메서드 실행

리플렉션 기술은 클래스정보를 추출하고, 이를 바탕으로 메서드를 추출할 수 있다. 메서드를 추출할 때 메서드 명을 문자열로 결정하므로, 동적으로 사용할 메서드를 결정할 수 있다.

리플렉션의 장점

private Object plusCall(Method method, Object target) throws Exception {
	log.info("start");
    Object result = method.invoke(target);
    log.info("result={}, result);
    return result;
}

plusCall(method1, target2);
plusCall(mehotd2, target2);

동적으로 사용메서드를 결정하면 무엇을 얻을 수 있을까? 모든 메서드에 공통로직을 쉽게 구현할 수 있다. 공통로직이 method 를 파라미터로 받고. 원하는 메서드 정보를 추출해서 공통로직의 파라미터로 넘겨주면 된다. 메서드의 앞뒤로 공통로직을 메서드로 빼는 것은 어려운데, 이를 쉽게 가능케한다.

즉 리플렉션은 클래스와 메서드정보를 동적으로 추출해서 메서드를 호출함으로서, 메서드의 공통로직을 쉽게 추가할 수 있는 장점이 있다.

한계

Class classTemp = Class.forName("hello.proxy.jdkdynamic.Temp"); // 클래스정보 추출
Method method1 = classTemp.getMethod("asdfksanpf"); // 메서드 추출

리플렉션은 치명적인 단점이 있는데, 런타임에 호출메서드를 결정하므로 컴파일시점에 에러를 잡지 못한다는 것이다. 예를 들어 위에서 getMethod 의 메서드명을 아무렇게나 입력해도 잘 실행이된다. 다만 런타임 중에 실제 메서드를 추출해서 가져올 때 메서드가 없다면 런타임오류가 발생한다. 사용자가 실행할 때 발생하는 런타임오류는 치명적이므로, 리플렉션은 꼭 필요할때만 사용해야한다.


[ JDK 동적 프록시 ]

동적 프록시란

실제객체들에 적용할 부가기능이 모두 동일한 경우가 있다. 이런 경우에도 각 객체마다 일일이 프록시객체 클래스를 생성해야한다. 동적 프록시란 이처럼 부가기능은 동일하고, 적용대상만 다를 때 런타임에 동적으로 프록시클래스를 생성해주는 방식을 말한다.

JDK 동적 프록시란

JDK 동적 프록시는 자바가 기본으로 제공하는 동적 프록시 기술인데, 인터페이스 형식으로 구현된 실제객체에만 적용 가능하다. JDK 동적 프록시에 적용할 공통 부가기능은 InvocationHandler 인터페이스를 구현하며 invoke 메서드를 오버라이딩해서 구현한다.


[ JDK 동적 프록시 - 예제 ]

예제 상황

AInterface-AImpl 과 BInterface-BImpl 에 시간을 측정하는 공통 부가기능을 도입해보자. 이전이라면 인터페이스마다 별도의 프록시 객체를 직접 생성했어야 할 것이다.

InvocationHandler

시간을 측정하는 공통로직을 정의해 놓은 TimeInvocationHandler 이다. InvocationHandler 인터페이스를 구현하면서 invoke 메서드를 오버라이딩하는 것을 볼 수 있다.

필드로 실제객체를 갖고, 부가기능 중간에 파라미터로 넘어온 메서드를 실제객체에서 실행한다.

프록시객체 생성

프록시객체가 필요할 때 동적으로 프록시 객체를 생성한다. 실제객체를 주입한 InvocationHandler 객체를 생성하고. Proxy.newProxyInstance 메서드로 인터페이스를 구현한 클래스 정보, 인터페이스 정보, handler 를 넘겨주면 프록시 객체가 생성된다. Object 형이 반환되는데 메서드를 호출할 수 있게 하기위하여 Interface 로 형변환 시켰다.

프록시객체 메서드 호출시 일어나는 일

프록시객체의 메서드인 call을 실행하면 무슨일이 일어날까?

  1. 클라이언트가 동적프록시의 call() 을 실행한다.
  2. 프록시객체는 handler 의 invoke() 를 호출하며 이때 메서드로는 실행한 call이 넘어간다.
  3. handler 의 invoke 에서 부가기능을 실행 & 실제객체의 call이 실행된다.

즉 클라이언트는 프록시객체에 의존. 프록시객체는 InvocationHandler 객체에 의존. InvocationHandler 객체는 실제객체에 의존한다. 그래서 클라이언트객체가 프록시객체의 메서드를 호출하면, 메서드정보를 넘기며 InvocationHandler의 invoke가 실행되고, invoke 안에서 실제객체의 메서드가 호출되는 것이다.

이제 다른 실제객체의 프록시객체를 생성하더라도, InvocationHandler 에 실제객체를 넘겨주며 handler 를 생성하고. 클래스정보, 인터페이스정보, 핸들러를 넘겨주며 프록시객체를 생성하여 사용하면된다. 프록시클래스를 일일이 정의할 필요가 없어진 것이다.


[ JDK 동적 프록시 - 적용 ]

이제 JDK 동적프록시를 Controller, Service, Repository 의 로그추적기에 적용해보겠다.

LogTraceFilterHandler

필드로 실제객체와 패턴을 갖는다. 실제객체는 모든타입이 들어올 수 있도록 object로 선언하였다. 패턴은 메서드이름에 특정패턴이 들어갈 때만 부가기능을 수행하도록 한다.

밑에 로그를 출력하는 부가기능 중간에 invoke 를 통해 실제객체의 메서드를 호출하는 것을 확인할 수 있다.

Bean 등록

이제 빈으로 등록을 해야하는데, 주입된 모든 객체에 부가기능이 적용되도록 프록시객체를 빈으로 등록해야한다. Proxy.newProxyInstance 메서드로 프록시객체를 생성하여 빈으로 등록한다. Handler 에 주입되는 실제객체가 다르므로, 핸들러는 각각 생성해줘야한다.

상단에 적용할 패턴을 입력해주고 핸들러 생성시 파라미터로 넣어주었다.

의존관계 확인

기존에 프록시객체->실제객체->프록시객체->실제객체가 아닌 핸들러가 추가되어. 프록시객체->핸들러객체->실제객체->프록시객체->핸들러객체->실제객체의 의존관계가 정립되었다.

구현시에 각 프록시객체는 핸들러객체를 주입받으며 자동생성된다.

한계

JDK 동적프록시는 실제객체가 인터페이스를 구현클래스의 객체인 경우에만 적용이 가능하다. 인터페이스 없이 클래스만 있는 경우는 JDK 동적프록시가 아닌 CGLIB 같은 바이트조작 라이브러리를 사용해야한다.


[ CGLIB - 예제 ]

CGLIB 이란

CGLIB 은 바이트코드를 조작해서 동적으로 클래스를 생성하는 라이브러리이다. 이를 활용해 인터페이스 없이 구체클래스의 자식인 동적프록시를 생성해낼 수 있다. CGLIB 은 외부 라이브러리이나 스프링 내부 소스 코드에 포함되어있으며, 스프링의 ProxyFactory 가 CGLIB 을 편하게 사용하도록 도와주므로 CGLIB 을 직접사용하는 경우는 많지 않다.

MethodInterceptor

JDK 동적 프록시가 InvocationHandler 를 구현하여 사용했듯이 CGLIB 은 MethodInterceptor 를 구현해 사용한다. JDK 동적 프록시와 같이 실제객체를 필드로 가지며, 부가기능 내에서 실제객체의 메서드를 호출하는 방식으로 사용한다.

프록시객체 생성

JDK 동적 프록시는 Proxy.getProxyInstance 로 인터페이스와 핸들러를 넣어주며 프록시객체를 생성하였다. CGLIB 은 Enhancer 객체에 구현클래스와 메서드인터셉터를 넣어주며 프록시객체를 생성한다. 이 때 프록시객체는 실제객체의 자식클래스의 인스턴스가 된다.

의존관계

클래스 의존관계를 보면 프록시객체는 실제객체를 상속하며, 메서드인터셉터를 의존한다.

런타임 의존관계에 따른 실행과정은 다음과 같다.
1. 프록시객체의 메서드가 호출됨
2. 파라미터로 메서드 메타데이터를 넘겨주며 메서드인터셉터의 메서드가 호출됨.
3. 메서드인터셉터는 부가기능을 수행하며 실제객체의 메서드를 호출함.

한계

실제객체가 인터페이스를 구현한 클래스 -> JDK 동적 프록시 사용
실제객체가 구현클래스만 존재 -> CGLIB 사용

두 기술을 각각 적용하려면 하나의 부가기능에도 두 쌍을 모두 만들어 실제객체에 따라 적용해줘야한다. 특정 조건에 맞춰 자동으로 둘 중 하나를 적용시켜줄 수는 없을까?

[ 결론 ]

기존방식은 실제객체마다 하나씩 프록시클래스를 정의해야 했다. 동적프록시는 공통 부가기능을 실제객체에 추가한 프록시객체를 자동으로 생성할 수 있다.
런타임에서 실행할 함수를 동적으로 정하기 위해 클래스+메서드의 메타데이터로 메서드를 호출할 수 있는 리플렉션 기술을 도입했다.
공통 부가기능이 있는 핸들러클래스는 부가기능+실제객체의 메서드를 호출한다. 이 때 핸들러내부의 실제객체는 주입하여 사용하고, 호출하는 메서드명은 리플렉션으로 정해줄 수 있다. 즉 핸들러라는 틀을 이용해서 실제객체마다 프록시객체를 생성할 수 있게 된 것이다.
실제객체의 인터페이스가 존재한다면 JDK 동적 프록시를. 인터페이스가 없다면 CGLIB 을 통해 동적으로 프록시객체를 생성한다.

0개의 댓글