[스프링1] 7장. AOP 프로그래밍
우리는 흔히 특정 로직의 실행 시간을 측정하여 해당 로직의 유효성 혹은 효율성을 판단해야 할 때가 있다. 이때 어떤 방법을 사용할 수 있을까?
❗ 실행 시간을 측정하는 방법 : 프록시
📌 메서드의 시작과 끝
가장 쉬운 방법으로는 메서드의 시작과 끝에서 시각을 구하고 두 시각의 차이를 나타내는 방법이다.
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;
}
}
재귀 호출이기 때문에 해당 함수가 재귀적으로 호출될 때마다 실행 시간이 출력된다는 사소한 문제가 발생하기도 한다.
그것보다 더 큰 문제는 코드의 가독성이 급격히 저하된다는 점이다.
📌 프록시 객체
프록시(proxy) : 핵심 기능의 실행은 다른 객체에게 위임하고 부가적인 기능을 제공하는 객체
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("%s.factorial(%d) 실행 시간 = %d\n",
delegate.getClass().getSimpleName(), num, (end-start));
return result;
}
}
실제 계산기 로직을 delegate로 위임하여 계산하는 방식이다.
ExeTimeCalculator에는 핵심 로직을 구현하지 않고, 시간을 계산하는 로직만 존재한다.
사용할 땐 아래와 같이 ExeTimeCalculator를 생성하고 그 안에 사용할 계산기를 넣어준다.
ImpeCalculator impeCal = new ImpeCalculator();
ExeTimeCalculator calculator = new ExeTimeCalculator(impeCal);
long result = calculator.factorial(4);
1) 프록시 객체를 사용하는 곳에서 직접 factorial 로직을 구현하는 게 아니라
delegate(Calculator) 객체로 로직을 위임하여 어떤 calculator 로직이냐에 관계 없이 시간을 측정할 수 있게 되었다.
2) 실행 시간을 구하는 코드를 ExeTimeCaculator로 빼서 중복을 제거했다.
❗ AOP(Aspect Oriented Programming)
여러 객체에 공통으로 적용할 수 있는 기능을 분리해서 재사용성을 높여주는 프로그래밍 기법
📌 AOP의 기본 개념
핵심 기능을 방해하거나 해치지 않으면서 공통 기능을 삽입하는 것을 말한다.
- 컴파일 시점에 코드에 공통 기능을 삽입하는 방법
- 클래스 로딩 시점에 바이트 코드에 공통 기능을 삽입하는 방법
- 런타임에 프록시 객체를 생성해서 공통 기능을 삽입하는 방법
스프링이 제공하는 방식은 마지막 세 번째 방식으로, 프록시 객체를 활용한다.
📌 AOP 주요 용어
- Advice : 언제 공통 관심 기능을 핵심 로직에 적용할 지를 정의한다.
- Joinpoint : Advice를 적용 가능한 지점을 의미한다. 메서드 호출, 필드 값 변경 등이 Joinpoint에 해당한다.
- Pointcut : Joinpoint의 부분 집합으로서 실제 Advice가 적용되는 Joinpoint를 나타낸다.
- Weaving : Advice를 핵심 로직 코드에 적용하는 것을 weaving이라고 한다.
- Aspect : 여러 객체에 공통으로 적용되는 기능을 Aspect라고 한다. 트랜잭션이나 보안 등이 Aspect의 좋은 예다.
❗ 스프링으로 AOP 구현하기
📌 @Aspect, @Pointcut, @Around
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..*(..))")
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 애노테이션을 적용한 클래스에서 Advice와 Pointcut을 사용할 수 있다.
@Pointcut은 공통 기능을 적용할 대상을 설정한다.
@Around은 Around Advice를 설정한다. publicTarget 메서드에 정의한 Pointcut에 공통 기능을 적용한다는 것을 의미한다.
@Configuration
@EnableAspectJAutoProxy
public class AppCtx {
@Bean
public ExeTimeAspect exeTimeAspect() {
return new ExeTimeAspect();
}
@Bean
public Calculator calculator() {
return new RecCalculator();
}
}
@Aspect 애노테이션을 붙인 클래스를 공통 기능으로 적용하려면 @EnableAspectJAutoProxy 애노테이션을 설정 클래스에 붙여야 한다. 이 애노테이션을 추가하면 스프링은 @Aspect 애노테이션이 붙은 빈 객체를 찾아 빈 객체의 @Pointcut 설정과 @Around 설정을 사용한다.
@Pointcut("execution(public * chap07..*(..))")
private void publicTarget() {
}
@Around("publicTarget()")
public Object measure(ProceedingJoinPoint joinPoint) throws Throwable {
...
}
@Around 애노테이션은 Pointcut으로 publicTarget() 메서드를 설정했다.
public class MainAspect {
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();
}
}
❗ 프록시 생성 방식
getBean 메서드에 Calculator 타입 대신 RecCalculator 타입을 사용하도록 하면 실제 타입과 기대한 타입이 다르다는 오류가 뜬다. 이때 실제 타입은 'com.sun.proxy.$Proxy17'이라고 뜨는데, 이 $Proxy17 클래스는 AOP를 위한 프록시 객체이며, RecCalculator 클래스가 상속받은 Calculator 인터페이스를 상속받는다.
// 수정 전
Calculator cal = ctx.getBean("calculator", Calculator.class);
// 수정 후
RecCaculator cal = ctx.getBean("calculator", RecCalculator.class);
정리하자면, AOP를 위한 프록시 객체를 생성할 때는 실제 생성할 빈 객체가 인터페이스를 상속하면 인터페이스를 이용해서 프록시를 생성한다는 것이다.
이때, 인터페이스가 아닌 클래스로 프록시를 설정하고 싶다면 다음과 같이 설정한다.
@Configuration
@EnableAspectAutoProxy(proxyTargetClass = true)
public class AppCtx {
❗ execution 명시자 표현식
execution 명시자는 Advice를 적용할 메서드를 지정할 떄 사용한다.
@Pointcut("execution(public * chap07..*(..))")
private void publicTarget() {
}
기본 형식은 다음과 같다.
execution(수식어패턴? 리턴타입패턴 클래스이름패턴?메서드이름패턴(파라미터패턴))
- 수식어패턴 : public, protected 등
- 리턴타입패턴 : 리턴 타입 명시
- 클래스이름패턴 : 클래스 이름을 패턴으로 명시
- 메서드이름패턴 : 메서드 이름을 패턴으로 명시
- 파라미터패턴 : 매칭될 파라미터 명시
각 패턴은 '*'을 이용하여 모든 값을 표현할 수 있다. 또한 '..'을 이용하여 0개 이상이라는 의미를 표현한다.
❗ 프록시 Advice 적용 순서
@Aspect
public class CacheAspect {
private Map<Long, Object> cache = new Hashmap<>();
@Pointcut("execution(public * chap07..*(long))")
public void cacheTarget() {
}
@Around("cacheTarget()")
public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
Long num = (Long) joinPoint.getArgs()[0]; // 첫번째 인자를 Long 타입으로 구한다
// 구한 값이 cache에 존재하면 키에 해당하는 값을 구해 리턴한다
if(cache.containsKey(num)) {
System.out.printf("CacheAspect: Cache에서 구함 [%d]\n", num);
return cache.get(num);
}
Object result = joinPoint.proceed();
cache.put(num, result); // cache에 값이 존재하지 않으면 프록시 대상 객체를 실행한다
System.out.printf("CacheAspect: Cache에 추가 [%d]\n", num); // 프록시 대상 객체를 실행한 결과를 cache에 추가
return result; // 프록시 대상 객체의 실행 결과 리턴
}
}
캐시를 구현한 공통 기능이다.
- @Around 값으로 cacheTarget() 메서드 지정
- @Pointcut 설정은 첫 번째 인자가 long인 메서드를 대상
이 Aspect를 스프링 설정 클래스에 다음과 같이 적용한다.
@Configuration
@EnableAspectJAutoProxy
public class AppCtxWithCache {
...
@Bean
public CacheAspect cacheAspect() {
return new CacheAspect();
}
...
}
이 설정 클래스는 다음과 같이 이용한다.
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AppCtxWithCache.class);
Calculator cal = ctx.getBean("calculator", Calculator.class);
❗ @Around의 Pointcut 설정과 @Pointcut 재사용
@Aspect
public class CacheAspect {
@Around("execution(public * chap07..*(..))")
public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
...
}
}
@Pointcut 애노테이션이 아닌 @Around 애노테이션에 execution 명시자를 직접 지정할 수도 있다.
Pointcut을 여러 Advice가 함께한다면 공통 Pointcut을 재사용할 수도 있다.
다른 클래스에 있는 특정 public Pointcut을 사용하려면 다음과 같이 명시할 수 있다.
패키지명.클래스명.Pointcut명
@Aspect
public class CacheAspect {
@Around("aspect.ExeTimeAspect.publicTarget()")
public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
...
}
}
Spring 1
EDITOR: PORO