✅ ProxyFactoryBean
스프링에서 프록시 오브젝트를 생성해주는 기술을 추상화한 팩토리 빈을 제공하고 있습니다. 스프링의 ProxyFactoryBean은 스프링 프레임워크에서 제공하는 기능 중 하나로, 동적 프록시를 생성하고 Bean으로 등록하여 관리하는 팩토리 빈(FactoryBean)입니다. ProxyFactoryBean을 사용하면 AOP(Aspect-Oriented Programming)를 구현하여 메소드 호출을 가로채고 부가적인 기능을 추가할 수 있습니다. 스프링에서 제공하는 팩토리 빈을 통해서, 기존에 작성했던 팩토리 빈의 한계를 극복할 수 있습니다.
![](https://blog.kakaocdn.net/dn/u0lNV/btsrH995TDJ/JJO0iZMvf9bvklB80YhPIK/img.png)
- Advice: 타겟이 필요 없는 순수한 부가 기능
Advice는 AOP에서 타겟 오브젝트에 적용하는 부가적인 기능을 담은 객체를 총칭합니다. 다양한 종류의 어드바이스가 있으며, 대표적으로는
BeforeAdvice
,AfterReturningAdvice
,AfterThrowingAdvice
,AroundAdvice
등이 있습니다. ProxyFactoryBean은 Advice를 설정하고, 필요한 경우 MethodInterceptor 인터페이스를 구현한 객체를 어드바이스로 설정합니다. Advice는 자신이 공유돼야 하므로 타겟 정보를 가지고 있지 않습니다. ProxyFactoryBean은 Advice 정보를 기반으로 동적 프록시 객체를 생성하고 관리합니다.
- MethodInterceptor ← { InvocationHandler의 발전된 형태 }
MethodInterceptor는 개발자가 정의한 구체적인 어드바이스 객체를 의미하는 인터페이스입니다. 개발자는
MethodInterceptor
를 구현하는 클래스를 작성하여 프록시에 원하는 부가 기능을 정의할 수 있습니다.MethodInterceptor
는 org.aopalliance.intercept.MethodInterceptor 인터페이스를 구현합니다. 구현해야 할 메소드는invoke(MethodInvocation invocation)
하나뿐이며, 메소드 실행 전후에 원하는 로직을 수행할 수 있습니다.invocation.proceed()
메소드를 사용하여 타겟 클래스의 메소드를 호출합니다. MethodInterceptor은MethodInvocation 덕분에 타겟에 대한 정보를 알고 있지 않아도 됩니다.
- Pointcut : 부가기능 적용 대상 메소드 선정 방법
Pointcut은 어떤 메소드를 프록시로 감싸야 하는지를 지정하는 역할을 합니다. Pointcut는 특정한 메소드를 선택하기 위한 규칙을 정의하는 것으로, AOP의 핵심 개념 중 하나입니다. 스프링에서는 org.springframework.aop.Pointcut 인터페이스를 구현하여 Pointcut을 정의합니다. 주요한 메소드는
matches(Method method, Class<?> targetClass)
이며, 이를 통해 어떤 메소드를 프록시로 감쌀지 결정합니다.
- MethodInvocation
MethodInvocation은 AOP 프레임워크에 의해 타겟 객체의 메소드 호출을 가로채고 제어하기 위한 정보를 제공하는 객체입니다. 이 정보는 타겟 객체의 구현 인터페이스나 클래스에 기반하여 생성되며, 타겟 메소드의 이름, 매개변수, 반환 타입 등을 포함합니다. MethodInterceptor을 구현한 클래스는 이 MethodInvocation을 활용하여 부가 기능을 구현하고 타겟 메소드를 호출할 수 있습니다.
👉 ProxyFactoryBean은 MethodInterceptor, Advice, Pointcut을 함께 사용하여 AOP를 구현합니다. ProxyFactoryBean은 Advice 정보를 기반으로 동적 프록시 객체를 생성하고 관리합니다. 개발자는 순수한 부가 기능을 담은 MethodInterceptor 인터페이스를 구현한 구체 클래스를 작성하고, 필요한 경우 메소드 선정 알고리즘을 정의하는 Pointcut을 설정하면 됩니다.
❓DynamicProxy 객체는 실제로 MethodInvocation 객체를 가지고 있을까?
![](https://blog.kakaocdn.net/dn/bUUi6n/btsrEzH1fBv/Podx5JOiI7EkkZkvJutS4K/img.png)
ProxyFactoryBean을 통해 생성되는 다이나믹 프록시 객체는 Spring AOP의 기본 구현체인 org.springframework.aop.framework.JdkDynamicAopProxy
혹은 org.springframework.aop.framework.CglibAopProxy
클래스의 인스턴스입니다. ProxyFactoryBean을 통해서 다이나믹 프록시 객체가 생성될 때, MethodInvocation도 생성된다고 하는데 실제로 가지고 있는지 확인했습니다. 실제로는 invoke() 메소드가 실행될 때 MethodInvocation 객체가 생성되고 콜백 오브젝트로써 동작합니다.
✅ ProxyFactoryBean 학습 테스트
- UppercaseAdvice
package com.jhcode.spring.ch6.learningtest.jdk.proxy; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; public class UppercaseAdvice implements MethodInterceptor { @Override public Object invoke(MethodInvocation invocation) throws Throwable { //타겟 클래스의 메소드 호출 //invocation은 ProxyFactoryBean에서 Proxy 객체가 생성되고 //invoke() 메소드가 호출될 때 생성되어 콜백 오브젝트로 템플릿 역할을 하는 Advice에게 전달된다 String ret = (String) invocation.proceed(); //부여할 부가 기능 return ret.toUpperCase(); } }
Advice를 상속받은, Interceptor을 상속 받은 MethodInterceptor 인터페이스를 구현하여 어떤 순수한 부가 기능만을 가진 클래스를 작성합니다. 이는 Proxy를 통해서 invoke() 메소드가 호출되어, 순수한 부가 기능을 부여하고 Proxy 객체로부터 전달받은 MethodInvocation 객체를 통해서 타겟 객체의 메소드를 호출하게 됩니다.
- DynamicProxyTest
@Test public void proxyFactoryBean() { ProxyFactoryBean pfBean = new ProxyFactoryBean(); // 타겟 오브젝트 지정 pfBean.setTarget(new HelloTarget()); //advice 지정, 여러개 동시 적용 가능 pfBean.addAdvice(new UppercaseAdvice()); Hello proxiedHello = (Hello) pfBean.getObject(); assertEquals(proxiedHello.sayHello("Toby"), "HELLO TOBY"); assertEquals(proxiedHello.sayHi("Toby"), "HI TOBY"); assertEquals(proxiedHello.sayThankYou("Toby"), "THANK YOU TOBY"); }
ProxyFactoryBean의 타겟과 어드바이스를 지정합니다. ProxyFactoryBean은 타겟과 어드바이스 정보를 가지고 Proxy 객체를 생성하고, Proxy 객체가 어드바이스의 invoke() 메소드를 호출할 때 내부적으로 MethodInvocation 객체를 생성하여 타겟 객체의 메소드를 호출할 수 있도록 정보를 전달하게 됩니다. 그렇기 때문에 Advice는 타겟에 대한 정보를 알고 있지 않아도 되는 것입니다.
- Pointcut을 적용한 ProxyFactoryBean
@Test public void pointcutAdvisorToProxyFactoryBean() { ProxyFactoryBean pfBean = new ProxyFactoryBean(); // 타겟 오브젝트 지정 pfBean.setTarget(new HelloTarget()); //advice 생성 UppercaseAdvice advice = new UppercaseAdvice(); //pointcut 생성 NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut(); pointcut.setMappedName("sayH*"); // sayH 로 시작하는 타겟의 메소드가 호출될 때만 Advisor을 적용함 // Advisor = Advice + Pointcut DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(pointcut, advice); // ProxyFactoryBean에 Advisor 주입 pfBean.addAdvisor(advisor); // Proxy 생성 -> 타겟 객체가 구현한 인터페이스 타입으로 받음 Hello proxiedHello = (Hello) pfBean.getObject(); assertEquals(proxiedHello.sayHello("Toby"), "HELLO TOBY"); assertEquals(proxiedHello.sayHi("Toby"), "HI TOBY"); assertEquals(proxiedHello.sayThankYou("Toby"), "Thank You Toby"); }
Advisor는 스프링 AOP에서 부가 기능(어드바이스)와 적용 대상(포인트컷)을 묶어주는 객체입니다. Advisor는 Advice와 Pointcut을 조합하여 한 번에 관리할 수 있도록 도와줍니다.
💡Advisor : advice + pointcut을 묶어서 관리하는 객체 Advice : 순수한 부가 기능만 담긴 객체 Pointcut : 부가 기능을 적용할 메소드 선정 알고리즘이 담긴 객체
✅ ProxyFactoryBean 적용
- TransactionAdvice
package com.kitec.springframe.ch6.study4_2.springframe.service; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.DefaultTransactionDefinition; public class TransactionAdvice implements MethodInterceptor { PlatformTransactionManager transactionManager; public void setTransactionManager(PlatformTransactionManager transactionManager) { this.transactionManager = transactionManager; } @Override public Object invoke(MethodInvocation invocation) throws Throwable { TransactionStatus status = this.transactionManager.getTransaction(new DefaultTransactionDefinition()); try { Object ret = invocation.proceed(); this.transactionManager.commit(status); return ret; } catch (RuntimeException e) { this.transactionManager.rollback(status); throw e; } } }
- TestServiceFactory, Bean 설정
@Bean public TransactionAdvice transactionAdvice() { TransactionAdvice advice = new TransactionAdvice(); advice.setTransactionManager(transactionManager()); return advice; } @Bean public NameMatchMethodPointcut transactionPointcut() { NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut(); pointcut.setMappedName("upgrade*"); return pointcut; } @Bean public DefaultPointcutAdvisor transactionAdvisor() { DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(); advisor.setAdvice(transactionAdvice()); advisor.setPointcut(transactionPointcut()); return advisor; } @Bean public ProxyFactoryBean userService() { ProxyFactoryBean proxyFactoryBean = new ProxyFactoryBean(); proxyFactoryBean.setTarget(userServiceImpl()); proxyFactoryBean.addAdvisor(transactionAdvisor()); //proxyFactoryBean.setInterceptorNames("transactionAdvisor"); return proxyFactoryBean; } @Bean public UserService userServiceImpl() { UserServiceImpl userServiceImpl = new UserServiceImpl(); userServiceImpl.setUserDao(userDao()); userServiceImpl.setMailSender(mailSender()); return userServiceImpl; }
proxyFactoryBean.addAdvisor(transactionAdvisor()); proxyFactoryBean.setInterceptorNames("transactionAdvisor");
ProxyFactoryBean에 Advisor을 설정할 때 위의 두 방법은 하지만 약간의 차이가 있습니다.
addAdvisor()
: Advisor 객체를 직접 주입함
setInterceptorNames()
: 동일한 이름을 가진 Advisor 빈 객체를 찾아서 주입함
- UserServiceTest
//예외 발생 시 작업 취소 여부 테스트 @Test @DirtiesContext public void upgradeAllorNothing() throws Exception { UserServiceImpl testUserService = new TestUserService(users.get(3).getId()); testUserService.setUserDao(userDao); testUserService.setMailSender(mailSender); // TransactionHandler txHandler = new TransactionHandler(); // txHandler.setTarget(testUserService); //타겟 오브젝트 주입, 핵심 기능을 위임할 오브젝트 // txHandler.setTransactionManager(transactionManager); //트랜잭션 기능에 필요한 객체 주입 // txHandler.setPattern("upgradeLevels"); //트랜잭션 기능을 부가할 메소드 이름 // UserService txUserService = // (UserService)Proxy.newProxyInstance(getClass().getClassLoader(), //현재 클래스의 로더를 가져옴 // new Class[] {UserService.class}, //프록시 객체가 구현해야할 인터페이스 // txHandler); //구현된 프록시 객체에 부가기능을 주고 위임할 invocation ProxyFactoryBean txProxyFatoryBean = context.getBean("&userService", ProxyFactoryBean.class); txProxyFatoryBean.setTarget(testUserService); UserService txUserService = (UserService) txProxyFatoryBean.getObject(); userDao.deleteAll(); for (User user : users) { testUserService.add(user); }
➡️ 아래와 같이 변경되었습니다.
- txProxyFactoryBean → ProxyFactoryBean
- txProxyFactoryBean.class → ProxyFactoryBean.class
여러 가지 어드바이스와 포인트컷을 조합한 Advisor을 통해서 기존의 빈으로 등록한 Advice와 Pointcut을 재사용할 수 있습니다.
💡Advice와 Pointcut은 모두 하나의 객체, 즉 싱글톤 패턴으로 관리됩니다. 더 이상 하나의 프록시 객체에 종속적이지 않게 되었습니다.
![](https://blog.kakaocdn.net/dn/b1oGXm/btsrBOGnXkS/2TONnWdB0RW8zBZaTkFtDk/img.png)
📖 토비 스프링 3.1 -p462~475
🚩jhcode33의 toby-spring-study.git으로 이동하기
🏷️이미지 출처 및 참고한 사이트
Uploaded by N2T