ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Spring] 스프링 핵심 원리 - 동적 프록시
    Study/Spring 2025. 4. 20. 20:54

    프록시 기술이 왜 필요할까?

    • 자바가 기본으로 제공하는 JDK 동적 프록시 기술이나 CGLIB 같은 프록시 생성 오픈소스 기술을 활용하면 프록시 객체를 동적으로 만들어낼 수 있음
      • 즉, 프록시 클래스를 직접 정의하지 않아도 됨
      • 프록시를 적용할 코드를 하나만 만들어두고 동적 프록시 기술을 사용해 프록시 객체를 찍어내는 것

     

    리플렉션

    • 동적 프록시를 이해하기 위해서는 먼저 자바의 리플렉션 기술을 이해해야 함
      • 리플렉션 기술을 사용하면 클래스나 메서드의 메타정보를 동적으로 획득하고, 코드도 동적으로 호출할 수 있음
    • 주의
      • 리플렉션 기술은 런타임에 동작하기 때문에, 컴파일 시점에 오류를 잡을 수 없음
      • 리플렉션은 프레임워크 개발이나 또는 매우 일반적인 공통 처리가 필요할 때 부분적으로 주의해서 사용해야 함

     

    예시)

    더보기
    @Test
    void reflection 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");
    }
    • Hello 클래스의 methodA, methodB 호출을 한 번에 처리할 수 있는 공통 처리 로직 제공을 위해 리플렉션을 사용함
      • 정적인 target.callA(), target.callB() 코드를 리플렉션을 사용해 Method라는 메타정보로 추상화함

     

    JDK 동적 프록시

    • 동적 프록시 기술을 사용하면 개발자가 직접 프록시 클래스를 만들지 않아도 됨
      • 프록시 객체를 동적으로 런타임에 개발자 대신 만들어줌
    • JDK 동적 프록시는 인터페이스를 기반으로 인터페이스를 구현(implement)해서 프록시를 동적으로 만듦. 따라서 인터페이스가 필수
      • java.lang.reflect.Proxy 클래스를 통해 생성
      • 프록시에 적용할 로직은 InvocationHandler 인터페이스를 구현해서 작성
        • 프록시 객체의 메서드 호출 시 InovocationHandler의 invoke() 호출됨
        • 리플렉션을 사용해 타깃 인스턴스의 메서드 호출함

     

    예시)

    더보기

    인터페이스와 구현체 선언 (타깃 정의)

    public interface AInterface {
    	String call();
    }
    
    public class AImpl implements AInterface {
        @Override
        public String call() {
        	return "a"; 
        }
    }
    public interface BInterface {
    	String call();
    }
    
    public class BImpl implements BInterface {
        @Override
        public String call() {
        	return "b"; 
        }
    }

     

    InvocationHandler 인터페이스 구현

    • JDK 동적 프록시에 적용할 로직은 InvocationHandler 인터페이스를 구현해서 작성함
    @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 start = System.currentTimeMillis();
    
            Object result = method.invoke(target, args);
    
            long end = System.currentTimeMillis();
            long duration = end - start;
            log.info("TimeProxy 종료 duration={}", duration);
    
            return result;
        }
    }
    • Object target : 동적 프록시가 호출할 대상
    • method.invoke(target, args) : 리플렉션을 사용해서 target 인스턴스의 메서드를 실행함. args는 메서드 호출 시 넘겨줄 인수

     

    테스트

    import lombok.extern.slf4j.Slf4j;
    import org.junit.jupiter.api.Test;
    
    import java.lang.reflect.Proxy;
    
    @Slf4j
    class JdkDynamicProxyTest {
    
        @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);
            proxy.call();
    
            log.info("targetClass={}", target.getClass());
            log.info("proxyClass={}", proxy.getClass());
        }
        
        @Test
        void dynamicB() {
            BInterface target = new BImpl();
            TimeInvocationHandler handler = new TimeInvocationHandler(target);
            BInterface proxy = (BInterface) Proxy.newProxyInstance(BInterface.class.getClassLoader(), new Class[]{AInterface.class}, handler);
            proxy.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를 통해 생성할 수 있음
      • 클래스 로더 정보, 인터페이스, 핸들러 로직을 넣어주면 됨
      • 해당 인터페이스를 기반으로 동적 프록시를 생성하고 그 결과를 반환함

     

    실행 결과

     

    생성된 JDK 동적 프록시

    실행 순서

    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로 응답이 돌아옴. 시간 로그를 출력하고 결과를 반환함

     

    정리

    • JDK 동적 프록시 기술 덕분에 적용 대상 만큼 프록시 객체를 직접 만들지 않아도 되고, 같은 부가 기능 로직을 한번만 개발해서 공통으로 적용할 수 있음
      • 만약 적용 대상이 100개여도 동적 프록시를 통해 생성하고, 각각 필요한 InvocationHandler만 만들어서 전달하면 됨
    • 결과적으로 프록시 클래스를 수 없이 만들어야 하는 문제도 해결하고, 부가 기능 로직도 하나의 클래스에 모아서 단일 책임 원칙(SRP)도 지킬 수 있게

     

    클래스 의존 관계

    - JDK 동적 프록시 도입 전

     

    - JDK 동적 프록시 도입 후

     

    런타임 객체 의존 관계

    - JDK 동적 프록시 도입 전

     

    - JDK 동적 프록시 도입 후

     

    CGLIB

    • CGLIB(Code Generator Library)는 바이트코드를 조작해서 동적으로 클래스를 생성하는 기술을 제공하는 라이브러리
    • 인터페이스가 없어도 구체 클래스만 가지고 구체 클래스를 상속(extends)해서 동적 프록시를 만들어낼 수 있음
    • CGLIB는 원래 외부 라이브러리인데, 스프링 프레임워크가 스프링 내부 소스 코드에 포함함
      • 스프링을 사용한다면 별도의 외부 라이브러리를 추가하지 않아도 사용할 수 있음
    • CGLIB 제약 - 상속을 사용하기 때문에 몇가지 제약이 있음
      • 부모 클래스의 생성자를 체크해야 함 --> 자식 클래스를 동적으로 생성하기 때문에 기본 생성자가 필요함
      • 클래스에 final 키워드 붙으면 상속이 불가능함 --> CGLIB에서는 예외가 발생함
      • 메서드에 final 키워드가 붙으면 해당 메서드를 오버라이딩 할 수 없음 --> CGLIB에서는 프록시 로직이 동작하지 않음
    •  참고
      • 스프링의 ProxyFactory가 이 기술을 편리하게 사용하게 도와주기 때문에 직접 CGLIB를 사용하는 경우는 거의 없음

     

    예시)

    더보기

    구체 클래스 (타깃 정의)

    import lombok.extern.slf4j.Slf4j;
    
    @Slf4j
    public class ConcreteService {
    
        public void call() {
            log.info("ConcreteService 호출");
        }
    }

     

    MethodInterceptor 인터페이스 구현

    • JDK 동적 프록시에서 실행 로직을 위해 InvocationHandler 인터페이스를 제공했듯이, CGLIB는 MethodInterceptor 인터페이스를 제공
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.cglib.proxy.MethodInterceptor;
    import org.springframework.cglib.proxy.MethodProxy;
    
    import java.lang.reflect.Method;
    
    @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 start = System.currentTimeMillis();
    
            Object result = proxy.invoke(target, args);
    
            long end = System.currentTimeMillis();
            long duration = end - start;
            log.info("TimeProxy 종료 duration={}", duration);
    
            return result;
        }
    }
    •  intercept() : CGLIB 프록시의 실행 로직을 정의함
    • Object target : 프록시가 호출할 실제 대상
    • proxy.invoke(target, args) : 실제 대상을 동적으로 호출함
      • Method를 사용해도 되지만, CGLIB는 성능상 MethodProxy를 사용하는 것을 권장함

     

    테스트

    import lombok.extern.slf4j.Slf4j;
    import org.junit.jupiter.api.Test;
    import org.springframework.cglib.proxy.Enhancer;
    
    @Slf4j
    class CglibTest {
    
        @Test
        void cglib() {
            ConcreteService target = new ConcreteService();
    
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(ConcreteService.class); // 상속할 클래스 지정
            enhancer.setCallback(new TimeMethodInterceptor(target)); // 프록시에 적용할 실행 로직 할당
            ConcreteService proxy = (ConcreteService) enhancer.create(); // 프록시 생성
    
            log.info("targetClass={}", target.getClass());
            log.info("proxyClass={}", proxy.getClass());
    
            proxy.call();
        }
    }
    •  Enhancer : CGLIB는 Enhancer를 사용해 프록시를 생성함

     

    실행 결과

    • CGLIB가 동적으로 생성하는 클래스 이름은 `대상클래스$$EnhancerByCGLIB$$임의코드` 규칙으로 생성됨

     

    정리

     

    참고

    댓글

Designed by Tistory.