Spring

[Spring] JDK Dynamic Proxy와 CGLib에 대해서 알아보자.

UnaUna 2025. 6. 16. 23:57
반응형

🚀 JDK Dynamic Proxy와 CGLib

Spring 은 Proxy 기반 AOP 를 제공한다 .

이때 Proxy 가 생성되는 방법에 따라 JDK Dynamic Proxy와 CGLIB Proxy 방식이 존재한다 .

 

✅ Proxy란?

사전적으로는 대리 ( 행위 ) 나 대리권 , 대리 투표 , 대리인 등등의 뜻을 가지고 있다.

 

클라이언트가 RealSubject 객체의 request() 메소드를 호출하고 싶을 때,

직접 RealSubject 를 불러와서 호출하지 않고,

프록시 객체가 대신 RealSubject 객체의 request()를 호출하고 응답해주는 방식

 

 Proxy 를 쓰는 이유

1. 실제 RealSubject 의 request() 코드를 변경하지 않고 , 메소드 앞 뒤로 필요한 기능을 추가할 수있다.

ex) → log

2. 캐시를 사용할 수 있다. 프록시가 똑같은 request() 메소드가 호출 되도록 명령 받았다면 , 굳이 RealSubject에 새롭게 request() 를 호출하지 않고도 응답을 줄 수 있다.

 

 Spring 컨테이너 (IoC 컨테이너 ) 와 AOP 프록시

 

  • Proxy 객체는 IoC 컨테이너에서 생성 해준다.
  • IoC 컨테이는 특정 객체 (Controller, Service 등)에 대해 생명주기를 관리해주는데 프록시 객체도 마찬가지이다.
  • IoC 컨테이너가 특정 호출 시점에 프록시 객체 (Proxy Bean)을 생성해준다.
  • 동적으로 생성된 프록시 객체는 타켓의 메소드가 호출되는 시점에 부가 기능을 추가할 메소드를 자체적으로 판단하고 가로채어 부가 기능을 주입해주는 것.
  • 이처럼 호출 시점에 동적으로 위빙 (weaving) 한다고 하여 런타임 위빙 이라고 부른다.
  • 프록시 객체가 생성되는 방식에는 JDK Dynamic Proxy CGLIB Proxy방식이 존재한다.

 

🚀 JDK Proxy(JDK Dynamic Proxy)

Java의 reflect 패키지 (java.lang.reflect)의 Proxy 클래스를 통해 생성된 Proxy 객체 이름에 dynamic이 들어가는 이유는 위에서 설명 한대로 Proxy 객체가 동적으로 생성되기 때문이다.

 import java.lang.reflect.Proxy;
 
 // Proxy 객체 생성 
Object proxy = Proxy.newProxyInstance(ClassLoader // 클래스로더 
                                      , Class<?>[] // 타깃의 인터페이스 
                                      , InvocationHandler // 타깃의 정보가 포함된 Handler
                        );

 

 핵심 특징

JDK Proxy의 핵심은 타겟 클래스가 아니라 타겟의 인터페이스를 기준으로 Proxy가 생성 된다는 점이다.

 // 인터페이스 
public interface MyService { 
  String findUserId(); 
} 

// 구현체 
@Service public class MyServiceImpl implements MyService { 
	@Override
 	public String findUserId(String input){ 
		// 로직.. 
		return param; 
  	} 
}
@Controller 
public class MyController{ 
	@Autowired 
	private MyServiceImpl myServiceImpl; // <- Runtime Error 발생... 
	
    // 로직 
}

 

  • JDK Proxy 는 타겟의 인터페이스를 자체 검증하고, ProxyFactory 가 해당 인터페이스를 상속 한 Proxy 객체를 생성한다.
  • 그리고 이 Proxy 객체에 InvocatinoHandler 를 포함하여 하나의 객체로 반환한다.
  • JDK Proxy 방식으로 Proxy 객체를 생성하기 위해서는 반드시 인터페이스가 존재해야 한다.
  • @Autowired 를 통해 생성된 Proxy 객체를 사용하기 위해서는 반드시 인터페이스를 지정해 주어야 하고, 이를 위배하면 Runtime 에러가 발생할 수 있다.
  • 위의 예시의 경우 인터페이스가 아닌 타겟 클래스를 주입 받기 때문에 JDK Proxy 방식을 사 용하면 Runtime Exception 이 발생하게 된다.

 

🚀 CGLIB(Code Generator Library Proxy)

  • 클래스의 바이트 코드를 조작하여 Proxy 객체를 생성 해주는 라이브러리이다.
  • 인터페이스가 아닌 타겟 클래스에 대해서도 Proxy 객체를 생성할 수 있다.
  • CGLib는 Enhancer 라는 클래스를 통해 Proxy를 생성한다.
Enhancer enhancer = new Enhancer();
         enhancer.setSuperclass(MemberService.class); // 타깃 클래스 
         enhancer.setCallback(MethodInterceptor); // Handler 
Object proxy = enhancer.create(); // Proxy 생성

 

 

  • 타겟 클래스를 상속 받고 타겟 클래스에 포함된 모든 메소드를 재정의 하여 Proxy 객체를 를 생성해준다.
  • 그리고 이 Proxy 객체에 InvocatinoHandler 를 포함하여 하나의 객체로 반환한다.
  • CGLIB 는 바이트 코드를 조작하여 Proxy 를 생성 해주기 때문에 성능이 JDK 프록시 보다 좋다.

 

 JDK Proxy vs CGLIB 뭘 써야 할까 ?

처음 권장된 방식은 JDK Proxy 이다.

  • Java 에서 기본적으로 지원하는 Proxy 클래스를 사용하기 때문에 의존성이 따로 필요 없다.
  • CGLib 같은 경우에는 net.sf.cglib.proxy.Enhancer 의존성이 필요
  • CGLib를 구현하기 위해선 반드시 NoArgs 생성자가 필요하다.
  • 생성된 CGLib Proxy 의 메소드를 호출하면 타겟 클래스의 생성자가 2번 호출된다 .

 

하지만 시간이 지나면서  Spring4.3, Spring Boot 1.4 부터는 CGLib 방식이 Default 가 되었다 .

  • Spring 3.2부터 CGLib 가 Spring core 패키지에 들어가면서 의존성 추가 필요 없어짐
  • Spring 4.0부터 Objensis 라이브러리의 도움을 받아 NoArgs 생성자 없이도 Proxy 생성이 가능해지고 , 생성자가 2 번 호출 되던 이슈도 수정

🚀 Spring Bean과 CGLib proxy

❓ Spring Context로 관리되는 모든 빈은 Proxy 클래스로 만들어질까?

➡️ Spring Bean은 필요한 경우에만 Proxy 객체로 만들어 진다.

 

🚀 @Component 와 CGLib

  • @Component 자체는 직접적으로 CGLIB 를 사용하는 것은 아니다.
  • AOP 적용 시 CGLIB(or JDK Proxy) 가 사용될 수 있다.
  • AOP 는 빈으로 등록된 객체의 메서드 호출을 가로채서 추가적인 로직(Advice)을 주입하는 방식으로 동작하는데 이 가로채는 객체는 프록시(proxy) 객체이다.
  • @Component 로 등록된 클래스가 AOP 적용 대상이라면, Spring은 실제 빈 대신 프록시 객체를 빈으로 등록한다.
  • 이 프록시 객체는 원래의 메서드 호출을 가로채서, AOP 로직을 수행한 후 원본 메서드를 호 출하거나 다른 처리를 하는 것이다.

 

 AOP와 프록시 역할

AOP 는 프록시 패턴을 사용한다.

Spring AOP가 적용된 클래스가 @Component 로 등록되면 Spring 은 CGLIB 를 사용하여 프록시 객체를 생성한다 . 

이 프록시 객체는 원본 클래스의 모든 메서드를 오버라이드 하고 AOP 로직을 주입한다.

 

CGLIB는 런타임에 동적으로 클래스의 서브클래스를 생성하여, 해당 클래스의 모든 메서드를 오버라이드한다. 이 오버라이드 된 메서드에서 AOP의 횡단 관심사(Advice)가 주입되는 것이다.

 

CGLIB로 생성된 프록시 객체는 AOP가 정의한 Pointcut(어드바이스가 적용될 메서드 지점)과 Advice(실제 실행될 코드)를 기반으로 메서드 호출을 가로채고, AOP 로직을 실행한다.

 

프록시 객체는 메서드 호출을 가로채서 AOP Advice를 실행한 후, 원본 메서드를 호출하거나 다른 로직을 수행하게 된다. 이 과정에서 CGLIB가 프록시 객체를 통해 모든 메서드 호출을 제어한다.

 

🚀 @Configuration 과 CGLib

  • Spring에서 @Configuration 클래스를 처리할 때, 내부적으로 CGLIB 를 사용하여 해당 클래 스의 프록시 객체를 생성한다.
  • Spring 은 CGLIB를 이용해 @Configuraton 클래스를 상속 받은 프록시 클래스를 생성하는 것이다.

 

 @Configuration 이 CGLIB가 필요한 이유

싱글톤 보장을 위해서!

  • Spring IoC 컨테이너는 @Configuration 클래스를 싱글톤으로 관리하며, 이 클래스의 메소드가 매번 새로운 인스턴스를 반환하지 않도록 보장해야 한다.
    • Spring에서 @Configuraiton 클래스를 프록시로 감싸지 않으면 때마다 새로운 객체가 생성될 수 있다.
    • @Configuration 클래스의 프록시를 사용하면, 각 @Bean 메서드 호출 시 동일한 Bean 인 스턴스를 반환 받게 된다.
@Configuration
 // @Configuration(proxyBeanMethod=false)
 public class AppConfig {
     @Bean
     public MyService myService() {
     	return new MyServiceImpl(myRepository());
     }
     
     @Bean
     public MyRepository myRepository() {
     	return new MyRepositoryImpl();
     }
 }

 

 

✅ Spring이 CGLIB 프록시를 사용하지 않을 때 동작

  • myService() 메서드를 호출 할 때마다 myRepository() 메서드가 호출되어 새로운 MyRepository 인스턴스가 생성된다.
  • 이는 싱글톤 패턴이 깨지게 되는 것을 의미한다.

 

 Spring이 CGLIB 프록시를 사용 시

  • Spring은 AppConfig 클래스의 프록시를 생성하고, 이 프록시 클래스의 myService()와 myRepository() 메서드를 호출한다.
  • 첫번째 호출 시 실제로 메서드를 호출해 Bean을 생성하고, 이후 호출에서는 캐싱된 Bean을 반환한다.
  • 따라서 myService() 와 myRepository() 모두 동일한 Bean 인스턴스를 반환하게 되어 싱글톤이 보장된다.

 

✅ 요약

프록시 객체로 관리함에 따라 클래스가 싱글톤 패턴을 보장하도록 하며, @Bean 메서드의 결과로 반환 되는 객체가 항상 동일하게 유지되도록 한다.

반응형