# JDK Dynamic Proxy와 CGLIB

### 💡**ProxyFactory**

스프링 AOP의 [ProxyFactory](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/aop/framework/ProxyFactory.html)는 프록시를 생성하는 과정에서 타겟이 하나 이상의 인터페이스를 구현하고 있다면 JDK Dynamic Proxy 방식으로 프록시가 생성하고, 그게 아니라면 CGLIB 방식으로 프록시가 생성해주는 팩토리입니다.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1689166110015/a055ccfc-0875-424a-87ee-cd7daeb92e60.png align="center")

클라이언트가 ProxyFactory로 생성된 프록시를 호출하면 JDK Dynamic Proxy의 경우 InvocationHandler의 invoke() 메서드가 실행되고, CGLIB의 경우 MethodInterceptor의 intercept() 메서드가 실행됩니다. 이후 Advice를 호출하면서 Target에 대한 부가기능을 적용시킬 수 있게 됩니다.

### 💡**JDK Dynamic Proxy**

```java
public interface MyService {
	void doSomething();
}

@Service
public class MyServiceImpl implements MyService {
	@Override
	public void doSomething() {
		// do something
	}
}
```

MyServiceImpl의 doSomething() 메서드 호출 전후로 로그를 찍기 위해, JDK Dynamic Proxy를 이용하여 프록시 객체를 생성해보겠습니다. JDK Dynamic Proxy는 Reflection API를 사용합니다. Proxy 클래스의 newProxyInstance로 프록시를 생성하는데, 아래와 같이 타겟의 인터페이스를 구현하는 프록시를 생성하게 됩니다.

```java
public class DynamicInvocationHandler implements InvocationHandler {
	@Override
	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
		MyService realSubject = new MyServiceImpl();
		log.debug("메서드 호출 이전");
		Object invoke = method.invoke(realSubject, args);
		log.debug("메서드 호출 이후");
		return invoke;
	}
}
```

```java
MyService myService = (MyService) Proxy.newProxyInstance(
				MyService.class.getClassLoader(), // 클래스로더
				new Class[]{MyService.class}, // 타겟의 인터페이스
				new DynamicInvocationHandler() // 타겟의 정보가 포함된 Handler
		);
```

InvocationHandler는 함수형 인터페이스이기 때문에 람다식을 이용하여 인라인으로 작성할 수도 있습니다.

```java
MyService myService = (MyService) Proxy.newProxyInstance(
				MyService.class.getClassLoader(), // 클래스로더
				new Class[]{MyService.class}, // 타겟의 인터페이스
				((proxy, method, args) -> {
					MyService realSubject = new MyServiceImpl();
					log.debug("메서드 호출 이전");
					Object invoke = method.invoke(realSubject, args);
					log.debug("메서드 호출 이후");
					return invoke;
				}) // 타겟의 정보가 포함된 Handler
		);
```

```bash
// myService.doSomething();

메서드 호출 이전
... Do something ...
메서드 호출 이후
```

myService의 doSomething() 메서드를 실행하게 되면, 메서드 호출 전후로 로그가 찍힙니다. 여기까지의 과정을 다시 한번 정리해봅시다.

1. Client가 MyService 타입 객체의 doSomthing()을 호출합니다. Client는 자신이 호출한 타겟이 프록시인지 아닌지 신경 쓸 필요 없습니다.
    
2. 동적으로 만들어진 프록시 객체는 Client가 요청한 메서드의 정보와 메서드의 파라미터를 InvocationHandler 구현체의 invoke() 메서드 파라미터로 전달하여 호출합니다.
    
3. InvocationHandler의 invoke() 메서드에서 Advice(로깅)를 적용하여 Target(realSubject)을 호출한 후, 반환합니다.
    

주의해야 할 것은 해당 Proxy Bean을 DI 할 때, 반드시 인터페이스 타입으로 지정해 줘야 합니다. 왜냐하면 클래스 타입으로 DI를 하는 경우, UnsatisfiedDependencyException이 발생합니다.

```java
@Controller
public class MyController {
  @Autowired
  private MyServiceImpl myServiceImpl; // Runtime Error!!
}


@Service
public class MyServiceImpl implements MyService {
	@Override
	public void doSomething() {
		// do something
	}
}
```

다른 객체지향적인 이유를 차치하고서라도, 이러한 런타임 에러를 피하기 위해선 의존성 주입 시 인터페이스 타입으로 지정해 주는 것이 좋습니다.

### 💡**CGLIB (Code Generator Library)**

CGLIB은 클래스의 바이트 코드를 조작하여 프록시 객체를 생성해주는 라이브러리입니다. org.springframework.cglib.proxy의 Enhancer 클래스를 이용하여 프록시를 생성할 수 있습니다.

```java
@Service
public class MyService {
	public void doSomething() {
		// do something
	}
}
```

MyService의 doSomething() 메서드에 대한 호출을 가로채는 프록시 클래스를 만들어봅시다. Enhancer 클래스를 사용하면 Enhancer 클래스의 setSuperclass() 메서드를 사용하여 MyService 클래스를 동적으로 확장하여 프록시를 만들 수 있습니다.

```java
public class MyMethodInterceptor implements MethodInterceptor {
	@Override
	public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
		log.debug("메서드 호출 이전");
		Object invoke = methodProxy.invokeSuper(o, objects);
		log.debug("메서드 호출 이후");
		return invoke;
	}
}
```

```java
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(MyService.class); // 타겟 클래스
enhancer.setCallback(new MyMethodInterceptor()); // 핸들러
MyService proxy = (MyService) enhancer.create(); // 프록시 생성
```

MethodInterceptor 또한 다음과 같이 함수형으로 사용할 수 있습니다.

```java
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(MyService.class); // 타겟 클래스
enhancer.setCallback((MethodInterceptor) (Object o, Method method, Object[] objects, MethodProxy methodProxy) -> {
    log.debug("메서드 호출 이전");
    Object invoke = methodProxy.invokeSuper(o, objects);
    log.debug("메서드 호출 이후");
    return invoke;
}); // 핸들러
```

```bash
// myService.doSomething();

메서드 호출 이전
... Do something ...
메서드 호출 이후
```

마찬가지로 메서드 호출 전후로 로그가 찍힙니다. CGLIB은 어떻게 프록시를 생성했는지 다시 정리해 봅시다.

1. Client가 MyService 타입 객체의 doSomthing()을 호출합니다. Client는 자신이 호출한 타겟이 프록시인지 아닌지 신경 쓸 필요 없습니다.
    
2. enhancer가 Target 클래스(MemberService)를 상속 받습니다.
    
3. enhancer에서 setCallback()을 통해 intercept() 메서드 내부에서 Advice 로직을 적용하여 Target(MyService)을 호출한 후, 반환합니다.
    

### 💡**JDK Dynamic Proxy vs CGLIB**

CGLIB은 최초로 호출됐을 때 바이트 코드를 조작하고, 이후 호출 시 조작된 바이트 코드를 재사용하기 때문에 성능적으로 JDK Dynamic Proxy에 비해 우수합니다. 차이가 어느정도인지 자세히 알고 싶으시다면, Baeldung에서도 사용된 [벤치마크](https://web.archive.org/web/20150520175004/https://docs.codehaus.org/display/AW/AOP+Benchmark)를 참고해주세요.

### 💡**인터페이스를 구현해도 CGLIB만 사용하는데요?**

CGLIB은 다음과 같은 문제점을 갖고 있었습니다.

1. CGLIB 의존성을 추가해야 함.
    
2. default 생성자가 꼭 필요함.
    
3. 생성자가 두 번 호출됨.
    

의존성을 추가해야 하는 부분은 [Spring 3.2부터 Spring Core에 CGLIB을 포함](https://docs.spring.io/spring-framework/docs/3.2.x/javadoc-api/org/springframework/cglib/package-summary.html)시켰습니다. [Spring 4.0부터 Objensis 라이브러리를 통해 default 생성자가 필요 없고, 생성자가 두 번 호출되던 문제를 해결](https://docs.spring.io/spring-framework/reference/core/aop/proxying.html)했습니다.

```json
// spring-boot-autoconfigure의 spring-configuraion-metadata.json
...
{
    "name": "spring.aop.proxy-target-class",
    "type": "java.lang.Boolean",
    "description": "Whether subclass-based (CGLIB) proxies are to be created (true), as opposed to standard Java interface-based proxies (false).",
    "defaultValue": true
},
```

CGLIB이 갖고 있던 여러 문제점이 해결되었기 때문에, Spring boot 1.4 이상에서는 기본적으로 인터페이스를 구현하더라도 [CGLIB으로 Proxy를 생성](https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-1.4-Release-Notes#transactional-default-to-cglib-proxies)합니다. 성능이 더 우수하니깐요.

JDK 동적 프록시를 사용하시려면 `@EnableAspectJAutoProxy` 어노테이션의 proxyTargetClass 옵션을 false로 주시거나, 프로퍼티에서 spring.aop.proxy-target-class=false를 적용시켜주시면 됩니다.

---

### 🎯**정리**

* 타겟이 하나 이상의 인터페이스를 구현하는 경우 JDK Dynamic Proxy를, 아닌 경우 CGLIB Proxy를 사용합니다.
    
* CGlib은 바이트 코드를 조작하여 프록시를 생성해 주기 때문에 JDK Dynamic Proxy보다 성능이 좋습니다.
    
* Spring Boot에서는 기본적으로 CGLIB을 사용합니다.
    

---

### 🔖**참고**

* [https://docs.spring.io/spring-framework/docs/4.0.1.RELEASE/spring-framework-reference/htmlsingle/#aop-understanding-aop-proxies](https://docs.spring.io/spring-framework/docs/4.0.1.RELEASE/spring-framework-reference/htmlsingle/#aop-understanding-aop-proxies)
    
* [https://www.baeldung.com/cglib](https://www.baeldung.com/cglib)
    
* [https://gmoon92.github.io/spring/aop/2019/04/20/jdk-dynamic-proxy-and-cglib.html](https://gmoon92.github.io/spring/aop/2019/04/20/jdk-dynamic-proxy-and-cglib.html)
