본문 바로가기

Java/Spring

Spring 5 입문: Chapter 07. AOP 프로그래밍(Aspect Oriented Programming)

더보기

이 프로젝트의 개발 환경

  • 개발 언어 및 개발 환경
    • OpenJDK 12
    • Spring: spring-context 5.0.2.RELEASE
    • AspectJ: aspectweaver: 1.8.13
    • Gradle 7.3
  • 기타 환경
    • macOS Sonoma 14.1.1
    • IntelliJ IDEA 2020.3 Ultimate Edition

예제 프로젝트 작성

sp5-chap07 프로젝트를 생성하고 chap07 패키지를 추가합니다.

$ tree
.
├── build.gradle
├── gradle
│   └── wrapper
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── src
    └── main
        ├── java
        └── resources

6 directories, 5 files

스프링 프레임워크의 AOP(Aspect Oriented Programming) 기능은 spring-aop 모듈이 제공합니다.

spring-context 모듈을 의존성에 추가하면 spring-aop 모듈도 함게 포함됩니다.

apsectjweaver 모듈은 AOP를 설정하는데 필요한 어노테이션을 제공합니다. aspectjweaver을 의존성에 새로 추가합니다.

dependencies {
    // https://mvnrepository.com/artifact/org.springframework/spring-context
    implementation group: 'org.springframework', name: 'spring-context', version: '5.0.2.RELEASE'
    // https://mvnrepository.com/artifact/org.aspectj/aspectjweaver
    implementation group: 'org.aspectj', name: 'aspectjweaver', version: '1.8.13'
}

chap07 패키지에서 Calculator 인터페이스를 생성합니다.

package chap07;

public interface Calculator
{
	public long factorial(long num);
}

chap07 패키지에서 ImpeCalculator 클래스를 생성합니다.

package chap07;

public class ImpeCalculator implements Calculator
{
	@Override
	public long factorial(long num)
	{
		long result = 1;
		for (long i = 1; i <= num; ++i) {
			result *= i;
		}
		
		return result;
	}
}

chap07 패키지에서 RecCalculator 클래스를 생성합니다.

package chap07;

public class RecCalculator implements Calculator
{
	@Override
	public long factorial(long num)
	{
		if (0 == num)
			return 1;
		else
			return num * factorial(num - 1);
	}
}

프록시와 AOP

예시의 클래스 ImpeCalculator에서 함수 실행 시간을 출력하는 코드를 추가합니다.

package chap07;

public class ImpeCalculator implements Calculator
{
	@Override
	public long factorial(long num)
	{
		long start = System.currentTimeMillis();
		long result = 1;
		for (long i = 1; i <= num; ++i) {
			result *= i;
		}
		
		long end = System.currentTimeMillis();
		System.out.printf("ImpeCalculator.factorial(%d) 실행 시간 = %d\n",
		                   num, (end - start));
		
		return result;
	}
}

예시의 클래스 RecCalculator에서 함수 실행 시간을 측정하는 것은 좀 더 복잡해집니다.

다음과 같이 함수 내부에서 실행 시간을 측정하면 재귀 함수 호출로 인해 시간 측정이 반복적으로 수행됩니다.

package chap07;

public class RecCalculator implements Calculator
{
	@Override
	public long factorial(long num)
	{
		long start = System.currentTimeMillis();
		try {
			if (0 == num)
				return 1;
			else
				return num * factorial(num - 1);	
		} finally {
			long end = System.currentTimeMillis();
			System.out.printf("RecCalculator.factorial(%d) 실행 시간 = %d\n",
			                  num, (end - start));
		}
	}
}

다른 방법은 함수를 최초 호출하는 코드에서 실행 시간을 측정하는 것입니다.

ReCalculator recCal = new ReCalculator();
long start = System.currentTimeMillis();
long result = recCal.factorial(4);
long end = System.currentTimeMillis();
System.out.printf("RecCalculator.factorial(%d) 실행 시간 = %d\n",
			num, (end - start));

하지만 위 방법 역시 실행 시간을 나노 초 단윌로 구해야 한다면 정합성이 떨어진다는 문제가 있습니다.

이때 기존 코드를 수정하지 않고도 문제를 해결하려면 프록시 객체를 사용합니다.

더보기

프록시(Proxy)대리 또는 중계의 의미로 사용됩니다.

일반적으로 IT 용어로 사용될 때 내부 네트워크에서 인터넷에 연결할 때 빠른 연결 또는 안정성을 위한 중계 서버를 프록시 서버라고 부릅니다.

프록시 서버는 클라이언트와 웹 서버와 중간에 위치합니다.

chap07 패키지에서 ExeTimeCalculator 클래스를 생성합니다.

package chap07;

public class ExeTimeCalculator implements Calculator
{
	private Calculator delegate;
	
	public ExeTimeCalculator(Calculator delegate)
	{
		this.delegate = delegate;
	}
	
	@Override
	public long factorial(long num)
	{
		long start = System.nanoTime();
		long result = delegate.factorial(num);
		long end = System.nanoTime();
		System.out.printf("ExeTimeCalculator.%s.factorial(%d) 실행 시간 = %d\n",
		                  delegate.getClass().getSimpleName(),
		                  num, (end - start));
		return result;
	}
}
코드 비고
Line 7:10 ExeTimeCalculator(Calculator delegate) ExeTimeCalculatorCalculator를 구현하는 동시에 Calculator를 구현하는 다른 객체를 저장합니다.
Line 12:22 factorial(long num) Calculator를 구현하는 함수입니다.
함수의 내용을 직접 구현하는 대신  Calculator를 구현하는 다른 객체의 함수를 호출합니다.

ExeTimeCalculator 클래스를 사용하면 ImpeCalculator의 실행 시간을 대신 측정 할 수 있습니다.

ImpeCalculator impeCal = new ImpeCalculator();
ExeTimeCalculator calculator = new ExeTimeCalculator(impeCalc);
long result = calculator.factorial(4);

프록시 객체가 호출된 이후 내부에서 동작하는 방식은 다음과 같습니다.

chap07 패키지에서 Main 클래스를 생성합니다.

package chap07;

public class Main
{
	public static void main(String[] args)
	{
		ExeTimeCalculator calculator0 = new ExeTimeCalculator(new ImpeCalculator());
		System.out.println(calculator0.factorial(20));
		
		ExeTimeCalculator calculator1 = new ExeTimeCalculator(new RecCalculator());
		System.out.println(calculator1.factorial(20));
	}
}

애플리케이션을 실행하고 출력 결과를 확인합니다. 기존 코드를 변경하지 않고 실행 시간을 출력합니다.

프록시의 특징은 핵심 기능을 직접 구현하지 않는다는 점입니다. 대신 프록시에 대입되는 객체의 기능을 대신 호출합니다. 특히 여러 객체에 걸쳐 공통으로 동작하는 것을 목표로 합니다.

더보기

엄밀히 말하면 ExeTimeCalculator 클래스는 프록시 객체라기보다는 데코레이터(Decorator) 객체입니다.

프록시는 접근 제어 관점에 초점이 맞춰져 있다면, 데코레이터는 기능 추가와 확장에 초점이 맞춰져 있습니다.

스프링의 AOP(Aspect Oriented Programming)

AOP는 관점 지향 프로그래밍으로써 여러 객체에 공통으로 적용할 수 있는 기능을 분리해서 재사용성을 높이는 기법입니다.

이를 위해 핵심 기능과 공통 기능을 분리하고 핵심 기능을 구현한 코드의 수정 없이 공통 기능을 적용합니다.

AOP를 구현하는 방법은 크게 다음 세 가지가 있습니다.

  • 컴파일 시점에 코드에서 공통 기능을 삽입하는 방법
  • 클래스 로딩 시점에 바이트 코드에서 공통 기능을 삽입하는 방법
  • 런타임에 프록시 객체를 생성해 공통 기능을 삽입하는 방법

컴파일 시점과 클래스 로딩 시점의 AOP 구현을 위해서는 AspectJ와 같은 전용 도구가 필요합니다.

스프링에서 제공하는 AOP는 프록시 객체를 생성하는 세 번째 방법에 해당합니다(클래스 로딩 시점에 대한 구현도 일부 지원하지만 대중적인 방법은 프록시 객체 사용입니다).

스프링의 AOP는 프록시 객체를 자동으로 생성합니다. AOP에서 구현하는 공통 기능은 Aspect라고 부릅니다.

용어 비고
Aspect 여러 객체에 공통으로 적용되는 기술을 의미합니다. 트랜잭션이나 보안 처리 등이 있습니다.
Advice Aspect를 언제 적용할지를 정의합니다. 예를 들면 함수가 호출되기 직전 또는 객체가 반환되는 시점 등이 있습니다.
Joinpoint Adivce를 적용 가능한 지점을 의미합니다. 함수 호출 필드 값 변경 등이 있습니다.
스프링에서는 프록시를 이용해 AOP를 구현하기 때문에 함수 호출에 대한 Joinpoint만을 지원합니다.
Pointcut Jointpoint의 부분 집합으로서 실제 Advice가 적용되는 지점을 나타냅니다.
스프링에서는 정규 표현식이나 AspectJ 문법을 사용하여 Pointcut을 정의합니다.
Weaving Advice를 핵심 로직 코드에 적용하는 것을 의미합니다.

Advice의 종류

스프링은 프록시를 이용해 AOP를 구현합니다.

프록시를 이용해서 함수 호출 시점에 Aspect를 적용하며 구현 가능한 Adivce 종류는 다음과 같습니다.

Adivce 비고
Before Advice 대상 객체의 함수가 호출되기 직전입니다.
After Returning Advice 대상 객체의 함수가 Exception 없이 종료된 이후입니다.
After Throwing Advice 대상 객체의 함수가 실행 도중에 Exception으로 종료된 이후입니다.
After Advice Exception 여부와 관계 없이 대상 객체의 함수가 종료된 이후입니다.
(try-catch-finally의 fianlly 블록과 유사합니다)
Around Advice 대상 객체의 함수가 호출되기 직전 종료된 이후 또는 Exception으로 종료된 이후입니다.

이 중에서 가장 많이 사용하는 Advice는 Around Advice입니다.

대상 객체의 함수 실행과 관련해서 다양한 시점에서 원하는 기능을 추가 할 수 있기 때문입니다.

스프링의 AOP 구현

스프링을 사용해 AOP를 구현하려면 다음 절차로 진행합니다.

  • Aspect로 사용할 클래스에서 @Aspect 어노테이션을 등록합니다.
  • @Pointcut 어노테이션을 등록해 공통 기능을 적용할 Pointcut을 정의합니다.
  • 공통 기능을 구현하고 있는 함수에서 @Around 어노테이션을 등록합니다.

sp5-chap07 프로젝트에서 chap07_b 패키지를 추가합니다.

이전 패키지 chap07의 소스 파일을 복사하여 사용합니다.

$ tree
.
├── build.gradle
├── gradle
│   └── wrapper
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── src
    └── main
        ├── java
        │   ├── chap07_a
        │   │   ├── Calculator.java
        │   │   ├── ExeTimeCalculator.java
        │   │   ├── ImpeCalculator.java
        │   │   ├── Main.java
        │   │   └── RecCalculator.java
        │   └── chap07_b
        │       ├── Calculator.java
        │       ├── ExeTimeCalculator.java
        │       ├── ImpeCalculator.java
        │       ├── Main.java
        │       └── RecCalculator.java
        └── resources

8 directories, 15 files

chap07_b 패키지에서 ExeTimeAspect 클래스를 생성합니다.

package chap07_b;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;

import java.util.Arrays;

@Aspect
public class ExeTimeAspect
{
	@Pointcut("execution(public * chap07_b..*(..))")
	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 sig = joinPoint.getSignature();
			System.out.printf("%s.%s(%s) 실행 시간 : %d ns\n",
			                  joinPoint.getTarget().getClass().getSimpleName(),
			                  sig.getName(),
			                  Arrays.toString(joinPoint.getArgs()),
			                  (finish - start));
		}
	}
}
코드 비고
Line 11 @Aspect AOP를 구현하는 클래스임을 등록합니다.
Line 14 @Pointcut("execution(public * chap07_b..*(..))") Aspect가 적용 가능한 지점에 부분 집합입니다.
chap07_b 패키지와 그 하위 패키지의 pulbic 접근 지정자 함수를 집합으로 정의합니다.
Line 20 @Around("publicTarget()") Around Advice를 설정합니다.
이 클래스의 publicTarget()에서 정의한 Pointcut에 Aspect를 사용합니다.
Line 21 ProceedingJoinPoint 프록시 대상 객체를 담고 있습니다.
대상 객체의 함수를 호출하거나 대상 객체의 정보를 읽기 위해 사용합니다.
더보기

자바에서 함수의 이름과 파라미터를 합쳐서 메소드 시그니처(Method signature)라고 부릅니다.

함수 이름이 다르거나, 파라미터 타입 또는 개수가 서로 다르다면 메소드 시그니처가 다르다고 표현합니다.

자바에서 리턴 타입이나 Exception 타입은 메소드 시그니처에 포함되지 않습니다.

sp5-chap07 프로젝트에서 chap07_b_ctx 패키지를 추가합니다.

새로 추가한 chap07_b_ctx 패키지에서 AppCtx 스프링 설정 클래스를 생성합니다.

package chap07_b_ctx;

import org.springframework.beans.factory.annotation.Configurable;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configurable
@EnableAspectJAutoProxy
public class AppCtx
{
	@Bean
	public ExeTimeAspect exeTimeAspect()
	{
		return new ExeTimeAspect();
	}
	
	@Bean
	public Calculator calculator()
	{
		return new RecCalculator();
	}
}
코드 비고
Line 8 @EnableAspectJAutoProxy @Aspect를 등록한 클래스를 공통 기능으로 사용하도록 선언합니다.
스프링은 @Aspect가 등록된 Bean 객체를 찾고 @Pointcut@Around 설정을 읽습니다.

chap07_b 패키지에서 Main 클래스에서 스프링 컨테이너를 생성하고 Aspect가 사용되도록 합니다.

package chap07_b;

import chap07_b_ctx.AppCtx;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Main
{
	public static void main(String[] args)
	{
		AnnotationConfigApplicationContext ctx
			= new AnnotationConfigApplicationContext(AppCtx.class);
		
		Calculator cal = ctx.getBean("calculator", Calculator.class);
		long fiveFact = cal.factorial(5);
		System.out.println("cal.factorial(5) = " + fiveFact);
		System.out.println(cal.getClass().getName());
		ctx.close();
	}
}

애플리케이션을 실행하고 출력 결과를 확인합니다.

RecCalculator.factorial([5]) 실행 시간 : 39484 ns
cal.factorial(5) = 120
com.sun.proxy.$Proxy18
코드 비고
Line 1 RecCalculator.factorial([5]) 실행 시간 : %d ns ExeTimeAspect 클래스의 measure() 함수 출력입니다.
Line 3 com.sun.proxy.$Proxy18 Bean 객체의 타입이 RecCalculator 클래스가 아닌 스프링이 자동 생성한 Proxy 객체입니다.

AOP가 적용된 Bean 객체는 Proxy 객체입니다.

Proxy 객체는 Aspect 객체를 호출하고 Aspect에서는 실제 Bean 객체를 호출합니다.

ProceedingJoinPoint

Aspect 공통 기능 구현은 핵심은 인자로 전달받은 ProceedingJoinPoint의 proceed()의 호출입니다.

@Around("publicTarget()")
public Object measure(ProceedingJoinPoint joinPoint) throws Throwable
{
	Object result = joinPoint.proceed();
	return result;	
}

ProceedingJoinPoint가 제공하는 주요 함수는 다음을 참고합니다.

리턴 타입 함수 비고
Object proceed() 대상 객체의 함수를 실행합니다.
Signature getSignature() 대상 객체의 함수 정보를 리턴합니다.
Object getTarget() 대상 객체를 리턴합니다.
Object[] getArgs() 대상 객체의 함수에 전달된 인자 목록을 리턴합니다.

대상 객체의 함수 정보는 Signature에서 읽을 수 있습니다. Signature에서 읽을 수 있는 함수의 주요 정보는 다음을 참고합니다.

리턴 타입 함수 비고
String getName() 대상 객체의 함수 이름입니다.
String toLongString() 대상 객체의 함수를 완전하게 표현한 문장을 리턴합니다.
(메소드 시그니처를 포함해 리턴 타입 등이 모두 표시됩니다)

프록시 객체의 생성 방식

sp5-chap07 프로젝트에서 chap07_cchap07_c_ctx패키지를 추가합니다.

이전 패키지 chap07_bchap07_b_ctx의 소스 파일을 복사합니다.

Main 클래스에서 엑세스하는 Bean 객체로 Calculator 대신 ReCalculator 타입을 사용하도록 수정합니다.

RecCalculator cal = ctx.getBean("calculator", RecCalculator.class);

스프링 설정 클래스 AppCtx에서 Bean 등록 코드를 다시 살펴봅니다.

@Bean
public Calculator calculator()
{
	return new RecCalculator();
}

calculator()에서 Bean 객체에 할당된 타입은 ReCalculator이기 때문에 표면상의 문제는 보이지 않습니다.

애플리케이션을 실행하고 출력 결과를 확인합니다.

Exception in thread "main" org.springframework.beans.factory.BeanNotOfRequiredTypeException: Bean named 'calculator' is expected to be of type 'chap07_c.RecCalculator' but was actually of type 'com.sun.proxy.$Proxy18'
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:384)
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:205)
	at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:1091)
	at chap07_c.Main.main(Main.java:13)

Bean 객체에 엑세스할 때 ReCalculator 타입을 지정하였으나 실제 타입은 com.sun.proxy.$Proxy18입니다.

$Proxy18은 스프링이 런타임에 생성한 프록시 객체의 클래스 이름입니다.

스프링은 AOP를 위한 프록시 객체를 생성할 때 Bean 객체가 인터페이스를 상속하면 해당 인터페이스를 상속하여 프록시 클래스를 구현합니다.

$Proxy18과 ReCalculator는 동일한 인터페이스를 구현하고 있지만 서로 다른 클래스입니다.

결국 Bean 객체에 접근하면서 $Proxy18과는 다른 타입으로 캐스팅하고 있어 오류가 발생하게 됩니다. 

더보기

Bean 객체가 인터페이스를 구현하고 있을 때 프록시 클래스가 Bean 객체 타입을 상속하게 하려면 @EnableAspectJAutoProxy에서 proxyTargetClass = true로 지정합니다.

@Configurable
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class AppCtx { }

이제 $Proxy18이 ReCalculator를 상속하게 됩니다.

Execution 명시자 표현식

Aspect를 적용할 위치를 지정하려면 @Pointcut을 등록합니다.

Pointcut에서는 execution 명시자를 사용합니다.

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

execution은 Advice를 적용할 메소드를 지정합니다. 명시자의 기본 형식은 다음과 같습니다.

execution(<접근 지정자> <리턴 타입> <패키지 이름> <클래스 이름> <함수 이름> (<함수 인자>)

스프링 AOP에서는 public 접근 지정자만 사용 할 수 있으며 생략 할 수 있습니다.

각 패턴은 * 심볼을 사용해 모든 값의 의미로 표현 할 수 있습니다. .. 심볼을 사용하면 0개 이상의 의미로 표현 할 수 있습니다.

예시 접근 지정자 리턴 타입 패키지 경로.클래스 이름.함수 이름 (함수 인자)
execution(Long chap07.Calculator.factorial(..)) 지정되지 않음 Long chap07. Calculator. factorial (..) 파라미터 0개 이상
execution(public void set*(..)) public void 지정되지 않음 지정되지 않음 set* (..) 파라미터 0개 이상
execution(* chap07.*.*()) 지정되지 않음 * chap07. *. * () 파라미터 없음
execution(* chap07..*.*()) 지정되지 않음 * chap07.. 하위 패키지 포함 *. * (..) 파라미터 0개 이상
execution(* get*(*)) 지정되지 않음 * get* (*) 파라미터 1개
execution(* get*(*, *)) 지정되지 않음 * get* (*, *) 파라미터 2개

Adivce 적용 순서 제어

Adivce는 Aspect가 언제 적용될지를 정의합니다. 하나의 Pointcut에서 여러 개의 Advice를 등록 할 수 있습니다.

sp5-chap07 프로젝트에서 chap07_d chap07_d_ctx패키지를 추가합니다.

이전 패키지 chap07_c chap07_c_ctx의 소스 파일을 복사합니다.

chap07_d 패키지에서 CacheAspect 클래스를 생성하고 다음 소스 코드를 입력합니다.

package chap07_d;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;

import java.util.HashMap;
import java.util.Map;

@Aspect
public class CacheAspect
{
	private Map<Long, Object> cache = new HashMap<>();
	
	@Pointcut("execution(public * chap07_d..*(long))")
	public void cacheTarget() {
		
	}
	
	@Around("cacheTarget()")
	public Object execute(ProceedingJoinPoint joinPoint) throws Throwable
	{
		Long num = (Long) joinPoint.getArgs()[0];
		if (cache.containsKey(num)) {
			System.out.printf("CacheAspect: Cache에서 구함[%d]\n", num);
			return cache.get(num);
		}
		
		Object result = joinPoint.proceed();
		cache.put(num, result);
		System.out.printf("CacheAspect: Cache에 추가[%d]\n", num);
		return result;
	}
}
코드 비고
Line 16 @Pointcut 이전 예제에서 작성한 ExeTimeAspect와 동일한 Pointcut을 사용하고 있습니다.

스프링 설정 클래스 AppCtx는 다음과 같이 Bean을 추가합니다.

@Bean
public CacheAspect cacheAspect() { return new CacheAspect(); }

CacheAspect를 테스트하기 위해 Main 클래스를 수정합니다.

package chap07_d;

import chap07_d_ctx.AppCtx;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Main
{
	public static void main(String[] args)
	{
		AnnotationConfigApplicationContext ctx
			= new AnnotationConfigApplicationContext(AppCtx.class);
		
		RecCalculator cal = ctx.getBean("calculator", RecCalculator.class);
		cal.factorial(7);
		cal.factorial(7);
		cal.factorial(5);
		cal.factorial(5);
		ctx.close();
	}
}

애플리케이션을 실행하고 출력 결과를 확인합니다.

RecCalculator.factorial([7]) 실행 시간 : 12332918 ns
CacheAspect: Cache에 추가[7]
CacheAspect: Cache에서 구함[7]
RecCalculator.factorial([5]) 실행 시간 : 4179 ns
CacheAspect: Cache에 추가[5]
CacheAspect: Cache에서 구함[5]

처음 인자로 전달된 값에 대해서는 두 개의 Aspect가 모두 실행되고 있습니다. 이미 전달된 인자가 다시 입력되면 ExeTimeAspect는 실행되지 않습니다.

어떤 Aspect가 먼저 적용될지는 스프링 프레임워크나 자바 버전에 따라서 달라질 수 있습니다.

만약 Aspect의 실행 순서가 중요하다면 @Order를 사용해 순서를 제어해야 합니다. @Order로 지정된 순서는 오름차순으로 실행됩니다.

@Aspect
@Order(1)
public class ExeTimeAspect { }

@Aspect
@Order(2)
public class CacheAspect { }

다음과 같이 @Order를 등록하고 실행 결과를 확인합니다. 

ExeTimeAspect이 먼저 실행되도록 순서를 지정하고 있습니다. 이제 인자가 반복 전달되더라도 두 개의 Aspect는 항상 실행됩니다.

CacheAspect: Cache에 추가[7]
RecCalculator.factorial([7]) 실행 시간 : 12002653 ns
CacheAspect: Cache에서 구함[7]
RecCalculator.factorial([7]) 실행 시간 : 112485 ns
CacheAspect: Cache에 추가[5]
RecCalculator.factorial([5]) 실행 시간 : 97671 ns
CacheAspect: Cache에서 구함[5]
RecCalculator.factorial([5]) 실행 시간 : 108744 ns

@Around의 Pointcut 설정과 @Pointcut 재사용

이전 예시에서는 Pointcut을 지정하기 위해 빈 메소드를 정의하고 @Pointcut을 등록했습니다.

@Pointcut("execution(public * chap07_d..*(long))")
public void cacheTarget() { }

@Around에서도 execution 명시자를 직접 지정할 수 있는데, 이렇게 되면 빈 메소드를 정의하지 않아도 돕니다.

@Around("execution(public * chap07_d..*(long))")
public Object execute(ProceedingJoinPoint joinPoint) throws Throwable { }

execution 명시자를 재활용 하려면 기존 방식처럼 @Pointcut 빈 메소드를 정의하고 동시에 참조하도록 합니다.

이 때 외부 Aspect에서 접근학게 하려면 접근 지정자는 public입니다.

ExeTimeAspect에서 Pointcut의 접근 지정자를 private에서 public으로 수정합니다.

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

CacheAspect@Around를 다음과 같이 지정합니다.

@Around("chap07_d.ExeTimeAspect.publicTarget()")
public Object execute(ProceedingJoinPoint joinPoint) throws Throwable { }
더보기

execution 명시자가 참조하는 @Pointcut이 동일한 패키지인 경우 패키지 경로를 생략 할 수 있습니다.

@Around("ExeTimeAspect.publicTarget()")
public Object execute(ProceedingJoinPoint joinPoint) throws Throwable { }

정리 및 복습

  • 스프링의 AOP(Aspect Oriented Programming)을 사용하면 함수의 전후 처리 과정을 공통 기능으로 구현 할 수 있습니다.
  • 스프링의 AOP는 런타임에 프록시 객체를 생성하여 동작합니다.
  • AOP의 주요 용어는 Aspect Advice Joinpoint Pointcut Weaving이 있습니다.
  • Aspect는 공통 기능 즉 AOP로 처리되는 로직을 의미합니다.
  • Advice는 Aspect가 적용되는 시점입니다.
  • Joinpoint는 Advice를 적용하는 지점입니다. Joinpoint를 표현하여 부분 집합을 구성하면 Pointcut이라고 부릅니다.
  • Weaving은 Advice를 로직에 적용한 것을 의미하며 AOP를 적용한 것과 동일합니다.
  • Advice는 Before Advice After Returning Advice After Throwing Advice After Advice Around Advice가 있습니다.
  • 스프링에서 AOP 구현은 다음 절차로 구분됩니다.
@EnableAspectJAutoProxy @Aspect @Pointcut @Around
스프링 설정 클래스에서 등록합니다. AOP를 구현하는 클래스에서 등록합니다. 빈 메소드를 정의하거나 @Around에서 지정합니다.
어떤 함수를 AOP로 동작시킬지 결정합니다.
AOP로 동작하는 공통 기능을 구현하는 함수에서 등록합니다.
  • AOP로 동작하는 Bean 객체를 생성하면 실제 타입은 스프링이 런타임에 생성한 $Proxy<Number>입니다.
  • Bean 객체가 인터페이스를 상속하면 프록시 객체도 인터페이스를 상속합니다.
  • 프록시 객체가 Bean 객체의 실제 클래스를 상속하게 하려면 @EnableAspectJAutoProxy(proxyTargetClass = true)로 지정합니다.
@Configurable
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class AppCtx { }
  • Advice 적용 순서를 제어하려면 @Order(n)를 사용합니다. 실행 우선 순위는 n에 대해서 오름차순입니다.
  • @Around에서 Pointcut을 직접 명시하거나 다른 @Pointcut을 참조 할 수 있습니다.