핵심 기능의 실행은 다른 객체에 위임하고 부가적인 기능을 제공하는 객체
책에 나온 예제대로 팩토리얼의 실행 시간을 측정하는 객체: 프록시
실제로 팩토리얼 연산을 수행하는 객체: 대상 객체
라고 표현한다.
프록시는 핵심 기능을 구현하지 않는다. 대신 여러 객체에 공통으로 적용할 수 있는 기능을 구현한다. 이렇게 공통 기능과 핵심 기능을 나누어 구현하는 것이 AOP의 핵심이다.
AOP(Aspect Oriented Programming)는 여러 객체에 공통으로 적용할 수 있는 기능을 분리하여 재사용성을 높여주는 프로그래밍 기법이다.
핵심 기능의 코드를 수정하지 않으면서 공통 기능을 추가할 수 있도록 해준다.
AOP에는 다음 세 가지 방법이 있다.
첫번째와 두번째 방법은 스프링 AOP에서는 지원하지 않으며 AspectJ와 같은 AOP 전용 도구를 사용해야 한다.
스프링이 제공하는 AOP 방식은 세 번째 방식이다. 스프링 AOP는 프록시 객체를 자동으로 만들어주기 때문에 공통 기능을 구현한 클래스만 알맞게 구현하면 된다.
AOP의 용어
Advice의 종류
Around Advice가 가장 널리 사용된다.
package aspect;
import java.util.Arrays;
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;
@Aspect
public class ExeTimeAspect {
@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 sig = joinPoint.getSignature();
System.out.printf("%S.%s(%s) 실행 시간 : %d ns\n",
joinPoint.getTarget().getClass().getSimpleName(),
sig.getName(), Arrays.toString(joinPoint.getArgs()),
(finish - start));
}
}
}
** 시그니처란? 자바에서 메서드 이름과 파라미터를 합쳐서 부르는 말
Aspect 애노테이션이 붙은 클래스를 공통 기능으로 적용하려면 스프링 설정 클래스에 @EnableAspectJAutoProxy 애노테이션을 추가해야 한다.
스프링은 AOP를 위한 프록시 객체를 생성할 때 실제 생성할 빈 객체가 인터페이스를 상속하면 인터페이스를 이용해서 프록시를 생성한다.
빈 객체가 인터페이스를 상속할 때 인터페이스가 아닌 클래스를 이용해서 프록시를 생성하고 싶다면 다음과 같은 코드를 추가해야 한다.
@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = true)
이렇게 proxyTargetClass를 true로 설정하면 자바 클래스를 상속받아 프록시를 생성한다.
위에서 Aspect를 적용할 위치를 지정할 때 사용한 Pointcut 설정을 보면 execution 명시자를 사용하였다.
이 명시자는 Advice를 적용할 메서드를 지정한다. 기본적인 형식은 다음과 같다.
execution(수식어패턴?리턴타입패턴 클래스이름패턴?메서드이름패턴(파라미터패턴))
*은 모든 값, ..는 0개 이상을 뜻한다.
한 Pointcut에 여러 Advice를 적용할 수도 있다.
package aspect;
import java.util.HashMap;
import java.util.Map;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
@Aspect
public class CacheAspect {
private Map<Long, Object> cache = new HashMap<>();
@Pointcut("execution(public * chap07..*(long)")
public void cahceTarget() {
}
@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;
}
}
Pointcut 설정이 첫 번째 인자가 long인 메서드이므로 기존 Calculator의 factorial 메서드에도 적용된다.
어떤 Aspect가 먼저 적용될지는 스프링 프레임워크나 자바 버전에 따라 달라질 수 있기 때문에 순서를 지정하고 싶다면 @Order 애노테이션을 사용해야한다.
@Aspect
@Order(1)
숫자가 작은 Aspect가 먼저 적용된다.
Pointcut 애노테이션이 아닌 @Around 애노테이션에 execution 명시자를 직접 지정할 수도 있다.
@Around("execution(public * chap07..*())")
다른 클래스에 위치한 @Around 애노테이션에서 같은 Pointcut을 재사용하고 싶다면 @Pointcut이 붙은 메서드를 public으로 지정하고 @Around 애노테이션에 해당 Pointcut의 완전한 클래스 이름을 포함한 메서드 이름을 사용하면 된다.
공통으로 사용되는 Pointcut들은 common 클래스로 빼서 따로 관리하는 것이 편리하다.