동적 프록시와 AOP의 기반 기술
AOP(Aspect Oriented Programming)를 구현하기 위해서는 핵심 기술인 동적 프록시(Dynamic Proxy)에 대한 이해가 필수적입니다. 동적 프록시는 런타임 시점에 타겟 객체를 감싸는 프록시 객체를 생성하여, 기존 코드의 수정 없이 메서드 호출을 가로채고 추가적인 로직을 적용할 수 있게 해줍니다. 이를 통해 로깅, 트랜잭션, 보안 등 횡단 관심사(Cross-cutting Concerns)를 비즈니스 로직으로부터 분리할 수 있습니다.
본문에서는 프록시 패턴의 두 가지 주요 구현 방식인 JDK 동적 프록시와 CGLIB를 사용하여 메서드를 인터셉트하고, 포인트컷 표현식을 통해 대상 메서드를 선별적으로 처리하는 방법을 다룹니다.
핵심 설계 구조
AOP의 핵심은 메서드 호출을 가로채는 '프록시'와 특정 조건에 부합하는 메서드를 찾아내는 '포인트컷(Pointcut)', 그리고 가로챈 메서드에 적용할 로직인 '어드바이스(Advice)'의 결합입니다. 이들을 유연하게 연결하기 위해 다음과 같은 구조로 설계합니다.
- 포인트컷 표현식 처리: AspectJ의 표현식 파싱 기능을 활용하여 클래스 및 메서드 필터링 기능을 구현합니다.
- 프록시 설정 정보 관리: 타겟 객체, 메서드 인터셉터, 메서드 매처 등을 하나의 설정 객체로 묶어 관리합니다.
- 프록시 팩토리: JDK 방식과 CGLIB 방식을 추상화하여 일관된 방식으로 프록시 객체를 생성합니다.
구현 상세
1. 포인트컷 및 메서드 매처 구현
먼저 특정 클래스나 메서드가 AOP의 대상인지 판별하는 인터페이스를 정의합니다. 이는 Pointcut, ClassFilter, MethodMatcher로 구성됩니다.
public interface Pointcut {
ClassFilter getClassFilter();
MethodMatcher getMethodMatcher();
}
public interface ClassFilter {
boolean matches(Class<?> clazz);
}
public interface MethodMatcher {
boolean matches(Method method, Class<?> targetClass);
}이 인터페이스들의 구현체로 AspectJ 라이브러리를 사용하여 표현식을 파싱하는 클래스를 작성합니다. 이 클래스는 전달받은 표현식 문자열을 분석하여 매칭 여부를 반환합니다.
public class AspectJExpressionPointcut implements Pointcut, ClassFilter, MethodMatcher {
private static final Set<PointcutPrimitive> SUPPORTED_PRIMITIVES = new HashSet<>();
static {
SUPPORTED_PRIMITIVES.add(PointcutPrimitive.EXECUTION);
}
private final PointcutExpression pointcutExpression;
public AspectJExpressionPointcut(String expression) {
PointcutParser parser = PointcutParser
.getPointcutParserSupportingSpecifiedPrimitivesAndUsingSpecifiedClassLoaderForResolution(
SUPPORTED_PRIMITIVES, this.getClass().getClassLoader());
this.pointcutExpression = parser.parsePointcutExpression(expression);
}
@Override
public boolean matches(Class<?> clazz) {
return pointcutExpression.couldMatchJoinPointsInType(clazz);
}
@Override
public boolean matches(Method method, Class<?> targetClass) {
return pointcutExpression.matchesMethodExecution(method).alwaysMatches();
}
@Override
public ClassFilter getClassFilter() { return this; }
@Override
public MethodMatcher getMethodMatcher() { return this; }
}2. 프록시 설정 및 메서드 인보케이션
프록시 생성에 필요한 메타정보(타겟, 인터셉터, 매처)를 보관하는 컨텍스트 클래스와, 실제 메서드 호출을 감싸는 래퍼 클래스를 정의합니다.
public class ProxyConfig {
private TargetSource targetSource;
private MethodInterceptor methodInterceptor;
private MethodMatcher methodMatcher;
// Getters and Setters...
}
public class TargetSource {
private final Object target;
public TargetSource(Object target) {
this.target = target;
}
public Class<?>[] getTargetClass() {
return target.getClass().getInterfaces();
}
public Object getTarget() {
return target;
}
}메서드 인보케션은 리플렉션 호출을 추상화하여 체이닝(Chain)이 가능하도록 돕습니다.
public interface MethodInvocation {
Object proceed() throws Throwable;
Object getThis();
Method getMethod();
Object[] getArguments();
}
public class ReflectiveMethodInvocation implements MethodInvocation {
protected final Object target;
protected final Method method;
protected final Object[] arguments;
public ReflectiveMethodInvocation(Object target, Method method, Object[] arguments) {
this.target = target;
this.method = method;
this.arguments = arguments;
}
@Override
public Object proceed() throws Throwable {
return method.invoke(target, arguments);
}
// Other getters...
}3. JDK 동적 프록시 구현
JDK 프록시는 인터페이스 기반으로 동작합니다. InvocationHandler를 구현하여 메서드 호출을 가로챕니다.
public class JdkDynamicProxyFactory implements AopProxy, InvocationHandler {
private final ProxyConfig config;
public JdkDynamicProxyFactory(ProxyConfig config) {
this.config = config;
}
@Override
public Object getProxy() {
return Proxy.newProxyInstance(
Thread.currentThread().getContextClassLoader(),
config.getTargetSource().getTargetClass(),
this
);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 메서드 매칭 검사
if (config.getMethodMatcher().matches(method, config.getTargetSource().getTarget().getClass())) {
// 인터셉터 실행
MethodInterceptor interceptor = config.getMethodInterceptor();
return interceptor.invoke(new ReflectiveMethodInvocation(config.getTargetSource().getTarget(), method, args));
}
// 매칭되지 않으면 타겟 메서드 직접 실행
return method.invoke(config.getTargetSource().getTarget(), args);
}
}4. CGLIB 프록시 구현
CGLIB는 클래스 기반 프록시를 지원합니다. Enhancer와 MethodInterceptor를 사용하여 구현합니다.
public class CglibProxyFactory implements AopProxy {
private final ProxyConfig config;
public CglibProxyFactory(ProxyConfig config) {
this.config = config;
}
@Override
public Object getProxy() {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(config.getTargetSource().getTarget().getClass());
enhancer.setInterfaces(config.getTargetSource().getTargetClass());
enhancer.setCallback(new CglibInterceptor(config));
return enhancer.create();
}
private static class CglibInterceptor implements MethodInterceptor {
private final ProxyConfig config;
public CglibInterceptor(ProxyConfig config) {
this.config = config;
}
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
if (config.getMethodMatcher().matches(method, config.getTargetSource().getTarget().getClass())) {
return config.getMethodInterceptor().invoke(
new ReflectiveMethodInvocation(config.getTargetSource().getTarget(), method, args)
);
}
return proxy.invokeSuper(obj, args);
}
}
}테스트 및 검증
구현한 프록시 기능을 검증하기 위해 간단한 서비스 인터페이스와 구현체를 정의합니다.
public interface PaymentService {
String pay(String orderId);
}
public class PaymentServiceImpl implements PaymentService {
@Override
public String pay(String orderId) {
System.out.println("결제 처리 중: " + orderId);
return "Success-" + orderId;
}
}메서드 실행 시간을 측정하는 인터셉터를 작성합니다.
public class TimerInterceptor implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
long start = System.currentTimeMillis();
try {
return invocation.proceed();
} finally {
long duration = System.currentTimeMillis() - start;
System.out.println("[LOG] 메서드 " + invocation.getMethod().getName() + " 실행 시간: " + duration + "ms");
}
}
}마지막으로 JDK와 CGLIB 프록시를 생성하여 테스트합니다.
@Test
public void testProxyExecution() {
// 1. 타겟 객체 생성
PaymentService target = new PaymentServiceImpl();
// 2. 프록시 설정
ProxyConfig proxyConfig = new ProxyConfig();
proxyConfig.setTargetSource(new TargetSource(target));
proxyConfig.setMethodInterceptor(new TimerInterceptor());
// execution 표현식을 통한 메서드 매칭 설정
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut("execution(* com.example.service.*.*(..))");
proxyConfig.setMethodMatcher(pointcut.getMethodMatcher());
// 3. JDK 프록시 테스트
AopProxy jdkProxy = new JdkDynamicProxyFactory(proxyConfig);
PaymentService jdkProxyInstance = (PaymentService) jdkProxy.getProxy();
String jdkResult = jdkProxyInstance.pay("ORD-001");
System.out.println("JDK 결과: " + jdkResult);
// 4. CGLIB 프록시 테스트
AopProxy cglibProxy = new CglibProxyFactory(proxyConfig);
PaymentService cglibProxyInstance = (PaymentService) cglibProxy.getProxy();
String cglibResult = cglibProxyInstance.pay("ORD-002");
System.out.println("CGLIB 결과: " + cglibResult);
}위 테스트 코드를 실행하면, TimerInterceptor가 개입하여 메서드 실행 전후에 로깅이 발생하고 실제 비즈니스 로직이 수행됨을 확인할 수 있습니다. 이 과정을 통해 프록시 객체가 타겟 객체를 대체하고, 지정된 메서드에 대해 부가 기능을 주입하는 AOP의 핵심 메커니즘을 확인했습니다.