[Java] 동적 프록시 (JDK 동적 프록시, CGLIB)
JDK 동적 프록시
인터페이스 기반으로 프록시를 동적으로 만들어 사용하기 때문에 인터페이스가 필수이다.
자바에서는 리플렉션을 사용해 proxy 클래스를 제공해주고 있다.
리플렉션(Refliction)이란
구체적인 클래스 타입을 알지 못해도 그 클래스의 메서드, 타입, 변수들에 접근할 수 있도록 해주는 자바 API 이다.
사용 시기는 동적으로 클래스를 사용해야할 때 사용하는데
작성 시점에는 어떤 클래스를 사용할지 모르지만 런타임 시점에 가져와 실행해야하는 경우에 필요하다.
단점은 메서드에 값이 잘못 입력 되어도 컴파일 오류가 발생하지 않고 해당 코드를 직접 실행하는 시점에 런타임 오류가 발생해서 그때 오류를 알 수 있다.
이러한 이유로 리플렉션을 잘 사용하지 않고, 만약 사용하게 된다면 공통 처리가 필요한 부분에는 주의해서 사용해야 한다.
JDK 동적 프록시 설명
아래 이미지는 JDK 동적 프록시 없이 직접 프록시를 만들어서 사용할 때와 JDK 동적 프록시를 사용할 때의 차이를 그림으로 표현한 것이다.
프록시 도입 전에는 클라이언트가 proxy를 직접 호출하고 그 proxy가 인터페이스를 호출했는데
프록시 도입 후에는 클라이언트는 $proxy1번을 호출하고
프록시는 핸들러의 invoke 를 호출하고
인터페이스에 구현되어 있는 프록시를 호출한다.
프록시 구현 예제
예제에 사용할 클래스와 인터페이스를 생성한다.
package hello.proxy.app.food;
public interface Food {
void eat();
void drink();
}
public class Cafe implements Food{
@Override
public void eat() {
System.out.println("카페에서 베이커리 먹기");
}
@Override
public void drink() {
System.out.println("카페에서 커피 마시기");
}
}
package hello.proxy.app.food;
public class Restaurant implements Food{
@Override
public void eat() {
System.out.println("식당에서 밥먹기");
}
@Override
public void drink() {
System.out.println("식당에서 물마시기");
}
}
프록시 객체를 생성할 때 핸들러에 InvocationHandler를 상속받아 구현한다.
package hello.proxy.config.jdkdynamic.code.handler;
import lombok.extern.slf4j.Slf4j;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
@Slf4j
public class FoodHandler implements InvocationHandler {
private final Object target;
public FoodHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object result = null;
try {
String message = method.getDeclaringClass().getSimpleName() + "." +
method.getName() + "()";
log.info("begin >> {}", message);
//로직 호출
result = method.invoke(target, args);
return result;
} catch (Exception e) {
log.error("exception >> {}", e);
throw e;
}
}
}
테스트 코드
프록시 클래스의 newProxyInstance() 를 사용한다.
package hello.proxy.jdkdynamic;
import hello.proxy.app.food.Cafe;
import hello.proxy.app.food.Food;
import hello.proxy.app.food.Restaurant;
import hello.proxy.config.jdkdynamic.code.handler.FoodHandler;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import java.lang.reflect.Proxy;
@Slf4j
public class JdkDynamicProxyTest {
@Test
void JdkDynamicProxyTest(){
Food cafe = (Food) Proxy.newProxyInstance(Food.class.getClassLoader(),
new Class[]{Food.class},
new FoodHandler(new Cafe()));
Food restaurant = (Food) Proxy.newProxyInstance(Food.class.getClassLoader(),
new Class[]{Food.class},
new FoodHandler(new Restaurant()));
cafe.eat();
log.info("cafe targetClass={}", cafe.getClass());
cafe.drink();
log.info("cafe targetClass={}", cafe.getClass());
log.info("-------");
restaurant.eat();
log.info("restaurant targetClass={}", restaurant.getClass());
restaurant.drink();
log.info("restaurant targetClass={}", restaurant.getClass());
}
}
실행해보면 아래와 같이 나오는 것을 확인할 수 있다.
getClass()로 로그를 남겨서 확인해보면 $proxy를 호출해서 사용하는 것을 확인할 수 있다.
JDK 동적 프록시는 인터페이스가 필수적인데 인터페이스 없이 클래스만 있는 경우에 동적 프록시를 사용하고 싶다면 CGLib를 사용하면 된다.
CGLIB
CGLIB 는 인터페이스가 아닌 클래스 기반으로 프록시를 생성하는 방식이다.
인터페이스가 아닌 클래스를 대상으로 동작이 가능하고 바이트 코드를 조작해 프록시를 만들 수 있다.
바이트 코드를 사용하기 때문에 동적 프록시보다 성능이 우수하다는 장점이 있다.
외부 라이브러리 이지만 스프링 내부 소스 코드에 포함이 되어있지만 실제로 사용하는 경우는 많이 없어서 개념만 알 수 있게 정리해 보았다.
사용 예제
CGLIB 에서는 MethodInterceptor를 사용한다.
public interface MethodInterceptor extends Callback {
Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable;
}
제약 사항
클래스 기반 프록시는 상속을 사용하기 때문에 몇가지 제약이 있다.
- 부모 클래스의 생성자를 체크 필요 -> 자식 클래스를 동적으로 생성하기 때문에 기본 생성자가 필요
- 클래스에 final 키워드가 붙으면 상속 불가능 -> CGLIB에서는 예외발생
- 매서드에 final 키워드가 붙으면 해당 메서드를 오버라이딩 할 수 없음 -> CGLIB에서는 프록시 로직이 동작하지 않음
프록시 팩토리를 사용하면 CGLIB 단점을 해결하고 편리하게 사용할 수 있다.
+
개발하다가 실제 사용되고 있는 부분을 확인할 수 있었다.
DashBoardServiceImpl 클래스에서 Querydsl용 클래스를 만들어서 해당 코드로 이동할 때 확인이 가능했다.
CGLIB의 단어가 보이는데 강의에서 설명한 Enhancer 의 단어는 보이지 않았고 Bound, Callback 의 단어들이 보였다.
프록시 생성에 필요한 모듈은 Enhancer, Callback, CallbackFilter가 있다고 한다.
Enhancer 는 프록시 객체를 생성하는 역할이고
Callback은 프록시 호출이 들어오면 모든 호출을 가로채 callback 자체의 부가 로직 수행 및 origin 메소드 호출하는 역할을 한다고 한다.
Callback 구현체중에서 MethodInterceptor가 일반적으로 많이 사용된다고 한다.