AOP
- Aspect-Oriented Programing, 객체 지향 프로그래밍을 보완합니다.
- OOP에서 모듈화의 핵심 단위는 클래스, AOP에서 모듈화의 단위는
“측면”
- 여러 유형과 객체를 가로지르는 문제의 모듈화를 가능하게 해줍니다.
AOP 용어
- 애스펙트(Aspect)
- AOP의 기본 모듈, 핵심 모듈
- 한 개 이상의 포인트컷과 어드바이스를 갖고 있습니다.
- Advisor가 Aspect의 역할을 합니다.
- 조인 포인트(Join point)
- Advice를 적용할 위치를 지정합니다.
- 어드바이스(Advice)
- 부가기능을 담고 있는 모듈
- 서비스를 Transaction 서비스, 비즈니스 서비스로 나눈다고 하면 이 중 Transaction 서비스를 부가기능이라고 생각하면 됩니다.
- 우리가 원한건 비즈니스 서비스고, 부가적인 처리가 필요하다는 관점
- 주로 인프라 관련 처리들이 부가기능
- 스프링은 어드바이스를 인터셉터로 모델링하고 조인 포인트 주변에 인터셉터 체인 유지
- 포인트 컷(Pointcut)
- 어드바이스를 적용할 조인 포인트를 선별하는 모듈
- 타겟(Target)
- 부가기능(Advice)을 적용할 대상
- 프록시(Proxy)
- 클라이언트와 타겟 사이에서 부가기능을 제공하는 객체
- 스프링에서 프록시는 JDK 동적 프록시 또는 CGLIB 프록시로 구현
- 스프링에서의 프록시를 제공하는 두가지 방법 : 리플렉션(JDK), CGLIB 라이브러리 사용
- 스프링은 JDK가 기본, 스프링 부트에서 CGLIB 라이브러리 사용
- JDK 프록시 → 메서드 단위, 인터페이스여야만 사용 가능
동적 프록시(Dynamic Proxy)
공식 문서의 설명을 따다 가져오면 다음과 같습니다.
동적 프록시 클래스 는 클래스 인스턴스 의 인터페이스 중 하나를 통한 메서드 호출이 균일한 인터페이스를 통해 인코딩되고 다른 객체에 전달되도록 런타임에 지정된 인터페이스 목록을 구현하는 클래스입니다.
말이 너무 어렵습니다.. 구현된 코드를 보면서 하나씩 알아보면 좋을 것 같습니다ㅎㅎ 구현된 코드의 요구사항은 다음과 같습니다.
Transactional 어노테이션이 붙어있는 메서드가 호출된다면, 해당 메서드가 온전히 실행되면 commit, 예외가 발생하면 rollback 한다.
public class TransactionHandler implements InvocationHandler {
private final PlatformTransactionManager transactionManager;
private final AppUserService appUserService;
public TransactionHandler(PlatformTransactionManager transactionManager, AppUserService appUserService) {
this.transactionManager = transactionManager;
this.appUserService = appUserService;
}
@Override
public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
Method realMethod = Arrays.stream(appUserService.getClass().getDeclaredMethods())
.filter(m -> compareTwoMethodSame(m, method))
.findAny()
.orElseThrow();
if (realMethod.isAnnotationPresent(Transactional.class)) {
final var transactionStatus = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
Object methodReturnValue = method.invoke(appUserService, args);
transactionManager.commit(transactionStatus);
return methodReturnValue;
} catch (Exception e1) {
transactionManager.rollback(transactionStatus);
throw new DataAccessException();
}
} else {
try {
return method.invoke(appUserService, args);
} catch (Exception e1) {
throw new DataAccessException();
}
}
}
private boolean compareTwoMethodSame(Method a, Method b) {
if (!a.getName().equals(b.getName())) {
return false;
}
Class<?>[] aParams = a.getParameterTypes();
Class<?>[] bParams = b.getParameterTypes();
if (aParams.length != bParams.length) {
return false;
}
for (int i = 0; i < aParams.length; i++) {
if (!aParams[i].getTypeName().equals(bParams[i].getTypeName())){
return false;
}
}
return true;
}
}
위 코드에서 중점이 되는 부분은 invoke 메서드입니다. 메소드가 호출되면, invoke()가 호출되며 전달받은 Method 객체를 사용하여 메서드를 호출하기 전, 후로 실행 로직을 처리할 수 있습니다. 실제 메소드가 호출되는 부분문 method.invoke() 입니다.
그렇다면 사용 측면에선 어떻게 사용하면 될까요? java.lang.reflect.Proxy 클래스의 newProxyInstance 메소드를 사용하여 프록시 객체를 만들어 줄 수 있습니다.
final var appUserService = new AppUserService(userDao, userHistoryDao);
final InvocationHandler invocationHandler = new TransactionHandler(platformTransactionManager, appUserService);
final UserService userService = (UserService) Proxy.newProxyInstance(
UserService.class.getClassLoader(),
new Class[]{UserService.class},
invocationHandler
);
주의할 점은, UserService는 인터페이스 타입입니다. Proxy의 newProxyInstance 메서드의 두번째 인자로 반드시 인터페이스 타입을 명시해 주어야 합니다. 자세한 사용 설명은 다음 사이트를 참고하시면 좋을 것 같습니다.
https://docs.oracle.com/javase/8/docs/technotes/guides/reflection/proxy.html
ProxyFactoryBean
스프링의 빈을 기반으로 AOP 프록시를 만들어 주는 객체입니다. 3개의 역할이 필요합니다.
- Advice
- 부가기능을 담고 있는 클래스입니다.
- Pointcut
- Advice가 적용될 조인 포인트를 선별합니다.
- 조인 포인트는 Advice가 적용 될 위치라고 생각하시면 됩니다.
- Advisor
- Pointcut과 Advice를 가지고 있는 객체입니다.
- AOP의 Aspect와 같습니다.
Advice
public class TransactionAdvice implements MethodInterceptor {
private final PlatformTransactionManager transactionManager;
public TransactionAdvice(PlatformTransactionManager transactionManager) {
this.transactionManager = transactionManager;
}
@Override
public Object invoke(final MethodInvocation invocation) throws Throwable {
TransactionStatus transaction = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
Object methodReturnValue = invocation.proceed();
transactionManager.commit(transaction);
return methodReturnValue;
} catch (Exception e) {
transactionManager.rollback(transaction);
throw new DataAccessException();
}
}
}
Pointcut
public class TransactionPointcut extends StaticMethodMatcherPointcut {
@Override
public boolean matches(final Method method, final Class<?> targetClass) {
return method.isAnnotationPresent(Transactional.class);
}
}
Advisor
public class TransactionAdvisor implements PointcutAdvisor {
private final TransactionAdvice advice;
private final TransactionPointcut pointcut;
public TransactionAdvisor(TransactionAdvice advice, TransactionPointcut pointcut) {
this.pointcut = pointcut;
this.advice = advice;
}
@Override
public Pointcut getPointcut() {
return pointcut;
}
@Override
public Advice getAdvice() {
return advice;
}
@Override
public boolean isPerInstance() {
return true;
}
}
각각에 맞추어서 구현이 완료되었다면, 이제 ProxyBeanFactory 객체를 생성하여 설정해 주고 인스턴스를 생성하면 됩니다.
- 우선 프록시를 적용할 인스턴스와 Advisor을 미리 만들어 둡니다.
- ProxyBeanFactory에 인스턴스와 Advisor을 넣어주고, getObject() 메서드로 프록시 객체를 얻어올 수 있습니다.
final ProxyFactoryBean proxyFactoryBean = new ProxyFactoryBean();
final UserService temporaryUserService = new UserService(userDao, userHistoryDao);
TransactionAdvice advice = new TransactionAdvice(platformTransactionManager);
final TransactionAdvisor advisor = new TransactionAdvisor(
advice,
new TransactionPointcut()
);
proxyFactoryBean.setTarget(temporaryUserService);
proxyFactoryBean.addAdvisor(advisor);
final UserService userService = (UserService) proxyFactoryBean.getObject();
DefaultAdvisorAutoProxyCreator
위 코드를 보면, 프록시 객체를 만들기 위해 많은 노력이 드는 것을 볼 수 있습니다. ProxyBeanFactory도 생성해야 하고.. Advice도 매번 만들어주어야 하고.. 인스턴스도 만들어주어야 하고..
이러한 부분들을 DefaultAdvisorAutoProxyCreator을 빈 등록만 해두면 쉽게 사용할 수 있습니다.
@Configuration
public class AopConfig {
@Bean
public TransactionAdvice transactionAdvice(PlatformTransactionManager platformTransactionManager) {
return new TransactionAdvice(platformTransactionManager);
}
@Bean
public TransactionPointcut transactionPointcut() {
return new TransactionPointcut();
}
@Bean
public TransactionAdvisor transactionAdvisor(TransactionAdvice advice, TransactionPointcut pointcut) {
return new TransactionAdvisor(advice, pointcut);
}
@Bean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
return new DefaultAdvisorAutoProxyCreator();
}
}
사실 이 부분은 이해가 잘 되지 않았습니다. 위의 ProxyFactoryBean은 명시적으로 프록시 객체를 만들어 주었거든요. 이 부분에서는 프록시 빈을 만드는 느낌은 아니었습니다.
이 부분은 찾아보니 빈 후처리기에서 동작하며 Advisors를 기반으로 AOP 프록시 객체를 만들어 준다고 합니다. 자세한 내용은 다음과 같습니다.
'프레임워크 > Spring' 카테고리의 다른 글
이미지 파일 업로드 도입기 (0) | 2022.10.29 |
---|---|
스프링의 생성자 주입 얕게 알아보기 (3) | 2022.10.25 |
Test에서의 의존성 주입 문제 (0) | 2022.10.24 |
RestTemplate를 사용한 슬랙봇으로 메세지 보내보기 (0) | 2022.10.20 |
좌충우돌 LogBack 적용기 (0) | 2022.10.18 |