-
Spring 핵심 원리 고급편 - 5. 동적 프록시 기술Spring-Boot/스프링핵심원리 - 고급편 2022. 6. 7. 13:42
- 자바가 기본으로 제공하는 JDK 동적 프록시 기술이나 CGLIB 같은 프록시 생성 오픈소스 기술을 활용하면 프록시 객체를 동적으로 만들어 낼 수 있다.
- 프록시를 적용할 코드를 하나만 만들어두고 동적 프록시 기술을 사용해서 프록시 객체를 찍어내면 된다.
- JDK 동적 프록시를 이해하기 위해서는 먼저 자바의 리플렉션 기술을 이해해야 한다.
리플렉션
- 리플렉션 기술을 사용하면 클래스나 메서드의 *메타정보*를 동적으로 획득하고, 코드도 동적으로 호출할 수 있다.
* 참고 * - 람다를 사용해서 공통화하는 것도 가능!
@Test void reflection1() throws Exception { // 클래스 정보 // 클래스 메타정보를 획득한다. 참고로 내부 클래스는 구분을 위해 $를 사용한다. Class classHello = Class.forName("hello.proxy.jdkdynamic.ReflectionTest$Hello"); Hello target = new Hello(); // 해당 클래스의 call메서드 메타정보를 획득 Method methodCallA = classHello.getMethod("callA"); // 획득한 메서드 메타정보로 실제 인스턴스의 메서드를 호출 Object result1 = methodCallA.invoke(target); // 동적으로 콜이 가능 log.info("result1={}",result1); }
- 중요한 핵심은 클래스나 메서드 정보를 동적으로 변경할 수 있다는 점이다!
@Test void reflection2() throws Exception { // 클래스 정보 Class classHello = Class.forName("hello.proxy.jdkdynamic.ReflectionTest$Hello"); Hello target = new Hello(); Method methodCallA = classHello.getMethod("callA"); dynamicCall(methodCallA, target); Method methodCallB = classHello.getMethod("callB"); dynamicCall(methodCallB, target); } private void dynamicCall(Method method, Object target) throws Exception { log.info("start"); Object result = method.invoke(target); log.info("result={}",result); }
- dynamicCall ( Method method, Object target )
ㆍ공통로직1, 공통 로직2를 한번에 처리할 수 있는 통합된 공통 처리 로직이다.
ㆍMethod method : 첫번째 파라미터는 호출할 메서드 정보가 넘어온다. 이것이 핵심이다.
기존의 메서드 이름을 직접 호출했지만, 이제는 ' Method '라는 메타정보를 통해서 호출할 메서드 정보가 동적으로 제공
ㆍObject target : 실제 실행할 인스턴스 정보가 넘어온다.
타입이 Object라는 것은 어떠한 인스턴스도 받을 수 있다는 뜻이다.
* 정리 *
: 정적인 target.callA(), target.callB() 코드를 리플렉션을 사용해서 Method라는 메타정보로 추상화했다. 덕분에 공통 로직을 만들 수 있게 되었다.
* 주의 *
- 리플랙션을 사용하면 클래스와 메서드의 메타정보를 사용해서 애플리케이션을 동적으로 유연하게 만들 수 있다.
- 하지만 리플렉션 기술은 런타임에 동작하기 때문에, 컴파일 시점에 오류를 잡을 수 없다.
- " getMethod("callZ") " 로 작성해도 컴파일오류가 발생하지 않고 직접 실행하는 시점에 발생하는 오류인 런타임에 오류가 발생한다.
- 가장 좋은 오류는 개발자가 즉시 확인할 수 있는 컴파일 오류이고, 가장 무서운 오류는 사용자가 직접 실행할 때 발생하는 런타임 오류다.
- 리플렉션은 일반적으로 사용하면 안된다. 지금까지 프로그래밍 언어가 발달하면서 타입 정보를 기반으로 컴파일 시점에 오류를 잡아준 덕분에 개발자가 편하게 살았는데, 리플렉션은 그것에 역행하는 방식이다.
- 리플렉션은 프레임워크 개발이나 또는 매우 일반적인 공통 처리가 필요할 때 부분적으로 주의해서 사용해야 한다.
JDK 동적 프록시
- 동적 프록시 기술을 사용하면 개발자가 직접 프록시 클래스를 만들지 않아도 된다. 이름 그대로 프록시 객체를 동적으로 런타임에 개발자 대신 만들어준다. 그리고 동적 프록시에 원하는 실행 로직을 지정할 수 있다.
* 주의 * - JDK 동적프록시는 인터페이스를 기반으로 프록시를 동적으로 만들어준다. 따라서 인터페이스가 필수이다.
JDK 동적 프록시 InvocationHandler
- Object proxy : 프록시 자신
- Method method : 호출한 메서드
- Object[ ] args : 메서드를 호출할 때 전달한 인수
@Slf4j public class TimeInvocationHandler implements InvocationHandler { private final Object target; public TimeInvocationHandler(Object target) { this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { log.info("TimeProxy 실행"); long startTime = System.currentTimeMillis(); // 리플렉션을 사용해서 target 인스턴스의 메서드를 실행한다 // args는 메서드 호출시 넘겨줄 인수이다. Object result = method.invoke(target, args); long endTime = System.currentTimeMillis(); long resultTime = endTime - startTime; log.info("TimeProxy 종료 resultTume={}", resultTime); return result; } }
- 활용
@Test void dynamicA() { AInterface target = new AImpl(); TimeInvocationHandler handler = new TimeInvocationHandler(target); AInterface proxy = (AInterface)Proxy.newProxyInstance(AInterface.class.getClassLoader(), new Class[]{AInterface.class}, handler); // handler의 로직이 수행 proxy.call(); // handler 메소드에 call을 전달 log.info("targetClass={}", target.getClass()); log.info("proxyClass={}", proxy.getClass()); }
- new TimeInvocationHandler(target) : 동적 프록시에 적용할 핸들러 로직이다.
- Proxy.newProxyInstance(AInterface.class.getClassLoader(), new Class[]{AInterface.class}, handler);
ㆍ동적 프록시는 java.lang.reflect.Proxy를 통해서 생성할 수 있다
ㆍ클래스 로더 정보, 인터페이스, 그리고 핸들러 로직을 넣어주면 된다.
ㆍ그러면 해당 인터페이스를 기반으로 동적 프록시를 생성하고 그 결과를 반환한다.
* 실행 순서 *
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로 응답이 돌아온다. 시간 로그를 출력하고 결과를 반환한다
* 정리 *
- 프록시 클래스를 수 없이 만들어야 하는 문제도 해결
- 부가 기능 로직도 하나의 클래스에 모아서 단일 책임 원칙(SRP)도 지킬 수 있게 되었다.
* 메서드 이름 필터 기능 추가 *
- 메서드 이름을 기준으로 특정 조건을 만족할 때만 로그를 남기는 기능을 개발해보자
- 스프링이 제공하는 PatternMatchUtils.simpleMatch( . . ) 를 사용하면 단순한 매칭 로직을 쉽게 적용할 수 있다
ㆍxxx : xxx가 정확히 매칭되면 참
ㆍxxx* : xxx로 시작하면 참
ㆍ*xxx : xxx로 끝나면 참
ㆍ*xxx* : xxx가 있으면 참
* JDK 동적 프록시 - 한계 *
- JDK 동적 프록시는 인터페이스가 필수이다
- 그렇다면 V2 애플리케이션처럼 인터페이스 없이 클래스만 있는 경우에는 어떻게 동적 프록시를 적용할 수 있을까?
이것은 일반적인 방법으로는 어렵고 ' CGLIB '라는 바이트코드를 조작하는 트별한 라이브러리를 사용해야한다.
CGLIB ( Code Generator Library )
- CGLIB는 바이트코드를 조작해서 동적으로 클래스를 생성하는 기술을 제공하는 라이브러리이다
- CGLIB를 사용하면 인터페이스가 없어도 구체 클래스만 가지고 동적 프록시를 만들어낼 수 있다.
- CGLIB는 원래는 외부 라이브러리인데, 스프링 프레임워크가 스프링 내부 소스 코드에 포함했다. 따라서 스프링을 사용한다면 별도의 외부라이브러리를 추가하지 않아도 사용할 수 있다.
- 참고로 우리가 CGLIB를 직접 사용하는 경우는 거의 없다. 이후에 설명할 스프링의 ProxyFactory라는 것 이 기술을 편리하게 사용하게 도와주기 떄문에, 너무 깊이있게 파기 보다는 CGLIB가 무엇인지 대략 개념만 잡으면 된다.
- JDK 동적 프록시에서 실행 로직을 위해 InvocationHandler를 제공했듯이, CGLIB는 MethodInterceptor를 제공한다
ㆍobj : CGLIB가 적용된 객체
ㆍmethod : 호출된 메서드
ㆍargs : 메서드를 호출하면서 전달된 인수
ㆍproxy : 메서드 호출에 사용
@Slf4j public class TimeMethodInterceptor implements MethodInterceptor { private final Object target; public TimeMethodInterceptor(Object target) { this.target = target; } @Override public Object intercept(Object obj, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { log.info("TimeProxy 실행"); long startTime = System.currentTimeMillis(); Object result = methodProxy.invoke(target, args); long endTime = System.currentTimeMillis(); long resultTime = endTime - startTime; log.info("TimeProxy 종료 resultTume={}", resultTime); return result; } }
- proxy.invoke(target, args) : 실제 대상을 동적으로 호출
ㆍ참고로 ' method ' 를 사용해도 되지만, CGLIB는 성능상 ' MethodProxy proxy '를 사용하는 것을 권장한다.
@Test void cglib() { ConcreteService target = new ConcreteService(); // Enhancer : CGLIB는 Enhancer를 사용해서 프록시 생성 Enhancer enhancer = new Enhancer(); // CGLIB는 구체 클래스를 상속 받아서 프록시를 생성할 수 있다. enhancer.setSuperclass(ConcreteService.class); // 프록시에 적용할 실행 로직을 할당한다 enhancer.setCallback(new TimeMethodInterceptor(target)); // 프록시를 생성한다. // setSuperclass에서 지정한 클래스를 상속받아서 프록시 생성 ConcreteService proxy = (ConcreteService)enhancer.create(); log.info("targetClass={}", target.getClass()); log.info("proxyClass={}", proxy.getClass()); }
- JDK 동적 프록시는 인터페이스를 구현( implements ) 해서 프록시를 만든다. CGLIB는 구체 클래스를 상속( extends ) 해서 프록시를 만든다.
* CGLIB 제약 *
- 클래스 기반 프록시는 상속을 사용하기 때문에 몇가지 제약이 있다.
ㆍ부모 클래스의 생성자를 체크해야한다
→ CGLIB는 자식 클래스를 동적으로 생성하기 때문에 기본 생성자가 필요하다
ㆍ클래스에 ' final ' 키워드가 붙으면 상속이 불가능하다 → CGLIB에서는 예외가 발생한다
ㆍ메소드에 ' final ' 키워드가 붙으면 해당 메서드를 오버라이딩 할 수 없다.
→ CGLIB에서는 프록시 로직이 동작하지 않는다
정리
* 남은 문제 *
- 인터페이스가 있는 경우에는 JDK 동적 프록시를 적용하고, 그렇지 않은 경우에는 CGLIB를 적용하려면 어떻게 해야할까?
- 두 기술을 함께 사용할 때 부가 기능을 제공하기 위해서 JDK 동적 프록시가 제공하는 InvocationHandler와 CGLIB가 제공하는 MethodInterceptor를 각각 중복으로 만들어서 관리해야 할까?
- 특정 조건에 맞을 때 프로시 로직을 적용하는 기능도 공통으로 제공되었으면?
출처 : 인프런 김영한님의 스프링 핵심원리 - 고급편
'Spring-Boot > 스프링핵심원리 - 고급편' 카테고리의 다른 글
Spring 핵심 원리 고급편 - 7. 빈 후처리기 (0) 2022.06.08 Spring 핵심 원리 고급편 - 6. 스프링이 지원하는 프록시 (0) 2022.06.07 Spring 핵심 원리 고급편 - 4. 프록시 패턴과 데코레이터 패턴 (0) 2022.06.03 Spring 핵심 원리 고급편 - 3. 템플릿 메서드 패턴과 콜백 패턴 (0) 2022.06.02 Spring 핵심 원리 고급편 - 2. ThreadLocal (0) 2022.06.02