이번 장은 그 유명한 Spring AOP를 다룬다. 처음 6장을 읽었을 땐 텍스트를 읽으면서도 이해가 안갔는데, 이번에 다시 한번 읽으며 동작 원리에 대해 어느정도 이해하게 되었다.
💬
관련해 토프링 읽기모임에서 나왔던 얘기들을 공유한다.
생성자나 메소드 파라미터 등에서 스프링 AOP
를 적용하려면 AspectJ를 사용해야 한다고 한다. 언젠가 코드에 적용해보고 싶다!
public interface UserService {
User findById(final long id);
void insert(final User user);
void changePassword(final long id, final String newPassword, final String createBy);
}
public class AppUserService implements UserService {
private final UserDao userDao;
public AppUserService(final UserDao userDao) {
this.userDao = userDao;
}
public User findById(final long id) {
return userDao.findById(id);
}
public void insert(final User user) {
userDao.insert(user);
}
public void changePassword(final long id, final String newPassword, final String createBy) {
final var user = findById(id);
user.changePassword(newPassword);
userDao.update(user);
}
}
자바 reflect(method.invoke())
를 활용.
final var userService = (UserService) Proxy.newProxyInstance(
getClass().getClassLoader(), // 클래스 로딩을 위한 클래스 로더
new Class[]{UserService.class}, // 프록시로 구현할 인터페이스
new TxHandler(appUserService, transactionManager, "changePassword")); // 적용할 에스팩트, 포인트컷을 담은 핸들러
public class TxHandler implements InvocationHandler {
Object target;
PlatformTransactionManager transactionManager;
String pattern;
public TxHandler(Object target, PlatformTransactionManager transactionManager, String pattern) {
this.target = target;
this.transactionManager = transactionManager;
this.pattern = pattern;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
if (method.getName().startsWith(pattern)) {
return invokeInTransaction(method, args);
} else {
return method.invoke(target, args);
}
}
private Object invokeInTransaction(Method method, Object[] args)
throws Throwable {
TransactionStatus status = this.transactionManager
.getTransaction(new DefaultTransactionDefinition());
try {
Object ret = method.invoke(target, args);
this.transactionManager.commit(status);
return ret;
} catch (InvocationTargetException e) {
this.transactionManager.rollback(status);
throw e.getTargetException();
}
}
}
이를 통해 특정 이름으로 시작하는 메소드에 트랜잭션을 적용할 수 있게 된다!
프록시를 통해 타깃에 부가기능을 제공하는 것은 메소드 단위로 일어나는 일이다. 하나의 클래스 안에 존재하는 여러 개의 메소드에 부가기능을 한 번에 제공하는 건 어렵지 않게 가능했다. 하지만 한 번에 여러 개의 클래스에 공통적인 부가기능을 제공하는 일은 지금까지 살펴본 방법으로는 불가능하다.
- 알라딘 eBook <토비의 스프링 3.1 Vol. 1 스프링의 이해와 원리> (이일민 지음) 중에서
스프링 내장 빈인 ProxyFactoryBean
을 통해 동적으로 프록시 생성 및 빈 등록이 가능하다!
// ProxyFactoryBean 구조 간략화
public class ProxyFactoryBean implements FactoryBean<Object> {
private Object singletonInstance;
private Advisor advisor;
public ProxyFactoryBean(Object singletonInstance) {
this.object = singletonInstance
}
public void addAdvisor(Advisor advisor) {}
//...
}
ProxyFactoryBean pfBean = new ProxyFactoryBean(new UserServiceImpl());
// 메소드 이름을 비교해서 대상을 선정하는 알고리즘을 제공하는 포인트컷 생성
NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
pointcut.setMappedName("changePassword*"); // 이름 비교조건 설정.
pfBean.addAdvisor(new DefaultPointcutAdvisor(pointcut, new UppercaseAdvice()));
// 포인트컷과 어드바이스를 advisor로 묶어서 한 번에 추가
UserService proxiedTxUserService = (UserService) pfBean.getObject();
스프링이 생성하는 빈 오브젝트의 일부를 프록시로 포장하고, 프록시를 빈으로 대신 등록할 수도 있다. 바로 이것이 자동 프록시 생성 빈 후처리기다.
- 알라딘 eBook <토비의 스프링 3.1 Vol. 1 스프링의 이해와 원리> (이일민 지음) 중에서
아직 트랜잭션 적용 대상이 되는 빈마다, 일일이 프록시 팩토리 빈을 설정해줘야 한다는 부담이 남아 있다.
이를 해결하기 위해, DefaultAdvisorAutoProxyCreator
를 사용한다.
자동으로 Advisor
를 호출하여 pointcut
에 부합한다면 advice
를 호출한다.
번거로운 프록시 팩토리 빈 설정을 안해도 된다!
포인트컷에는 클래스 정보 뿐 아니라, 어드바이스를 적용할 메소드에 대한 정보도 필요하다!
public interface Pointcut {
ClassFilter getClassFilter(); //프록시를 적용할 클래스인지 확인
MethodMatcher getMethodMatcher(); //어드바이스를 적용할 메소드인지 확인
}
포인트컷 표현식은 자바의 RegEx 클래스가 지원하는 정규식처럼 간단한 문자열로 복잡한 선정조건을 쉽게 만들어낼 수 있는 강력한 표현식을 지원한다.
사실 스프링이 사용하는 포인트컷 표현식은 AspectJ라는 유명한 프레임워크에서 제공하는 것을 가져와 일부 문법을 확장해서 사용하는 것이다. 그래서 이를 AspectJ 포인트컷 표현식이라고도 한다.
@Test
public void methodSignaturePointcut() throws NoSuchMethodException {
AspectJExpressionPointcut pointcut =new AspectJExpressionPointcut();
pointcut.setExpression("execution(public int springbook.learningtest.pointcut.Target.minus(int,int) throws java.lang.RuntimeException)");
assertThat(pointcut.getClassFilter().matches(Target.class), is(true));
assertThat(pointcut.getMethodMatcher().matches(Target.class.getMethod("minus", int.class, int.class), null), is(true));
assertThat(pointcut.getMethodMatcher().matches(Target.class.getMethod("plus", int.class, int.class), null), is(false));
}
bean()
: 특정 이름의 빈을 선정하는 포인트컷@annotation
: 특정 애노테이션이 적용된 메소드를 선정하는 포인트컷런타임 시점까지 문법검증/기능 확인이 되지 않는다. 검증된 표현식을 쓰자!
포인트컷 표현식 속 클래스 이름은 타입 패턴이다.
AspectJ는 프록시처럼 간접적인 방법이 아니라, 타깃 오브젝트를 뜯어고쳐서 부가기능을 직접 넣어주는 직접적인 방법을 사용한다
컴파일된 타깃의 클래스 파일 자체를 수정하거나 클래스가 JVM에 로딩되는 시점을 가로채서 바이트코드를 조작하는 복잡한 방법을 사용한다. 트랜잭션 코드가 UserService 클래스에 비즈니스 로직과 함께 있었을 때처럼 만들어버리는 것이다.
물론 소스코드를 수정하지는 않으므로 개발자는 계속해서 비즈니스 로직에 충실한 코드를 만들 수 있다.
자동 프록시 생성기
DefaultAdvisorAutoProxyCreator
클래스를 빈으로 등록한다. 다른 빈을 DI 하지도 않고 자신도 DI 되지 않으며 독립적으로 존재한다. 따라서 id도 굳이 필요하지 않다. 애플리케이션 컨텍스트가 빈 오브젝트를 생성하는 과정에 빈 후처리기로 참여한다. 빈으로 등록된 어드바이저를 이용해서 프록시를 자동으로 생성하는 기능을 담당한다.어드바이스
TransactionAdvice
는 AOP 관련 빈 중에서 유일하게 직접 구현한 클래스를 사용한다.포인트컷
AspectJExpressionPointcut
을 빈으로 등록하고 expression
프로퍼티에 포인트컷 표현식을 넣어주면 된다. 코드를 작성할 필요는 없다.어드바이저
DefaultPointcutAdvisor
클래스를 빈으로 등록해서 사용한다. 어드바이스와 포인트컷을 프로퍼티로 참조하는 것 외에는 기능은 없다. 자동 프록시 생성기에 의해 자동 검색되어 사용된다.트랜잭션 경계에서, 이미 진행중인 트랜잭션이 있을 때, 없을 때 어떻게 동작할 것인가?
Required
기존 트랜잭션(부모 트랜잭션)이 있을 시 합류한다.
Required New
부모 트랜잭션과 관계없는 새 트랜잭션을 생성한다.두 트랜잭션은 별개로 처리된다.
Not Supported
트랜잭션 없이 동작
트랜잭션을 무시하는 속성을 두는 데는 이유가 있다.
트랜잭션 경계설정은 보통 AOP를 이용해 한 번에 많은 메소드에 동시에 적용하는 방법을 사용한다. 그런데 그중에서 특별한 메소드만 트랜잭션 적용에서 제외하려면 어떻게 해야 할까?
물론 포인트컷을 잘 만들어서 특정 메소드가 AOP 적용 대상이 되지 않게 하는 방법도 있겠지만 포인트컷이 상당히 복잡해질 수 있다.
그래서 차라리 모든 메소드에 트랜잭션 AOP가 적용되게 하고, 특정 메소드의 트랜잭션 전파 속성만 PROPAGATION_NOT_SUPPORTED로 설정해서 트랜잭션 없이 동작하게 만드는 편이 낫다.
- 알라딘 eBook <토비의 스프링 3.1 Vol. 1 스프링의 이해와 원리> (이일민 지음) 중에서
DefaultTransactionDefinition
에 설정된 격리수준은 ISOLATION_DEFAULT
다.DataSource
에 설정되어 있는 디폴트 격리수준을 그대로 따른다는 뜻이다.스프링의 트랜잭션은 기본적인 예외처리 원칙에 따라 비즈니스적인 의미가 있는 예외상황에만 체크 예외를 사용하고, 그 외의 모든 복구 불가능한 순수한 예외의 경우는 런타임 예외로 포장돼서 전달하는 방식을 따른다고 가정한다.
- 해당 속성을 통해, 기본 예외처리 원칙과 다른 예외처리가 가능하다.
- 특정 체크예외를 롤백시키거나, 특정 런타임 예외시 트랜잭션을 커밋할 수 있다.
해당 타겟 클래스 안에서 호출한 메소드에는 AOP가 적용되지 않는다.
테스트에서 트랜잭션을 시작하거나 조작할 수 있는 기능은 매우 유용하다. 테스트 코드에서 미리 트랜잭션을 시작해놓으면 직접 호출하는 DAO 메소드도 하나의 트랜잭션으로 묶을 수 있다. 트랜잭션 결과나 상태를 조작하면서 테스트하는 것도 가능하다. 예를 들어 하이버네이트 같은 ORM에서 세션에서 분리된
detached
엔티티의 동작을 확인할 때도 유용하다.
- 알라딘 eBook <토비의 스프링 3.1 Vol. 1 스프링의 이해와 원리> (이일민 지음) 중에서
@Test
public void transactionSync(){
userDao.deleteAll();
assertThat(userDao.getCount(), is(0)); // 롤백테스트를 위해 초기 상태 지정
DefaultTransactionDefinition txDefinition = new DefaultTransactionDefinition();
TransactionStatus txStatus = transactionManager.getTransaction(txDefinition);
// 트랜잭션 매니저에게 트랜잭션을 요청한다. 기존에 진행되는 트랜잭션이 없으니 새 트랜잭션을 시작한다.
// 트랜잭션을 반환함과 동시에 동기화한다.
// ... service 메서드 호출
userService.add(users.get(0));
userService.add(users.get(2));
assertThat(userDao.getCount(), is(2));
transactionManager.rollback(txStatus); // 강제 롤백
assertThat(userDao.getCount(), is(0)); // 롤백 이전의 상태로 돌아가는지 확인.
}
테스트가 끝날 시 트랜잭션을 롤백하는 테스트.
DB 작업이 포함된 테스트가 수행돼도 DB에 영향을 주지 않기 때문에 장점이 많다.
만약 트랜잭션에서 특정 메소드를 제외하고 싶다면?
@NotTransactional
대신 @Transactional
의 트랜잭션 전파 속성을 사용!CGLib
를 각각 언제 사용하는지 궁금하다. 다이나믹 프록시의 단점이 인터페이스 구현 이외에 없는지?AOP
는 메소드 단위로 이뤄지는데, 생성자나 메소드 파라미터 등에서 스프링 AOP
를 적용할 수 있는 방법이 없는지 궁금하다. 아니면 해당 케이스에서 스프링 AOP
대신 고려되는 방식이 궁금하다.new
로 생성할 시 들어가는 파라미터에 대해 암호화해주는 로직을 AOP
로 할 수 있는지 궁금했다.토비의 스프링 6장