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개의 역할이 필요합니다.

  1. Advice
    • 부가기능을 담고 있는 클래스입니다.
  2. Pointcut
    • Advice가 적용될 조인 포인트를 선별합니다.
    • 조인 포인트는 Advice가 적용 될 위치라고 생각하시면 됩니다.
  3. 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 객체를 생성하여 설정해 주고 인스턴스를 생성하면 됩니다.

  1. 우선 프록시를 적용할 인스턴스와 Advisor을 미리 만들어 둡니다.
  2. 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 프록시 객체를 만들어 준다고 합니다. 자세한 내용은 다음과 같습니다. 

 

https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/aop/framework/autoproxy/DefaultAdvisorAutoProxyCreator.html

 

DefaultAdvisorAutoProxyCreator (Spring Framework 5.3.23 API)

BeanPostProcessor implementation that creates AOP proxies based on all candidate Advisors in the current BeanFactory. This class is completely generic; it contains no special code to handle any particular aspects, such as pooling aspects. It's possible to

docs.spring.io

 

+ Recent posts