본문 바로가기
Spring/Advanced

동적 프록시 기술

by JHyun0302 2023. 8. 16.
728x90

리플렉션

→ 클래스나 메서드의 메타정보를 동적으로 획득하고, 코드도 동적으로 호출할 수 있다.

 

 

ReflectionTest

@Slf4j
public class ReflectionTest {
   
    @Test
    void reflection0() {
        Hello target = new Hello(); 

        //공통 로직1 시작
        log.info("start");
        String result1 = target.callA(); //호출하는 메서드가 다름
        log.info("result={}", result1); 
        //공통 로직1 종료

        //공통 로직2 시작 
        log.info("start");
        String result2 = target.callB(); //호출하는 메서드가 다름 
        log.info("result={}", result2);
        //공통 로직2 종료 
    }
   
    @Slf4j
    static class Hello {
        public String callA() {
            log.info("callA");
            return "A";	
        }

        public String callB() {
            log.info("callB");
            return "B"; 
        }
    } 
}

 

 

공통 로직1과 공통 로직 2를 하나의 메서드로 뽑아서 합칠 때 리플렉션 사용!

 

 

 

 

 

리플렉션 사용 : 공통 로직 합치기

@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);
}

 

  • classHello.getMethod("call") : 해당 클래스의 call 메서드 메타정보를 획득한다.
  • methodCallA.invoke(target) : 획득한 메서드 메타정보로 실제 인스턴스의 메서드를 호출한다

 

 

 

 

☆ 주의 : 리플렉션 기술은 런타임에 동작하기 때문에, 컴파일 시점에 오류를 잡을 수 없다.

 

 

 

 

 

 

 

 

 

 

 


JDK 동적 프록시 - 소개 + 예제

 

 

JDK 동적 프록시는 인터페이스를 기반으로 프록시를 동적으로 만들어준다. 따라서 인터페이스가 필수이다.

 

 

 

 

JDK 동적 프록시가 제공하는 InvocationHandler 를 구현해서 작성하면 된다.

 public interface InvocationHandler {
	public Object invoke(Object proxy, Method method, Object[] args)
		throws Throwable;
}

 

  • Object proxy : 프록시 자신
  • Method method : 호출한 메서드
  • Object[] args : 메서드를 호출할 때 전달한 인수

 

 

 

 

 

TimeInvocationHandler

@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();
        
        Object result = method.invoke(target, args);
        
        long endTime = System.currentTimeMillis();
        long resultTime = endTime - startTime; 
        log.info("TimeProxy 종료 resultTime={}", resultTime); 
        return result;
    } 
}

 

 

 

 

 

 

dynamicA() 출력 결과

TimeInvocationHandler - TimeProxy 실행
AImpl - A 호출
TimeInvocationHandler - TimeProxy 종료 resultTime=0
JdkDynamicProxyTest - targetClass=class hello.proxy.jdkdynamic.code.AImpl 
JdkDynamicProxyTest - proxyClass=class com.sun.proxy.$Proxy1

 

 

 

 

 

실행 순서

 

 

 

※ 정리

  • AImpl , BImpl 각각 프록시를 만들지 않았다.
  • 프록시는 JDK 동적 프록시를 사용해서 동적으로 만들고 TimeInvocationHandler 는 공통으로 사용했다.

 

 

 

 

 

JDK 동적 프록시 도입 전 - 직접 프록시 생성

 

 

 

JDK 동적 프록시 도입 전

 

 

 

 


 

 

JDK 동적 프록시 도입 후

 

 

 

 

 

JDK 동적 프록시 도입 후

 

 

 

 

 

 

 

 


JDK 동적 프록시 - 적용

 

 

 

 

LogTraceBasicHandler

public class LogTraceBasicHandler implements InvocationHandler {
    
    private final Object target;
    private final LogTrace logTrace;
    
    public LogTraceBasicHandler(Object target, LogTrace logTrace) {
        this.target = target;
        this.logTrace = logTrace;
    }
    
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        TraceStatus status = null;
        try {
            String message = method.getDeclaringClass().getSimpleName() + "." + method.getName() + "()";
            status = logTrace.begin(message); 
        
            //로직 호출
            Object result = method.invoke(target, args);

            logTrace.end(status);
            return result;
        } catch (Exception e) {
            logTrace.exception(status, e);
            throw e; 
        }
    } 
}

 

 

 

 

DynamicProxyBasicConfig

 @Configuration
public class DynamicProxyBasicConfig {
    
    @Bean
    public OrderControllerV1 orderControllerV1(LogTrace logTrace) {
        OrderControllerV1 orderController = new OrderControllerV1Impl(orderServiceV1(logTrace));
        OrderControllerV1 proxy = (OrderControllerV1)
        Proxy.newProxyInstance(OrderControllerV1.class.getClassLoader(), new Class[]{OrderControllerV1.class}, 
                new LogTraceBasicHandler(orderController, logTrace)
        );
    	return proxy;
    }
	
    //OrderServiceV1
    
    //OrderRepositoryV1

 

 

 

 

 

 

 

Proxy 직접 생성 X, InvocationHandler를 위임받은 LogTrace를 쓰면 됨.

 

 

 

 

 

 

 

 

 

 

 

 

 

문제 : no-log() 메서드도 LogTraceBasicHandler 적용됨.

 

 

 

 

 

 


메서드 이름 필터 기능 추가

 

 

 

 

LogTraceFilterHandler

public class LogTraceFilterHandler implements InvocationHandler {

    private final Object target;
    private final LogTrace logTrace;
    private final String[] patterns;
    
    public LogTraceFilterHandler(Object target, LogTrace logTrace, String... patterns) {
        this.target = target;
        this.logTrace = logTrace;
        this.patterns = patterns;
    }
    
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    
    	//메서드 이름 필터
        String methodName = method.getName();
        if (!PatternMatchUtils.simpleMatch(patterns, methodName)) {
            return method.invoke(target, args);
        }

        TraceStatus status = null;
        try {
            String message = method.getDeclaringClass().getSimpleName() + "." + method.getName() + "()";
            status = logTrace.begin(message);

            //로직 호출
            Object result = method.invoke(target, args);
            logTrace.end(status);
            return result;
        } catch (Exception e) {
            logTrace.exception(status, e);
            throw e; 
        }
	} 
}

 

  • PatternMatchUtils.simpleMatch(..) 를 사용하면 단순한 매칭 로직을 쉽게 적용할 수 있다.
            xxx : xxx가 정확히 매칭되면 참
            xxx* : xxx로 시작하면 참
            *xxx : xxx로 끝나면 참
            *xxx* : xxx가 있으면 참
  • String[] patterns : 적용할 패턴은 생성자를 통해서 외부에서 받는다.

 

 

 

DynamicProxyFilterConfig

@Configuration
public class DynamicProxyFilterConfig {
    public static final String[] PATTERNS = {"request*", "order*", "save*"};

    // OrderControllerV1
    // OrderServiceV1
    // OrderRepositoryV1
}

 

no-log 가 사용하는 noLog() 메서드에는 로그 남지 않는다.

 

 

 

 

 

 


JDK 동적 프록시 - 한계

 

 

 

JDK 동적 프록시는 인터페이스가 필수

해결 방안 : CGLIB

 

 

 

 

 

 

 

 


CGLIB  - 소개 + 예제

 

 

CGLIB : Code Generator Library 

→ 인터페이스가 없어도 구체 클래스만 가지고 동적 프록시를 만들어낼 수 있다.

 

 

 

 

CGLIB가 제공하는 MethodInterceptor 를 구현해서 작성하면 된다.

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

 

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

 

 

 

 

 

TimeMethodInterceptor

@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 proxy) throws Throwable {
        log.info("TimeProxy 실행");
        long startTime = System.currentTimeMillis();
        
        Object result = proxy.invoke(target, args);
        
        long endTime = System.currentTimeMillis();
        long resultTime = endTime - startTime; 
        log.info("TimeProxy 종료 resultTime={}", resultTime);
        return result;
    } 
}

 

proxy.invoke(target, args) : 실제 대상을 동적으로 호출한다.

 

 

 

 

 

 

실행 결과

CglibTest - targetClass=class hello.proxy.common.service.ConcreteService
CglibTest - proxyClass=class hello.proxy.common.service.ConcreteService$$EnhancerByCGLIB$$25d6b0e3
TimeMethodInterceptor - TimeProxy 실행
ConcreteService - ConcreteService 호출
TimeMethodInterceptor - TimeProxy 종료 resultTime=9

 

→ 대상클래스$$EnhancerByCGLIB$$임의코드

 

CGLIB가 생성한 프록시 클래스 이름

        ex) ConcreteService$$EnhancerByCGLIB$$25d6b0e3

 

JDK 프록시 클래스가 생성한 이름

        ex) proxyClass=class com.sun.proxy.$Proxy1

 

 

 

 

 

 

 

CGLIB 동적 프록시 적용

 

 

 

 

 

 

CGLIB 동적 프록시 적용

 

 

 

 

 

 

 


CGLIB 제약

 

 

  1. 부모 클래스의 생성자를 체크해야 한다. → CGLIB는 자식 클래스를 동적으로 생성하기 때문에 기본 생성자가 필요하다.
  2. 클래스에 final 키워드가 붙으면 상속이 불가능하다. CGLIB에서는 예외가 발생!
  3. 메서드에 final 키워드가 붙으면 해당 메서드를 오버라이딩 할 수 없다. 


 

 

 

 

 

 

 


문제

 

 

인터페이스가 있는 경우에는 JDK 동적 프록시(InvocationHandler)를 적용,

인터페이스가 없는 경우에는 CGLIB(MethodInterceptor)를 적용하려면

중복해서 만들어서 관리해야할까...?

 

 

 

 

 

반응형

'Spring > Advanced' 카테고리의 다른 글

빈 후처리기  (0) 2023.08.17
스프링이 지원하는 프록시  (0) 2023.08.17
프록시 패턴과 데코레이터 패턴  (0) 2023.08.14
템플릿 메서드 패턴과 콜백 패턴  (0) 2023.08.13
쓰레드 로컬 - ThreadLocal  (0) 2023.08.12