스프링5; AOP의 실제 #1

🗓️

  • 스프링에서 프록시 패턴을 어떻게 적용시키는지 실제로 스프링을 통해 적용해본다.
  • proxy 패턴decorator 객체 로도 활용할 수 있다. 기능 추가와 확장에 초점이 맞춰져있다.

짤막한 프록시의 핵심

  • 프록시의 특징은 핵심 기능은 구현하지 않는다는 점이다.
  • 프록시는 핵심 기능을 구현하지 않는 대신 여러 객체에 공통으로 적용할 수 있는 기능을 구현한다.
  • AOP의 기본 핵심은 공통 기능을 삽입하는 것 이다.

AOP의 주요 용어

1. Advice

  • 공통 관심 기능을 핵심 로직에 적용하는 시점을 정의한다.
  • ex) 메소드를 호출하기 전트랜잭션을 시작하는 기능을 적용한다.

2. Joinpoint

  • Advice를 적용하는 지점을 의미한다.
  • 메소드 호출, 필드 값 변경 등을 포함한다.
  • 스프링은 메소드 호출에 대한 Joinpoint만 지원.

3. Pointcut

  • Advice가 실제 적용되는 Joinpoint를 의미한다.
  • 스프링에서는 정규표현식이나 AspectJ의 문법을 이용해 Poincut을 정의한다.

4. Weaving

  • Advice를 핵심 로직 코드에 적용하는 행위.

5. Aspect

  • 여러 객체에 공통으로 적용되는 기능을 Aspect라고 한다.

Aspect의 종류

  • 스프링에서 구현 가능한 Aspect의 종류
  1. Before Advice : 메소드 호출 전 Aspect를 실행한다.
  2. After Returning Advice : 메소드가 Exception 없이 실행 된 이후 Aspect를 실행한다.
  3. After Throwing Advice : 메소드가 Exception 이 발생한 경우 Aspect를 실행한다.
  4. After Advice : Exception 상관 없이 메소드 실행 후 Aspect를 실행한다.
  5. ? Around Advice : 메소드 실행 전, 후, Exception 발생 시점에 Aspect를 실행한다.

이 중에서 가장 많이 사용되는 것은 Around Advice 방식인데, 시점을 다양하게 사용할 수 있기 때문이다.

스프링의 AOP구현

  • 프록시를 적용할 클래스에 @Aspect 어노테이션을 붙인다.
  • @Pointcut 어노테이션으로 Joinpoint 를 지정한다.
  • 공통 기능을 구현한 메소드에 @Around 어노테이션을 지정한다.

1. @Aspect, @Pointcut, @Around를 이용한 AOP 구현

@Aspect
public class ExecutiveTimeAspect {

    @Pointcut("execution(public * chap07..*(..))")
    private void publicTarget() {
    }

    @Around("publicTarget()")
    public Object measure(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.nanoTime();
        try {
            Object result = joinPoint.proceed();
            return result;
        } finally {
            long finish = System.nanoTime();
            Signature signature = joinPoint.getSignature();
            System.out.printf("%s.%s(%s) runtime : %s ns\n",
                joinPoint.getTarget().getClass().getSimpleName(),
                signature.getName(), Arrays.toString(joinPoint.getArgs()),
                (finish - start));
        }
    }
  • @Aspect 어노테이션을 적용한 클래스는 Advice(@Around)와 Pointcut을 제공한다.
  • @Pointcut 어노테이션은 공통 기능을 적용할 대상을 지정한다.
  • @Around 어노테이션은 publicTarget()의 pointcut에 공통 기능을 적용한다는 의미다.
    • measure()ProceedingJoinPoint 타입 파마리터는 프록시 대상 객체의 메소드를 호출할 때 사용한다.
    • joinPoint.proceed() 메소드를 호출하면 대상 객체의 메소드가 실행되므로 이 코드 이전과 이후에 공통 기능을 위한 코드를 위치시키면 된다. (기존 코드의 delegate 위치와 같다)

Bean설정

@Configuration
@EnableAspectJAutoProxy
public class AppContext {

    @Bean
    public ExecutiveTimeAspect executiveTimeAspect() {
        return new ExecutiveTimeAspect();
    }

    @Bean
    public Calculator calculator() {
        return new ReCalculator();
    }
}
  • @EnableAspectJAutoProxy 어노테이션을 bean 설정에 붙이면, @Aspect 클래스를 공통 기능으로 사용할 수 있다.
  • 스프링은 @Aspect 어노테이션이 붙은 bean 객체를 찾아 @Pointcut 설정과 @Around 설정을 사용한다.

스프링 컨테이너

public class MainAspect {

    public static void main(String[] args) {

        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
            AppContext.class);

        Calculator calculator = context.getBean("calculator", Calculator.class);
        long fiveFactorial = calculator.factorial(5);
        System.out.println("calculator.factorial(5) = " + fiveFactorial);
        System.out.println(calculator.getClass().getName());
        context.close();
    }
  • 아래는 실행 결과다
ReCalculator.factorial([5]) runtime : 22574 ns
calculator.factorial(5) = 120
com.sun.proxy.$Proxy17
  • 첫번째 줄은 ExecutiveCalculatorAspect클래스의 measure()가 출력한 것이다.
  • 세번째 줄은 MainAspect의 main에서 getName()을 출력한 것이다.
  • 아래와 같은 실행 구조를 가진다.

  • 여기서 AOP를 적용하지 않으면 $Proxy17 대신 ReCalculator를 반환한다. (ExecutiveTimeAspect bean설정 주석처리)
calculator.factorial(5) = 120
chap07.ReCalculator

ProceedingJoinPoint의 메소드

  • Around Advice 방식에서 사용할 공통 기능 메소드는 대부분 파라미터로 전달받은 ProceedingJoinPointproceed()메소드만 호출하면 된다.

ProceedingJoinPoint 인터페이스에서 제공하는 메소드

  • Signature getSignature(): 호출 되는 메소드에 대한 정보를 구한다.
  • Object getTarget(): 대상 객체를 구한다.
  • Object[] getArgs(): 파라미터 목록을 구한다.

org.aspectj.lang.Signature 인터페이스에서 제공하는 메소드

  • String getName(): 호출되는 메소드의 이름을 구한다.
  • String toLongString(): 호출되는 메소드의 모든 정보를 구한다.
  • String toShortString(): 호출되는 메소드를 축약한다 (이름만)

스프링에서 프록시를 생성할 때

  • 스프링에서 에서 다음과 같이 ReCalculator를 사용한다.
// 스프링 컨테이너
Calculator calculator = context.getBean("calculator", Calculator.class);

// bean  설정
@Bean
public Calculator calculator() {
    return new ReCalculator();
}
  • ReCalculator를 사용하는데 Calculator 타입을 사용한다. 이것을 ReCalculator를 사용하도록 바꾸면 Expection이 발생한다.
  • 이유는 런타임에 생성한 프록시 객체인 $Proxy17ReCalculator구현체가 상속받는 Calculator인터페이스를 상속받기 때문이다.
  • bean 객체가 인터페이스를 상속할 때 인터페이스가 아닌 구현체를 이용해서 프록시를 생성하고 싶다면 다음과 같이 하면된다.
// 스프링 컨테이너
AnnotationConfigApplicationContext context =
    new AnnotationConfigApplicationContext(AppContext.class);

ReCalculator calculator = context.getBean("calculator", ReCalculator.class);

// bean 설정
@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class AppContext {
//...
  • 아래는 실행 결과다
ReCalculator.factorial([5]) runtime : 15285343 ns
calculator.factorial(5) = 120
chap07.ReCalculator$$EnhancerBySpringCGLIB$$7bd3a4d7

ReCalculator를 받는다.

  • 다음엔 excution 필터와 Advice 적용 순서에 대해서 마저 정리하겠다.