SPRING
토비의 스프링 7. AOP (2)
짜비
2022. 9. 18. 18:39
개선 목표
- 타깃 오브젝트마다 거의 비슷한 내용의
ProxyFactoryBean
빈 설정정보를 추가해주는 부분 제거
빈 후처리기를 이용한 자동 프록시 생성기
빈 후처리기
- 스프링 빈 오브젝트로 만들어지고 난 후에, 빈 오브젝트를 다시 가공할 수 있게 해줌
- 스프링은 빈 후처리기가 빈으로 등록되어 있으면 빈 오브젝트가 생성될 때마다 빈후처리기에 보내서 후처리 작업을 요청
빈 후처리기를 이용한 자동 프록시 생성 방법
- 스프링은 빈 오브젝트를 만들 때마다 후처리기에게 빈을 보낸다.
DefaultAdvisorAutoProxyCreator
는 빈으로 등록된 모든 어드바이저내의 포인트컷을 이용해 전달받은 빈이 프록시 적용 대상인지 확인- 프록시 적용 대상이면 내장된 프록시 생성기에게 현재 빈에 대한 프록시를 만들게 하고, 만들어진 프록시에 어드바이저를 연결
- 프록시가 생성되면 원래 컨테이너가 전달해준 빈 오브젝트 대신 프록시 오브젝트를 컨테이너에 돌려 줌.
확장된 포인트컷
public interface Pointcut {
Pointcut TRUE = TruePointcut.INSTANCE;
//프록시를 적용할 클래시인지 확인 (New)
ClassFilter getClassFilter();
//어드바이스를 적용할 메소드인지 확인
MethodMatcher getMethodMatcher();
}
- 사실 포인트컷은 어드바이스를 적용할 메소드인지 확인하는 기능 외에, 프록시를 적용할 클래스인지 확인하는 ‘클래스 필터’ 역할도 담당
클래스 필터 기능 학습테스트
@Test
void classNamePointcutAdvisor() {
NameMatchMethodPointcut classMethodPointcut = new NameMatchMethodPointcut() {
public ClassFilter getClassFilter() {
return new ClassFilter() {
@Override
public boolean matches(Class<?> clazz) {
return clazz.getSimpleName().startsWith("HelloT");
}
};
}
};
classMethodPointcut.setMappedName("sayH*");
checkAdviced(new HelloTarget(), classMethodPointcut, true);
class HelloWorld extends HelloTarget{
};
checkAdviced(new HelloWorld(), classMethodPointcut, false);
class HelloToby extends HelloTarget{};
checkAdviced(new HelloToby(), classMethodPointcut, true);
}
private void checkAdviced(Object target, Pointcut pointcut, boolean adviced) {
ProxyFactoryBean pfBean = new ProxyFactoryBean();
pfBean.setTarget(target);
pfBean.addAdvisor(new DefaultPointcutAdvisor(pointcut, new UppercaseAdvice()));
Hello proxiedHello = (Hello)pfBean.getObject();
if (adviced) {
assertThat(proxiedHello.sayHello("Toby")).isEqualTo("HELLO TOBY");
assertThat(proxiedHello.sayHi("Toby")).isEqualTo("HI TOBY");
assertThat(proxiedHello.sayThankYou("Toby")).isEqualTo("Thank you Toby");
} else {
assertThat(proxiedHello.sayHello("Toby")).isEqualTo("Hello Toby");
assertThat(proxiedHello.sayHi("Toby")).isEqualTo("Hi Toby");
assertThat(proxiedHello.sayThankYou("Toby")).isEqualTo("Thank you Toby");
}
}
Transaction 예제로의 적용
public class NameMatchClassMethodPointcut extends NameMatchMethodPointcut {
public void setMappedClassName(String mappedClassName) {
this.setClassFilter(new SimpleClassFilter(mappedClassName));
}
static class SimpleClassFilter implements ClassFilter {
String mappedName;
public SimpleClassFilter(String mappedName) {
this.mappedName = mappedName;
}
@Override
public boolean matches(Class<?> clazz) {
//와일드카드가 들어간 문자열 비교를 지원하는 스프링의 유틸리티 메소드. *name | name* | *name*
return PatternMatchUtils.simpleMatch(mappedName,
clazz.getSimpleName());
}
}
}
@Bean
public DefaultBeanFactoryPointcutAdvisor defaultBeanFactoryPointcutAdvisor() {
return new DefaultBeanFactoryPointcutAdvisor();
}
@Bean
public UserService userService() {
UserService userService = new UserServiceImpl(userDao(), mailSender());
return userService;
}
@Bean(name = "transactionPointcut")
public NameMatchMethodPointcut transactionPointcut() {
//NameMatchMethodPointcut transactionPointcut = new NameMatchMethodPointcut();
NameMatchClassMethodPointcut transactionPointcut = new NameMatchClassMethodPointcut();
transactionPointcut.setMappedName("upgrade*");
transactionPointcut.setMappedClassName("*ServiceImpl");
return transactionPointcut;
}
@Bean
public TransactionAdvice transactionAdvice() {
TransactionAdvice transactionAdvice = new TransactionAdvice(transactionManager());
return transactionAdvice;
}
@Bean(name = "transactionAdvisor")
public PointcutAdvisor transactionAdvisor() {
PointcutAdvisor transactionAdvisor = new DefaultPointcutAdvisor( transactionPointcut(), transactionAdvice());
return transactionAdvisor;
}
// @Bean(name = "userService")
// public ProxyFactoryBean proxyFactoryBean() {
// ProxyFactoryBean proxyFactoryBean = new ProxyFactoryBean();
// proxyFactoryBean.setTarget(userServiceImpl());
// proxyFactoryBean.addAdvisor(transactionAdvisor());
// return proxyFactoryBean;
// }
- ProxyFactoryBean 제거
UserServiceImpl
빈의 이름을userService
로 원상복구- Q. @Bean의 반환 타입이 UserServiceImpl 이 아니라 UserService로 되어 있어도 classFilter가 이를 감지할 수 있는 것인가?
포인트컷 표현식을 이용한 포인트컷
포인트컷 표현식
- 정규식과 같은 표현식 언어를 사용해서 포인트컷을 작성할 수 있도록 하는 방법.
- 필요성
- 단순히 메소드 이름을 비교하는 것 이외에 정의된 패키지, 파라미터, 리턴 값, 어노테이션, 구현한 인터페이스, 상속한 클래스 등의 정보로 포인트컷 적용대상 결정
- Reflection API를 활용할 수도 있지만 너무 번거로움.
execution()
- 메소드 시그니처를 비교하는 방식
- execution(* minus(int,int))
- 리턴 타입은 상관 없이 minus 라는 메소드 이름, 두 개의 int 파라미터를 가진 모든 메소드를 선정
- execution(* minus(..))
- 리턴 타입, 파라미터 종류, 개수 상관 없이 minus라는 메소드 이름 가진 모든 메소드를 선정
- execution(* *(..))
- 리턴 타입, 파라미터, 메소드 이름에 상관없이 모든 메소드 조건을 다 허용
bean()
- 빈의 이름으로 비교하는 방식
- bean(*Service)
- bean 의 이름이 Service로 끝나는 모든 빈을 선택
@annotation()
- 어노테이션으로 포인트컷 적용 메소드를 선정하는 방식
- @annotation(org.springframework.transaction.annotation.Transactional)
@Transactional
이라는 어노테이션이 붙은 메소드만 선택
@Bean(name = "transactionPointcut")
public AspectJExpressionPointcut transactionPointcut() {
// NameMatchClassMethodPointcut transactionPointcut = new NameMatchClassMethodPointcut();
// transactionPointcut.setMappedName("upgrade*");
// transactionPointcut.setMappedClassName("*ServiceImpl");
AspectJExpressionPointcut transactionPointcut = new AspectJExpressionPointcut();
transactionPointcut.setExpression(
"execution(* *..*ServiceImpl.upgrade*(..))"
);
return transactionPointcut;
}
- 클래스 이름은 ServiceImpl로 끝나고, 메소드 이름은 upgrade로 시작하는 모든 클래스에 적용.
- 기존처럼 ClassName 필터링을 위한 클래스를 별도로 적용할 필요가 없어짐
- Q. AspectJ Deprecate 되지 않았나..?
AOP란 무엇인가?
부가기능의 모듈화
- 부가 기능
- 다른 모듈의 코드에 부가적으로 부여되는 기능
- 한데 모을 수 없고 어플리케이션 전반에 여기저기 흩어져 있음
- 부가 기능을 어떻게 독립적인 모듈로 만들 수 있을까
- == 부가 기능 코드를 중복되지 않게 하고 변경이 필요할 시 한 곳만 수정하면 되도록 만든다.
- 지금까지 해온 모든 작업은 핵심기능에 부여되는 부가기능을 효과적으로 모듈화하는 방법을 찾는 과정이었음
애스펙트 (Aspect)
- 부가기능 모듈
- 그 자체로 애플리케이션의 핵심 기능을 담고 있지는 않지만 애플리케이션을 구성하는 중요한 한 가지 요소이고 핵심기능에 부가되어 의미를 갖는 특별한 모듈
- 애스펙트는 부가될 기능을 정의한 코드인 어드바이스와 어드바이스를 어디에 적용할지를 결정하는 포인트컷을 함께 갖고 있음
AOP (Aspect Oriented Programming)
- 애플리케이션의 핵심적인 기능에서 부가적인 기능을 분리해서 애스펙트라는 독특한 모듈로 만들어서 설계하고 개발하는 방법
- AOP는 OOP를 돕는 보조적인 기술
- 애스펙트를 분리함으로써 핵심기능을 설계하고 구현할 때 객체지향적인 가치를 지킬 수 있도록 도와주는 것
AOP 적용 기술
- 프록시를 이용한 AOP
- 바이트코드 생성과 조작을 통한 AOP
- e.g. AspectJ 프레임워크
- 프록시처럼 간접적인 방법이 아니라 타깃 오브젝트를 뜯어고쳐서 부가 기능을 직접 넣어주는 방법 사용
- DI 컨테이너 없이도 사용 가능
- 부가기능을 부여할 수 있는 대상이 클라이언트가 호출하는 메소드로만 제한되지 않고 오브젝트 생성, 필드 값 조회와 조작, 스태틱 초기화 등 다양한 작업에 부가기능 부여 가능
AOP 네임스페이스
- 기계적으로 적용하는 빈을 간편하게 등록하기 위함.
- 어드바이저
- 포인트컷
- 자동 프록시 생성기
applicationContext.xml
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-3.0.xsd">
<aop:config>
<aop:pointcut id="transactionPointcut"
expression="execution(* *..*ServiceImpl.upgrade*(..))" />
<aop:advisor advice-ref="transactionAdvice" pointcut-ref="transactionPointcut" />
</aop:config>
태그를 사용했을 때보다 이해하기 쉬우며 코드의 양이 대폭 줄어 들었다.
트랜잭션 속성
트랜잭션 속성 4가지
트랜잭션 전파
- 트랜잭션 경계에서 이미 진행 중인 트랜잭션이 있을 때 혹은 없을 때 어떻게 동작할 것인가를 결정하는 방식.
- PROPAGATION_REQUIRED
- 진행 중인 트랜잭션이 없으면 새로 시작하고 이미 시작된 트랜잭션이 있으면 이에 참여
- PROPAGATION_REQUIRES_NEW
- 앞에서 시작된 트랜잭션이 있든 없든 상관없이 새로운 트랜잭션을 만들어서 독자적으로 동작
- PROPAGATION_NOT_SUPPORTED
- 트랜잭션 없이 동작. (트랜잭션 무시)
- 특정한 메소드만 트랜잭션 적용에서 제외하고자 할 때 유용
격리수준
- 동시에 여러 트랜잭션이 처리될 때 특정 트랜잭션이 다른 트랜잭션에서 변경하거나 조회하는 데이터를 볼 수 있도록 허용할지 말지를 결정하는 것.
- Read Uncommitted(0), Read Committed(1), Repeatable Read(2), Serializable(3)
제한시간
- 트랜잭션을 수행하는 제한시간
읽기전용
- 트랜잭션 내에서 데이터를 조작하는 시도를 금지시킴
메소드별로 다른 트랜잭션 정의를 적용하려면 어떻게 해야할까?
TransactionInterceptor
- 동작방식은 기존에 만들었던 TransactionAdvice와 동일
- 트랜잭션 정의를 메소드 이름 패턴을 이용해서 다르게 지정할 수 있는 방법을 추가적으로 제공
메소드 이름 패턴을 이용한 트랜잭션 속성 지정
<bean id="transactionAdvice" class="org.springframework.transaction.interceptor.TransactionInterceptor">
<property name="transactionManager" ref="transactionManager"/>
<property name="transactionAttributes">
<props>
<prop key="get*">PROPAGATION_REQUIRED,readOnly</prop>
<prop key="*">PROPAGATION_REQUIRED</prop>
</props>
</property>
</bean>
- PROPAGATION_NAME, ISOLATION_NAME, readOnly, timeout_NNNN, -Exception1, +Exception2
- 위 설정을 tx 네임스페이스를 이용해서 바꾸면 다음과 같다.
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd">
....
<!--
tx태그를 사용할 경우
- transactionManager는 트랜잭션 매니저 빈 아이디가 transactionManager가 있으면 생략 가능
-->
<tx:advice id="transactionAdvice">
<tx:attributes>
<tx:method name="get*" read-only="true"/>
<tx:method name="upgrade*" read-only="true"/>
<tx:method name="*" />
</tx:attributes>
</tx:advice>
<aop:config>
<aop:advisor advice-ref="transactionAdvice" pointcut="bean(*Service)"/>
</aop:config>
포인트컷, 트랜잭션 속성 적용 팁
- 트랜잭션 포인트컷 표현식은 타입 패턴이나 빈 이름을 이용한다.
- 일반적으로 트랜잭션의 경우 메소드 단위까지 세밀하게 포인트컷을 정의할 필요 X
- 클래스들이 모여있는 패키지를 통째로 선택하거나 클래스 이름에서 일정한 패턴을 찾아서 표현식으로 만드는 것이 바람직
- 공통된 메소드 이름 규칙을 통해 최소한의 트랜잭션 어드바이스와 속성을 정의한다.
- 트랜잭션 속성은 최소(모든 메소드에 기본 트랜잭션 속성)로 시작해서 점차 확대하는 방식을 권장.
- e.g. 디폴트 속성을 일괄 부여 -> 조회용 메소드에 ‘Read-only’ 속성 추가 -> ….
- 프록시 방식 AOP 는 같은 타깃 오브젝트 내의 메소드를 호출할 때는 적용되지 않는다.
- 프록시를 거치지 않고 직접 타깃의 메소드가 호출되기 때문.
- 해결 방법
- AspectJ 적용 (바이트코드 조작하는 방식의 AOP 기술)
트랜잭션 속성 -> UserService 적용
부가기능을 어느 layer에 적용할 것인가.
- 비즈니스 로직을 담고 있는 서비스 계층 오브젝트의 메소드가 가장 이상적.
- Dao의 메소드를 UserService에 위임.
- 다른 레이어나 모듈에서 Dao로 직접 접근 제한
public interface UserService {
void add(User user);
// 새로 추가된 4개의 메소드
User get(String id);
List<User> getAll();
void deleteAll();
void update(User user);
void upgradeLevels();
}
//UserServiceImpl
//Dao 로 위임
@Override
public User get(String id) {
return userDao.get(id);
}
@Override
public List<User> getAll() {
return userDao.getAll();
}
@Override
public void deleteAll() {
userDao.deleteAll();
}
@Override
public void update(User user) {
userDao.update(user);
}
<aop:config>
<aop:advisor advice-ref="transactionAdvice" pointcut="bean(*Service)" />
</aop:config>
<tx:advice id="transactionAdvice">
<tx:attributes>
<tx:method name="get*" read-only="true"/>
<tx:method name="*" />
</tx:attributes>
</tx:advice>
어노테이션 트랜잭션 속성 & 포인트컷
- 문제의식
- 클래스나 메소드에 따라 제각각 속성이 다른, 세밀하게 튜닝된 트랜잭션 속성을 적용해야 하는 경우도 있다.
- 위 경우는 메소드 이름 패턴을 이용해서 일괄적으로 트랜잭션 속성을 부여하는 방식은 적합하지 않음
트랜잭션 어노테이션 @Transactional
스프링은 @Transactional 이 부여된 모든 오브젝트를 자동으로 타깃 오브젝트로 인식.
- TransactionalAttributeSourcePointcut
- @Transactional 이 붙은 빈 오브젝트를 모두 찾아서 포인트컷의 선정 결과로 돌려준다.
- 그림 6-24 참고
문제의식
- 메소드마다 @Transactional 을 붙이면 코드가 지저분해지고 동일한 속성 정보를 가진 어노테이션이 반복된다.
대체 정책(fallback)
- 타깃 메소드에 @Transaction 이 있는지 확인
- 없다면 타깃 클래스에 있는지 확인
- 없다면 타깃 클래스 상위 인터페이스 메소드에 있는지 확인
- 없다면 인터페이스에 있는지 확인
UserService 에 적용
@Transactional
public interface UserService {
void add(User user);
@Transactional(readOnly = true)
User get(String id);
@Transactional(readOnly = true)
List<User> getAll();
void deleteAll();
void update(User user);
void upgradeLevels();
}
- 인터페이스 레벨에 디폴트 속성 부여
- 메소드별 디테일한 속성 설정은 메소드 레벨로 설정
트랜잭션 테스트
트랜잭션 동기화 기술
- 트랜잭션 정보를 저장소에 보관해뒀다가 DAO 에서 공유
- 이 기술 덕분에 트랜잭션 전파가 가능
- 트랜잭션 매니저를 이용해서 트랜잭션에 참여하거나 트랜잭션을 제어할 수 있음.
- e.g. 테스트 코드에서 세 개의 트랜잭션을 하나의 트랜잭션으로 묶기
@ExtendWith(SpringExtension.class)
class UserServiceTest {
@Autowired
PlatformTransactionManager transactionManager;
@Test
public void transactionSync() {
DefaultTransactionDefinition txDefinition = new DefaultTransactionDefinition();
TransactionStatus txStatus = transactionManager.getTransaction(txDefinition);
userService.deleteAll();
userService.add(users.get(0));
userService.add(users.get(1));
transactionManager.commit(txStatus);
}
}
- 위와 같이 임의의 트랜잭션 묶음을 만들어 놓고 트랜잭션 동기화가 잘 이루어지는지, 롤백은 정상적으로 되는지 등을 테스트해볼 수 있다.
롤백 테스트
- 테스트 내의모든 DB 작업을 하나의 트랜잭션 안에서 동작하게 하고 테스트가 끝나면 무조건 롤백하는 테스트
테스트를 위한 트랜잭션 어노테이션
- @Transactional 어노테이션을 붙여서 위와 동일하게 여러 트랜잭션을 하나의 트랜잭션으로 묶을 수 있음
- 주의사항 : 테스트용 트랜잭션은 테스트가 끝나면 자동으로 롤백된다.
- 만약 DB에 그대로 커밋하고 싶다면?
@Rollback(false)
어노테이션을 붙인다.@TransactionConfiguration(defaultRollback=false)
을 테스트 클래스에 붙여서 여러 테스트 메소드에 일괄 적용 가능. —> deprecated?
@Test
@Transactional
public void transactionSync() {
userService.deleteAll();
userService.add(users.get(0));
userService.add(users.get(1));
}
Q. AOP - 실무와의 연관성
- Transaction을 예로 들면 트랜잭션이 필요한 메소드에 @Transactional 을 붙이는 방식으로 쓰이는 것 같은데.. AOP (pointcut, advice) 등을 직접 구현해서 사용하는 경우가 있는지?