[스프링 핵심 원리 - 고급편] #6. 동적 프록시 기술 #693
Develop-KIM
started this conversation in
동환
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
리플렉션
지금까지 프록시를 사용해서 기존 코드를 변경하지 않고, 로그 추적기라는 부가 기능을 적용할 수 있었다.
그런데 문제는 대상 클래스 수 만큼 로그 추적을 위한 프록시 클래스를 만들어야 한다는 점이다.
자바가 기본으로 제공하는 JDK 동적 프록시 기술이나 CGLIB 같은 프록시 생성 오픈소스 기술을 활용하면 프록시 객체를 동적으로 만들어낼 수 있다.
쉽게 이야기해서 프록시 클래스를 지금처럼 계속 만들지 않아도 된다는 것이다.
프록시를 적용할 코드를 하나만 만들어두고 동적 프록시 기술을 사용해서 프록시 객체를 찍어내면 된다
JDK 동적 프록시를 이해하기 위해서는 먼저 자바의 리플렉션 기술을 이해해야 한다.
리플렉션 기술을 사용하면 클래스나 메서드의 메타정보를 동적으로 획득하고, 코드도 동적으로 호출할 수 있다.
ReflectionTest
target.callA(),target.callB()이 부분만 동적으로 처리할 수 있다면 문제를 해결할 수 있을 듯 하다.ReflectionTest - reflection1 추가
Class.forName("hello.proxy.jdkdynamic.ReflectionTest$Hello"): 클래스 메타정보를 획득한다.참고로 내부 클래스는 구분을 위해
$를 사용한다.classHello.getMethod("call"): 해당 클래스의call메서드 메타정보를 획득한다.methodCallA.invoke(target): 획득한 메서드 메타정보로 실제 인스턴스의 메서드를 호출한다.여기서
methodCallA는Hello클래스의callA()이라는 메서드 메타정보이다.methodCallA.invoke(인스턴스)를 호출하면서 인스턴스를 넘겨주면 해당 인스턴스의callA()메서드를 찾아서 실행한다.여기서는
target의callA()메서드를 호출한다.그런데
target.callA()나target.callB()메서드를 직접 호출하면 되지 이렇게 메서드 정보를 획득해서 메서드를 호출하면 어떤 효과가 있을까?여기서 중요한 핵심은 클래스나 메서드 정보를 동적으로 변경할 수 있다는 점이다.
기존의
callA(),callB()메서드를 직접 호출하는 부분이Method수 있게 되었다.ReflectionTest - reflection2 추가
dynamicCall(Method method, Object target)Method method: 첫 번째 파라미터는 호출할 메서드 정보가 넘어온다.Method라는 메타정보를 통해서 호출할 메서드 정보가 동적으로 제공된다.Object target: 실제 실행할 인스턴스 정보가 넘어온다. 타입이Object라는 것은 어떠한 인스턴스도 받을 수 있다는 뜻이다.물론
method.invoke(target)를 사용할 때 호출할 클래스와 메서드 정보가 서로 다르면 예외가 발생한다.정리
정적인
target.callA(),target.callB()코드를 리플렉션을 사용해서Method라는 메타정보로 추상화했다.추상화되어 있기 때문에 공통 로직을 만들 수 있다.
주의
리플렉션을 사용하면 클래스와 메서드의 메타정보를 사용해서 애플리케이션을 동적으로 유연하게 만들 수 있다.
하지만 리플렉션 기술은 런타임에 동작하기 때문에, 컴파일 시점에 오류를 잡을 수 없다.
예시) 코드에서
getMethod("callA")안에 들어가는 문자를 실수로getMethod("callZ")로 작성해도 컴파일 오류가 발생하지 않는다.그러나 해당 코드를 직접 실행하는 시점에 발생하는 오류인 런타임 오류가 발생한다.
가장 좋은 오류는 개발자가 즉시 확인할 수 있는 컴파일 오류이고, 가장 무서운 오류는 사용자가 직접 실행할 때 발생하는 런타임 오류다.
따라서 리플렉션은 일반적으로 사용하면 안된다.
언어가 발달하면서 타입 정보를 기반으로 컴파일 시점에 오류를 잡아준 덕분에 개발자가 편하게 살았는데, 리플렉션은 그것에 역행하는 방식이다.
리플렉션은 프레임워크 개발이나 또는 매우 일반적인 공통 처리가 필요할 때 부분적으로 주의해서 사용해야 한다.
JDK 동적 프록시 - 소개
앞서 프록시를 적용하기 위해 적용 대상의 숫자만큼 프록시 클래스를 만들었다.
프록시 클래스의 기본 코드와 흐름은 거의 같지만 적용 대상이 차이가 있어 각각 다르게 프록시 클래스를 만들어서 적용시켜야 하는 문제가 있었는데
이 문제를 해결하는 것이 동적 프록시 기술이다.
동적 프록시 기술을 사용하면 개발자가 직접 프록시 클래스를 만들지 않아도 된다.
기본 예제 코드
AInterface
AImpl
BInterface
BImpl
JDK 동적 프록시 - 예제 코드
JDK 동적 프록시 InvocationHandler
JDK 동적 프록시에 적용할 로직은
InvocationHandler인터페이스를 구현해서 작성하면 된다.JDK 동적 프록시가 제공하는 InvocationHandler
제공되는 파라미터
Object proxy: 프록시 자신Method method: 호출한 메서드Object[] args: 메서드를 호출할 때 전달한 인수TimeInvocationHandler
TimeInvocationHandler은InvocationHandler인터페이스를 구현한다. 이렇게해서 JDK 동적 프록시에 적용할 공통 로직을 개발할 수 있다.Object target: 동적 프록시가 호출할 대상method.invoke(target, args): 리플렉션을 사용해서target인스턴스의 메서드를 실행한다.args는 메서드 호출시 넘겨줄 인수JdkDynamicProxyTest
new TimeInvocationHandler(target): 동적 프록시에 적용할 핸들러 로직Proxy.newProxyInstance(AInterface.class.getClassLoader(), new Class[] {AInterface.class}, handler)java.lang.reflect.Proxy를 통해서 생성할 수 있다.dynamicA() 출력 결과
생성된 JDK 동적 프록시
proxyClass=class com.sun.proxy.$Proxy1이 부분이 동적으로 생성된 프록시 클래스 정보이다.직접 만든 클래스가 아니라 JDK 동적 프록시가 이름 그대로 동적으로 만들어준 프록시이다. 이 프록시는
TimeInvocationHandler로직을 실행한다.실행 순서
call()을 실행한다.InvocationHandler.invoke()를 호출한다.TimeInvocationHandler가 구현체로 있으므로TimeInvocationHandler.invoke()가 호출된다.TimeInvocationHandler가 내부 로직을 수행하고,method.invoke(target, args)를 호출해서target인 실제 객체(AImpl)를 호출한다.AImpl인스턴스의call()이 실행된다.AImpl인스턴스의call()의 실행이 끝나면TimeInvocationHandler로 응답이 돌아온다.시간 로그를 출력하고 결과를 반환한다.
실행 순서 그림

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

JDK 동적 프록시 도입 후

JDK 동적 프록시 도입 전

JDK 동적 프록시 도입 후

JDK 동적 프록시 - 적용1
JDK 동적 프록시는 인터페이스가 필수이기 때문에 V1 애플리케이션에만 적용할 수 있다.
LogTraceBasicHandler
LogTraceBasicHandler는InvocationHandler인터페이스를 구현해서 JDK 동적 프록시에서 사용된다.private final Object target: 프록시가 호출할 대상이다.String message = method.getDeclaringClass().getSimpleName() + "." ...LogTrace에 사용할 메시지이다.프록시를 직접 개발할 때는
"OrderController.request()"와 같이 프록시마다 호출되는 클래스와 메서드 이름을 직접 남겼다.이제는
Method를 통해서 호출되는 메서드 정보와 클래스 정보를 동적으로 확인할 수 있기 때문에 이 정보를 사용하면 된다.DynamicProxyBasicConfig
Controller,Service,Repository에 맞는 동적 프록시를 생성LogTraceBasicHandler:LogTrace를 출력하는 로직은 모두 같기 때문에 프록시는 모두LogTraceBasicHandler를 사용ProxyApplication - 수정
그림으로 정리
남은 문제
LogTraceBasicHandler가 실행되기 때문에 로그가 남는다. 이 부분을 로그가 남지 않도록 처리해야 한다.JDK 동적 프록시 - 적용2
요구사항에 의해 이것을 호출 했을 때는 로그가 남으면 안된다.
메서드 이름을 기준으로 특정 조건을 만족할 때만 로그를 남기는 기능 추가
LogTraceFilterHandler
LogTraceFilterHandler는 기존 기능에 다음 기능이 추가되었다.LogTrace로직을 실행한다. 이름이 매칭되지 않으면 실제 로직을 바로 호출한다.PatternMatchUtils.simpleMatch(..)를 사용하면 단순한 매칭 로직을 쉽게 적용할 수 있다.xxx: xxx가 정확히 매칭되면 참xxx*: xxx로 시작하면 참*xxx: xxx로 끝나면 참*xxx*: xxx가 있으면 참String[] patterns: 적용할 패턴은 생성자를 통해서 외부에서 받는다.DynamicProxyFilterConfig
public static final String[] PATTERNS = {"request*", "order*", "save*"};적용할 패턴이다.request,order,save로 시작하는 메서드에 로그가 남는다.LogTraceFilterHandler: 앞서 만든 필터 기능이 있는 핸들러를 사용한다. 그리고 핸들러에 적용 패턴도 넣어준다.ProxyApplication - 추가
JDK 동적 프록시 - 한계
JDK 동적 프록시는 인터페이스가 필수이기 때문에 V2 애플리케이션 처럼 인터페이스 없이 클래스만 있는 경우에는 적용하지 못한다.
그렇기 때문에
CGLIB라는 바이트코드를 조작하는 특별한 라이브러리를 사용해야 함CGLIB - 소개
CGLIB: Code Generator Library
따라서 스프링을 사용한다면 별도의 외부 라이브러리를 추가하지 않아도 사용할 수 있다.
공통 예제 코드
ServiceInterface,ServiceImplConcreteServiceServiceInterface
ServiceImpl
ConcreteService
CGLIB - 예제 코드
CGLIB는 실행 로직을 위해
MethodInterceptor를 제공한다.MethodInterceptor -CGLIB 제공
obj: CGLIB가 적용된 객체method: 호출된 메서드args: 메서드를 호출하면서 전달된 인수proxy: 메서드 호출에 사용TimeMethodInterceptor
TimeMethodInterceptor는MethodInterceptor인터페이스를 구현해서 CGLIB 프록시의 실행 로직을 정의Object target: 프록시가 호출할 실제 대상proxy.invoke(target, args): 실제 대상을 동적으로 호출method를 사용해도 되지만, CGLIB는 성능상MethodProxy proxy를 사용하는 것을 권장CglibTest
ConcreteService는 인터페이스가 없는 구체 클래스이다. 여기에 CGLIB를 사용해서 프록시를 생성Enhancer: CGLIB는Enhancer를 사용해서 프록시를 생성한다.enhancer.setSuperclass(ConcreteService.class): CGLIB는 구체 클래스를 상속 받아서 프록시를 생성할 수 있다.어떤 구체 클래스를 상속 받을지 지정한다.
enhancer.setCallback(new TimeMethodInterceptor(target))enhancer.create(): 프록시를 생성한다.앞서 설정한
enhancer.setSuperclass(ConcreteService.class)에서 지정한 클래스를 상속 받아서 프록시가 만들어진다.실행 결과
CGLIB가 생성한 프록시 클래스 이름
ConcreteService$$EnhancerByCGLIB$$25d6b0e3CGLIB가 동적으로 생성하는 클래스 이름은 다음과 같은 규칙으로 생성된다.
대상클래스$$EnhancerByCGLIB$$임의코드그림으로 정리
CGLIB 제약
final키워드가 붙으면 상속이 불가능하다. -> CGLIB에서는 예외가 발생한다.final키워드가 붙으면 해당 메서드를 오버라이딩 할 수 없다. -> CGLIB에서는 프록시 로직이 동작하지 않는다.정리
남은 문제
JDK 동적 프록시가 제공하는
InvocationHandler와 CGLIB가 제공하는MethodInterceptor를 각각 중복으로 만들어서 관리해야 할까?Beta Was this translation helpful? Give feedback.
All reactions