인터페이스 기반 프록시와 클래스 기반 프록시
인터페이스 기반으로 필수로 생성할 때는, 기존 로직을 구현하고, 구현하는 곳에서 추가 기능을 사용하며 이를 스프링 빈에 등록해서 사용한다.
클래스 기반 프록시는 부모를 상속받아 기존 로직을 오버라이드해서 사용했다.
결론적으로, 인터페이스가 없어도 클래스 기반의 프록시가 잘 적용된다.
하지만 지금은 부가기능을 추가하려면, 추가할 기능 만큼 모든 프록시를 직접 생성해야하는 문제가 있다.
이를 해결하기 위해 나온 것이 동적 프록시이다.
1. 리플랙션
리플렉션은 클래스나 메서드의 메타정보를 사용해서 동적으로 호출하는 메서드를 변경할 수 있다.
다만, 이 기술을 써서 동적프록시가 동작한다는 것을 이해하는 것이 중요하지 실제로 사용할 땐 정말 주의를 해야한다.
만약 메서드 명을 잘못 적어서 사용한다면 컴파일 시점이 아닌 런타임 시점에 오류가 나오기 때문에, 일반적으로 사용하면 안된다.
간단한 예제로 알아보자.
@Slf4j
public class ReflectionTest {
@Test
void reflection0() {
Hello target = new Hello();
// 공통 로직1 시작
log.info("start");
String result1 = target.callA();
log.info("result={}", result1);
// 공통 로직1 종료
// 공통 로직2 시작
log.info("start");
String result2 = target.callB();
log.info("result={}", result2);
// 공통 로직1 종료
}
@Slf4j
static class Hello {
public String callA() {
log.info("callA");
return "A";
}
public String callB() {
log.info("callB");
return "B";
}
}
@Test
void reflection1()
throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
// 클래스 메타 정보 휙득, 내부 클래스는 구분을 위해 "$"사용
Class classHello = Class.forName("hello.proxy.jdkdynamic.ReflectionTest$Hello");
Hello target = new Hello();
//callA 메서드 정보
Method methodCallA = classHello.getMethod("callA");
Object result1 = methodCallA.invoke(target);
log.info("result1={}", result1);
//callB 메서드 정보
String parameter = "callB";
Method methodCallB = classHello.getMethod(parameter);
Object result2 = methodCallB.invoke(target);
log.info("result1={}", result2);
}
@Test
void reflection2() throws Exception {
// 클래스 메타 정보 휙득, 내부 클래스는 구분을 위해 "$"사용
Class classHello = Class.forName("hello.proxy.jdkdynamic.ReflectionTest$Hello");
Hello target = new Hello();
//callA 메서드 정보
Method methodCallA = classHello.getMethod("callA");
dynamicCall(methodCallA, target);
//callB 메서드 정보
Method methodCallB = classHello.getMethod("callB");
dynamicCall(methodCallB, target);
}
private void dynamicCall(Method method, Object target) throws Exception {
log.info("start");
/**
* String result1 = target.callA();
* 기존에는 위와 같이 메서드가 정해져있으므로 수정할 수 없었는데,
* 메타데이터를 사용하여 아래와 같이 추상화를 사용하여 동적으로 해결하는 기술이 리플렉션이다.
*/
Object result = method.invoke(target);
log.info("result={}", result);
}
}
JDK 동적 프록시
기존에는 부가기능을 추가하기 위해서 클래스에 대응되게 1개씩 무조건 만들었지만, 이를 동적프록시 기술을 사용하여 해결할 수 있다.
public interface AInterface {
String call();
}
@Slf4j
public class AImpl implements AInterface {
@Override
public String call() {
log.info("A 호출");
return "a";
}
}
public interface BInterface {
String call();
}
@Slf4j
public class BImpl implements BInterface {
@Override
public String call() {
log.info("B 호출");
return "b";
}
}
위와 같이 적용해야할 클래스가 2개인데, 부가 기능 클래스를 동적으로 만들어 1개만의 생성으로 해결할 수 있다.
JDK 동적프록시를 사용하기 위해선 InvocationHandler를 구현해야 사용할 수 있다.
@Slf4j
public class TimeInvocationHandler implements InvocationHandler {
private final Object target;
public TimeInvocationHandler(final Object target) {
this.target = target;
}
@Override // args: method 의 파라미터
public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
log.info("TimeProxy 실행");
long startTime = System.currentTimeMillis();
// method 호출을 동적으로 사용하기 때문에 이것이 가능함
Object result = method.invoke(target, args); // call, 이때 동적으로 실행
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("TimeProxy 종료 resultTime={}", resultTime);
return result;
}
}
이렇게 존재할 때, 테스트 단계에서 아래와 같이 위 부가기능 클래스를 둘 다 사용할 수 있다.
Proxy.netProxyInstance()는 JDK에서 직접 제공하는 방식의 프록시 생성 방법이다.
첫번째 인수로는 어떠한 클래스를 로더에 할지, 두번째 인수는 어떤 인터페이스를 기반으로 프록시를 할 지, 세번째 인수는 프록시에 사용되어야할 로직이 뭔지를 넣어주면 된다.
@Slf4j
public class JdkDynamicTest {
@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를 실행하면 핸들러의 proxy.call 여기서 "call" 이라는 메서드가 전달, */
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(AInterface.class.getClassLoader(), new Class[]{BInterface.class}, handler);
proxy.call();
log.info("targetClass={}", target.getClass());
log.info("proxyClass={}", proxy.getClass());
}
}
이렇게 사용한다면, 적용해야할 클래스 AImpl, BImpl 각각 프록시를 만들지 않고, JDK 동적 프록시를 사용해서 동적으로 만들었으며 TimeInvocationHandler는 공통으로 사용했다.
JDK 동적 프록시 도입 전
JDK 동적 프록시 도입 후
CGLIB
JDK 동적 프록시는 InvocationHandler를 제공했던 것처럼 CGLIB는 MethodInterceptor를 제공한다.
MethodInterceptor는 아래와 같이 생겼다.
public interface MethodInterceptor extends Callback {
Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable;
}
기존 데코레이터 패턴에서 작성했던 시간을 재는 기능을 구현해보자.
@Slf4j
public class ConcreteService {
public void call() {
log.info("ConcreteService 호출");
}
}
MethodInterceptor 구현
@Slf4j
public class TimeMethodInterceptor implements MethodInterceptor {
private final Object target;
public TimeMethodInterceptor(final Object target) {
this.target = target;
}
@Override
public Object intercept(final Object obj, final Method method, final Object[] args, final MethodProxy methodProxy)
throws Throwable {
log.info("TimeProxy 실행");
long startTime = System.currentTimeMillis();
// Object result = method.invoke(target, args); // call, 이때 동적으로 실행
Object result = methodProxy.invoke(target, args);
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("TimeProxy 종료 resultTime={}", resultTime);
return result;
}
}
Test
@Slf4j
public 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();
}
}
상속을 위해 Enhacner를 만들어 CGLIB를 만든다.
구체클래스를 기반으로 만들어야하므로 ConcreteService를 기반으로 프록시 객체를 만들 것이므로 setSuperclass를 해준다.
그 후 Callback을 정해 TimeMethodInterceptor를 적용시킨다. 그 후 프록시 객체를 생성한다.
위와 같이 사용하면 CGLIB 또한 동적으로 프록시객체를 생성할 수 있다.
하지만 여전히 아직 문제가 남았다.
인터페이스라면 InvocationHandler를, 클래스라면 MethodInterceptor를 구현해야한다.
매번 구현하고, 프록시 객체를 직접 생성해서 적용해야한다.
이를 해결해주는 것이 Spring이 제공하는 ProxyFactory이다.
다음 시간에 ProxyFactory를 알아보겠다.
'Spring > Spring AOP' 카테고리의 다른 글
Spring AOP 6. 포인트컷, 어드바이스, 어드바이저 (0) | 2024.03.20 |
---|---|
Spring AOP 5. ProxyFactory (0) | 2024.03.18 |
Spring AOP 3. 디자인 패턴: 데코레이터 패턴 (0) | 2024.03.06 |
Spring AOP 2. 디자인 패턴: 프록시패턴 (0) | 2024.03.06 |
Spring AOP 1. 디자인 패턴: 전략 패턴 - 템플릿 메서드 패턴 (0) | 2024.03.01 |