-
[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 동적 프록시
실행 순서
- 클라이언트는 JDK 동적 프록시 객체의 call()을 실행함
- JDK 동적 프록시는 InvocationHandler.invoke()를 호출함. TimeInvocationHandler가 구현체로 전달되었으므로 TimeInvocationHandler.invoke()가 호출됨
- TimeInvocationHandler가 내부 로직을 수행하고, method.invoke(target, args)를 호출해 target인 실체 객체(AImpl)를 호출함
- AImpl 객체의 call()이 실행됨
- 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$$임의코드` 규칙으로 생성됨
정리
참고
'Study > Spring' 카테고리의 다른 글
[Spring] 스프링 핵심 원리 - 빈 후처리기 (0) 2025.04.21 [Spring] 스프링 핵심 원리 - 스프링이 지원하는 프록시 (0) 2025.04.20 [Spring] 스프링 핵심 원리 - ThreadLocal (1) 2025.04.19 [Spring] Spring MVC - 필터, 인터셉터 (0) 2025.04.19 [Spring] Spring MVC - 구조 이해 및 기본 기능 (0) 2025.04.19 - 자바가 기본으로 제공하는 JDK 동적 프록시 기술이나 CGLIB 같은 프록시 생성 오픈소스 기술을 활용하면 프록시 객체를 동적으로 만들어낼 수 있음