공통 기능(AOP)을 핵심 로직에 깔끔하게 적용하고 싶다면, 결국 프록시가 중요한 역할을 합니다.
스프링을 사용하다 보면 AOP(Aspect-Oriented Programming)를 통해 로그, 보안, 트랜잭션 같은 공통 기능을 핵심 비즈니스 로직에서 분리되어 있습니다.
그렇다면 ‘어떻게’ 분리가 가능하냐고 물으신다면, 그 비결은 바로 프록시(Proxy)를 이용하기 때문입니다.
이 글에서는 스프링 AOP가 사용하는 JDK 동적 프록시, CGLIB 프록시, 그리고 그 중심에서 활약하는 InvocationHandler, MethodInterceptor, ProxyFactoryBean이 어떻게 맞물려 돌아가는지 정리해보겠습니다.
스프링 AOP의 프록시 생성 방식에는 대표적으로 두 가지가 있습니다.
InvocationHandler
가 담당.MethodInterceptor
가 담당.final
로 선언된 클래스나 메서드는 상속 불가 → 프록시 불가능.스프링 AOP에서는 내부적으로 JDK 동적 프록시를 쓸 땐 InvocationHandler를, CGLIB를 쓸 땐 MethodInterceptor를 통해 메서드를 가로챕니다.
두 녀석 다 메서드가 호출됐을 때 무슨 작업(어드바이스, 로직)을 어떻게 껴넣을지 를 담당합니다
public class JdkDynamicAopProxy implements InvocationHandler {
private final AdvisedSupport advised;
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
List<MethodInterceptor> chain = advised.getInterceptorsAndDynamicInterceptionAdvice(method);
if (chain.isEmpty()) {
// 적용할 어드바이스가 없다면 바로 타겟 메서드 호출
return method.invoke(advised.getTargetSource().getTarget(), args);
} else {
// 어드바이스가 있다면 순서대로 실행
MethodInvocation invocation = new ReflectiveMethodInvocation(
advised.getTargetSource().getTarget(), method, args, chain);
return invocation.proceed();
}
}
}
invoke
로 몰려옴 → 어떤 어드바이스를 적용할지 판단하고, 타겟 메서드 실행 여부를 조정합니다.public class MyMethodInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("Before " + method.getName());
Object result = proxy.invokeSuper(obj, args);
System.out.println("After " + method.getName());
return result;
}
}
스프링이 제공하는 ProxyFactoryBean
은 프록시 설정을 간단하게 해주는 공장 같은 클래스입니다.
setTarget
(타겟 객체 지정) addAdvice
(어드바이스 추가) setProxyTargetClass
(true
= CGLIB, false
= JDK 동적 프록시) @Configuration
public class AppConfig {
@Bean
public ProxyFactoryBean myServiceProxy() {
ProxyFactoryBean proxyFactoryBean = new ProxyFactoryBean();
proxyFactoryBean.setTarget(myServiceImpl());
proxyFactoryBean.addAdvice(new LoggingInterceptor());
proxyFactoryBean.setProxyTargetClass(false); // JDK 동적 프록시
return proxyFactoryBean;
}
@Bean
public MyServiceImpl myServiceImpl() {
return new MyServiceImpl();
}
}
설정 끝나면, myServiceProxy
Bean을 주입받았을 때 사실은 프록시 객체가 들어오게 됩니다.
아무리 프록시가 있어도, 포인트컷(적용 지점)에서 벗어나면 어드바이스가 동작하지 않습니다. 즉, MethodInterceptor 체인이 비어있는 경우에는 타겟 메서드가 바로 호출돼버립니다.
ProxyFactoryBean
썼지만 addAdvice()
를 안 해놨다면? → 어드바이스가 없으니 바로 타겟 호출.public interface MyService {
void performTask(); // AOP 적용 대상
void anotherTask(); // AOP 미적용 가능
}
@Service
public class MyServiceImpl implements MyService {
@Override
public void performTask() {
System.out.println("Performing Task");
}
@Override
public void anotherTask() {
System.out.println("Executing Another Task");
}
}
@Aspect
@Component
public class LoggingAspect {
@Before("execution(* com.example.MyService.performTask(..))")
public void logBeforePerformTask(JoinPoint joinPoint) {
System.out.println("Before performTask");
}
}
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
@Bean
public ProxyFactoryBean myServiceProxy() {
ProxyFactoryBean proxyFactoryBean = new ProxyFactoryBean();
proxyFactoryBean.setTarget(myServiceImpl());
// 간단한 Before Advice
proxyFactoryBean.addAdvice((MethodBeforeAdvice) (method, args, target) -> {
System.out.println("Before method: " + method.getName());
});
proxyFactoryBean.setProxyTargetClass(false); // JDK 동적 프록시
return proxyFactoryBean;
}
@Bean
public MyServiceImpl myServiceImpl() {
return new MyServiceImpl();
}
}
@SpringBootApplication
public class AopDemoApplication implements CommandLineRunner {
@Autowired
private MyService myService;
public static void main(String[] args) {
SpringApplication.run(AopDemoApplication.class, args);
}
@Override
public void run(String... args) throws Exception {
myService.performTask(); // 어드바이스 적용
myService.anotherTask(); // 어드바이스 미적용
}
}
실행 결과(콘솔 출력):
Before performTask
Before method: performTask
Performing Task
Executing Another Task
performTask()
에서는 포인트컷에 매칭되고, 프록시가 MethodBeforeAdvice를 실행 후 실제 메서드를 호출합니다.anotherTask()
는 포인트컷 매칭이 안 되므로, 어드바이스 체인이 비어있어 바로 실행됩니다.@EnableAspectJAutoProxy(proxyTargetClass = true)
로 강제도 가능합니다.AOP로 로깅, 보안, 트랜잭션 같은 공통 기능을 한 곳에서 관리하고, 핵심 로직을 깔끔하게 유지할 수 있는 이유에는 오늘 살펴본 프록시와 InvocationHandler/MethodInterceptor가 핵심적인 역할을 맡고 있기 때문입니다.
궁금한 점이나 피드백이 있으면 언제든 댓글로 달아주세요!
참고
잘 읽었습니다~~