자동 프록시 생성
우선 타깃코드에서 부가기능의 코드는 모두 제거했다.
타깃에서 어떤 메서드에 부가기능을 적용할지도, 포인트컷을 사용해서 각 타깃마다 다르게 적용할 수도 있었다.
이제 마지막으로 ProxyFactoryBean을 적용해주기 위해 빈 설정을 계속 복사해야 한다는 문제만 해결하면 될 것 같다.
BeanPostProcessor 인터페이스는 빈 후처리기로, 스프링 빈 오브젝트로 만들어지고 난 후에, 빈 오브젝트를 다시 가공할 수 있게 해 준다.
스프링은 빈 후처리기가 빈으로 등록되어 있으면, 빈 오브젝트가 생성될 때마다 빈 후처리기에 보내서 후처리 작업을 요청한다.
빈 후처리기는 빈 오브젝트의 프로퍼티를 강제로 수정할 수도 있고, 별도의 초기화 작업을 수행할 수도 있다.
심지어는 만들어진 빈 오브젝트 자체를 바꿔치기할 수도 있다.
이를 이용하면 스프링이 생성하는 빈 오브젝트 중 일부를 프록시로 감싸거나, 프록시를 빈으로 대신 등록할 수도 있다.
프록시 적용 대상이면 그때는 내장된 프록시 생성기에 현재 빈에 대한 프록시를 만들게하고, 만들어진 프록시를 어드바이저에 연결해준다.
빈후처리기는 프록시가 생성되면 컨테이너가 전달해준 빈 오브젝트 대신 프록시 오브젝트를 컨테이너에게 돌려준다.
컨테이너는 최종적으로 빈 후처리기가 돌려준 오브젝트를 빈으로 등록하고 사용한다.
이 빈 후처리기를 사용하면 일일이 빈을 저번처럼 등록하지 않아도 타깃 오브젝트에 자동으로 프록시가 등록되도록 할 수 있다.
그리고 그림을 보면 빈 후처리기도 어드바이스를 통해 대상을 확인하는 것을 볼 수 있다.
우리는 기존에 getMethodMatcher() 메서드만 사용했지만, 사실 포인트컷 인터페이스에는 getClassFilter() 메서드도 존재한다.
이 메서드를 사용하면 모든 클래스에 프록시를 적용하는 것이 아닌, 원하는 클래스에만 프록시를 적용할 수도 있게 된다.
Class 필터 기능을 사용해 보자.
기존에 사용했던 NameMatchMethodPointcut은 모든 클래스를 통과시켜 주는 알고리즘을 가지고 있기 때문에 해당 클래스를 상속받아 메서드를 재정의해야 한다.
package seungkyu;
import jakarta.annotation.Nonnull;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.aop.ClassFilter;
import org.springframework.aop.Pointcut;
import org.springframework.aop.framework.ProxyFactoryBean;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.aop.support.NameMatchMethodPointcut;
public class HelloTest {
@Test
public void classNamePointCutAdvisor(){
NameMatchMethodPointcut classMethodPointCut = new NameMatchMethodPointcut(){
@Override
@Nonnull
//클래스 필터를 재정의
public ClassFilter getClassFilter() {
//해당 이름으로 시작하는 클래스만 적용
return clazz -> clazz.getSimpleName().startsWith("Hi");
}
};
//해당 클래스에서도 해당 메서드에만 프록시를 적용
classMethodPointCut.setMappedName("say*");
class HiChild extends DynamicProxyTest.HelloImpl{}
checkAdvice(new HiChild(), classMethodPointCut, true);
class HelloChild extends DynamicProxyTest.HelloImpl{}
checkAdvice(new HelloChild(), classMethodPointCut, false);
}
private void checkAdvice(Object target, Pointcut pointcut, boolean isAdvice){
ProxyFactoryBean proxyFactoryBean = new ProxyFactoryBean();
proxyFactoryBean.setTarget(target);
proxyFactoryBean.addAdvisor(new DefaultPointcutAdvisor(pointcut, new DynamicProxyTest.UpperCaseAdvisor()));
DynamicProxyTest.Hello proxyHello = (DynamicProxyTest.Hello) proxyFactoryBean.getObject();
if(isAdvice){
Assertions.assertEquals("HELLO SEUNGKYU", proxyHello.sayHello("seungkyu"));
Assertions.assertEquals("HI SEUNGKYU", proxyHello.sayHi("seungkyu"));
Assertions.assertEquals("BYE SEUNGKYU", proxyHello.sayBye("seungkyu"));
}
else{
Assertions.assertEquals("Hello seungkyu", proxyHello.sayHello("seungkyu"));
Assertions.assertEquals("Hi seungkyu", proxyHello.sayHi("seungkyu"));
Assertions.assertEquals("Bye seungkyu", proxyHello.sayBye("seungkyu"));
}
}
}
ClassFilter를 사용해 특정 이름으로 시작하는 클래스만 적용하도록 했다.
이렇게 포인트컷을 사용하면 원하는 클래스의 원하는 메서드에만 프록시를 적용할 수 있게 되는 것이다.
DefaultAdvisorAutoProxyCreator
이제 실제로 적용해 보도록 하자.
public class NameMatchClassMethodPointcut extends NameMatchMethodPointcut {
public NameMatchClassMethodPointcut(String mappedClassName) {
this.setClassFilter(clazz -> PatternMatchUtils.simpleMatch(mappedClassName, clazz.getSimpleName()));
}
}
간단하게 사용하던 NameMatchMethodPointCut에 클래스 필터를 넣어보자.
이제 'DefaultAdvisorAutoProxyCreator'를 사용해 보자.
이 DefaultAdvisorAutoProxyCreator는 등록된 빈 중에서 Advisor 인터페이스를 구현한 것을 모두 찾는다.
그리고 생성되는 모든 빈에 대해 어드바이저의 포인트컷을 적용해 보며 프록시 적용 대상을 선정한다.
아래와 같이 DefaultAdvisorAutoProxyCreator를 등록한다.
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"/>
어차피 다른 빈에서 사용하지 않기 때문에 class만 등록해서 사용한다.
그리고는 기존의 포인트컷 설정을 삭제하고 새로운 포인트컷을 빈으로 등록한다.
<bean id="transactionPointcut"
class="seungkyu.NameMatchClassMethodPointcut">
<constructor-arg value="say"/>
</bean>
이제 UserService는 진짜 프록시 설정을 아예 신경 쓰지 않고 빈으로 등록해주기만 하면 된다.
<bean id="userService" class="seungkyu.UserServiceImpl">
<constructor-arg ref="userDao"/>
<constructor-arg ref="mailSender"/>
</bean>
이제 프록시에 관한 테스트이다.
중간에 예외가 발생해야 하기 때문에 예외가 발생하는 Service인 TestUserServiceImpl를 만들어주었다.
public class TestUserServiceImpl extends UserServiceImpl {
public TestUserServiceImpl(UserDao userDao, MailSender mailSender) {
super(userDao, mailSender);
}
@Override
protected void upgradeLevel(User user) {
String name = "user3";
if(user.getName().equals(name)){
throw new IllegalArgumentException("test");
}
super.upgradeLevel(user);
}
}
그리고 해당 테스트용 서비스를 빈으로 등록하자.
<bean id="dataSource"
class = "org.springframework.jdbc.datasource.SimpleDriverDataSource">
<property name="driverClass" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/test"/>
<property name="username" value="root"/>
<property name="password" value="1204"/>
</bean>
<bean id="userDao" class="seungkyu.UserDaoImpl">
<constructor-arg ref="dataSource" />
</bean>
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"/>
<bean id="transactionAdvice" class="seungkyu.TransactionAdvice">
<constructor-arg ref="transactionManager"/>
</bean>
<bean id="transactionAdvisor" class="org.springframework.aop.support.DefaultPointcutAdvisor">
<constructor-arg ref="transactionAdvice"/>
<constructor-arg ref="transactionPointcut"/>
</bean>
<bean id="transactionPointcut"
class="seungkyu.NameMatchClassMethodPointcut">
<constructor-arg value="*TestUserService*"/>
</bean>
<bean id="mailSender" class="seungkyu.MailSenderImpl"/>
<bean id="userService" class="seungkyu.UserServiceImpl">
<constructor-arg ref="userDao"/>
<constructor-arg ref="mailSender"/>
</bean>
<bean id="testUserService" class="seungkyu.TestUserServiceImpl" parent="userService"/>
작성한 test-applicationContext.xml은 위와 같다.
그리고 아래와 같은 테스트를 수행하니 통과하는 것을 볼 수 있었다.
@Test
public void upgradeAllOrNothing(){
userDao.deleteAll();
for(User user: users) userDao.add(user);
try
{
this.testUserService.upgradeLevels();
}
catch(IllegalArgumentException ignored)
{
}
User user1 = userDao.get(users.get(1).getId());
Assertions.assertEquals(Level.BRONZE, user1.getLevel());
}
user3에서 에러가 발생했기에 user1이 롤백된 것을 볼 수 있다.
포인트컷 표현식을 이용한 포인트컷
지금까지는 클래스와 메서드를 선정하기 위해, 이름을 사용해 문자열로 비교하며 선정했었다.
이런 방법으로도 필터링은 가능하지만, 포인트컷 표현식을 사용하면 더욱 섬세한 필터링은 가능하다고 한다.
우선 해당 코드의 출력을 확인해 보자.
System.out.println(Target.class.getMethod("plus", int.class, int.class));
이렇게 메서드에 대한 정보를 리플랙션을 통해 출력하고 있다.
포인트컷 표현식을 통해 적용할 메서드를 등록할 수 있다.
execution([접근 제한자 패턴] 타입패턴(리턴 값의 타입 패턴) [타입패턴.(패키지와 클래스 이름에 대한 패턴)]이름패턴(메서드 이름 패턴) (타입패턴(파라미터 타입 패턴) | "..", ...)
- 접근제한자
public, protected 등이 오며, 포인트컷 표현식에서는 생략이 가능하다.
- 리턴 타입
리턴 값의 타입을 나타내는 패턴이다. 필수항목이며 *를 써서 모든 타입을 지정 가능하다.
- 클래스의 타입
패키지와 타입 이름을 포함한 클래스의 타입 패턴이다. *를 사용가능하며,.. 을 사용하면 여러 개의 패키지를 선택할 수 있다.
- 메서드 이름
매머드의 이름이며, 필수항목이고 * 사용 가능하다.
- 파라미터 타입
파리미터의 타입을 , 로 구분하면서 넣으면 된다. 파라미터와 관계없이 지정하려면.. 을 넣고,... 을 사용해서 뒷부분의 파리미터 조건만 생략할 수도 있다.
- 예외
예외에 대한 타입 패턴이며 생략 가능하다.
그렇기에 AspectJExpressionPointcut을 테스트해 보면
@Test
public void methodSignaturePointcut() throws SecurityException, NoSuchMethodException {
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
pointcut.setExpression("execution(public int seungkyu.Target.minus(int, int) throws java.lang.RuntimeException)");
Assertions.assertTrue(pointcut.getClassFilter().matches(Target.class));
Assertions.assertTrue(pointcut.getMethodMatcher().matches(Target.class.getMethod("minus", int.class, int.class), null));
}
다음과 같이 작성한 표현식에 작성한 Target.class의 minus 메서드가 통과하는 것을 볼 수 있다.
xml에는 포인트컷 표현식을 사용하는 포인트컷을 다음과 같이 등록한다.
<bean id="transactionPointcut"
class="org.springframework.aop.aspectj.AspectJExpressionPointcut">
<property name="expression" value="execution(* *..*ServiceImpl.upgrade*(..))"/>
</bean>
AOP란 무엇인가?
트랜잭션을 처리했던 과정을 살펴보자.
- 그냥 만들기
그냥 만들어서 트랜잭션을 적용했었다.
하지만 트랜잭션이 특정한 트랜잭션 기술에 종속된다는 문제가 있었다.
- 트랜잭션 서비스 추상화
트랜잭션의 인터페이스를 만들어서 어떻게 사용하는지만 서비스에 남기고 기술은 별도로 구현했다.
하지만 여전히 서비스에서 비즈니스 로직과 상관없는 트랜잭션 설정이 노출되고 있다.
- 프록시와 데코레이트 패턴
스프링의 DI를 통해 데코레이터 패턴으로 클라이언트가 데코레이터 패턴이 적용된 빈을 사용하도록 했다.
하지만 이런 패턴을 적용하기 위해서는 기존의 메서드를 하나하나 데코레이터 패턴으로 재정의 해야했다.
- 다이내믹 프록시와 프록시 팩토리 빈
프록시 오브젝트를 런타임시에 만들어주는 JDK 다이나믹 프록시 기술을 적용했다.
프록시 클래스의 작성 부담을 줄었지만, 이런 빈을 하나하나 xml에 등록해주어야 했다.
- 자동 프록시 생성 방법과 포인트컷
스프링 컨테이너의 빈 생성 후처리기를 사용해 생성된 빈 중에서 프록시 설정이 필요하면 프록시 설정을 한 클래스로 교체한 후 빈을 변경해줄 수 있도록 했다.
관심사가 같은 코드는 분리하여 모아서 한 번에 관리하는 게 개발자의 기본 원칙이다.
트랜잭션 설정처럼 공통부분은 AOP를 적용해서 모아보도록 하자.
이렇게 비즈니스 로직은 유지하고, 부가기능만 뽑아서 더 독립적으로 객체지향적인 개발을 할 수 있도록 한다.
AOP 적용기술
스프링은 다양한 기술을 통해 AOP를 지원하고 있다.
프록시가 설정된 클래스를 빈으로 주입하는 것이기 때문에, 스프링 컨테이너와 JVM 외에는 필요한 게 없다.
하지만 대표적인 AOP인 AspectJ는 프록시를 사용하지 않는다고 한다.
AspectJ는 진짜 타깃 오브젝트를 고친다고 한다.
방법은 모르겠지만, 컴파일된 타깃의 클래스 파일 자체를 수정하거나 클래스가 JVM에 로딩되는 시점을 가로채 바이트코드를 조작한다고 한다.
이렇게 직접 변경하기에 스프링 컨테이너가 없어도 AOP를 적용할 수 있으며, 기존에는 적용 불가능한 private 메서드에도 AOP를 적용할 수 있다고 한다.
AOP의 용어
- 타깃
부가기능을 부여할 대상이다.
- 어드바이스
부가기능을 담은 모듈이다.
- 조인 포인트
어드바이스가 실행되는 시점이다.
스프링의 프록시 AOP에서 조인 포인트는 메서드의 실행 단계 뿐이다.
- 포인트컷
어드바이스를 적용할 조인 포인트를 선별하는 알고리즘을 담은 모듈
- 프록시
클라이언트와 타깃 사이에서 부가기능을 제공하는 오브젝트이다.
- 어드바이저
포인트컷과 어드바이스를 세트로 가지고 있는 오브젝트이다.
- 애스팩트
AOP의 기본 모듈이다.
한 개 또는 그 이상의 포인트컷과 어드바이스의 조합으로 만들어진다.
어드바이저도 애스팩트의 한 종류이다.
AOP 네임스페이스
기존의 빈들과는 다르게 스프링의 프록시 방식 AOP를 사용하려면 4가지를 등록해야 한다.
- 자동 프록시 생성기
저번에는 DefaultAdvisorAutoProxyCreator 클래스를 빈으로 등록했다.
다른 빈을 DI하지 않고, 자신도 DI하지 않으며 독립적으로 존재한다.
그냥 작성하기만 하면 프록시를 자동으로 생성하는 기능을 담당한다.
- 어드바이스
부가기능을 구현한 클래스를 빈으로 등록한다.
- 포인트컷
스프링의 AspectJExpressionPointcut을 빈으로 등록하고, expression 프로퍼티에 포인트컷 표현식을 넣어주면 된다.
- 어드바이저
스프링의 DefaultPointcutAdvisor 클래스를 빈으로 등록한다.
자동 프록시 생성기에 의해 찾아지고 사용된다.
스프링에서는 이렇게 많이 사용하는 AOP 기능을 간편하게 등록할 수 있도록 한다.
기존에 빈을 작성하던 xml에서 aop 영역을 새로 만들어서 등록한다.
하나하나 적용하던 설정을 다음과같이 간편하게 적용이 가능하다.
<aop:config>
<aop:pointcut id="transactionPointcut"
expression="execution(* *..*ServiceImpl.upgrade*(..))"/>
<aop:advisor advice-ref="transactionAdvice" pointcut-ref="transactionPointcut"/>
</aop:config>
아래가 기존에 적용하던 방법이다.
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"/>
<bean id="transactionAdvice" class="seungkyu.TransactionAdvice">
<constructor-arg ref="transactionManager"/>
</bean>
<bean id="transactionAdvisor" class="org.springframework.aop.support.DefaultPointcutAdvisor">
<constructor-arg ref="transactionAdvice"/>
<constructor-arg ref="transactionPointcut"/>
</bean>
<bean id="transactionPointcut"
class="org.springframework.aop.aspectj.AspectJExpressionPointcut">
<property name="expression" value="execution(* *..*ServiceImpl.upgrade*(..))"/>
</bean>
코드의 양이 줄어든 것 뿐만 아니라, 기존의 빈과 aop를 분리해서 작성이 가능하다.
분리해서 관리 할 수 있도록, aop 영역에 별도로 작성하도록 하자.
'Spring > 토비의 스프링' 카테고리의 다른 글
[토비의 스프링] 6.7 어노테이션 트랜잭션 속성과 포인트컷 (1) | 2025.06.30 |
---|---|
[토비의 스프링] 6.6 트랜잭션 속성 (2) | 2025.06.26 |
[토비의 스프링] 6.4 스프링의 프록시 팩토리 빈 (0) | 2025.06.19 |
[토비의 스프링] 6.3 다이내믹 프록시와 팩토리 빈 (3) | 2025.06.17 |
[토비의 스프링] 6.2 고립된 단위 테스트 (2) | 2025.06.16 |