우리는 흔히 특정 로직의 실행 시간을 측정하여 해당 로직의 유효성 혹은 효율성을 판단해야 할 때가 있다. 이때 어떤 방법을 사용할 수 있을까?
가장 쉬운 방법으로는 메서드의 시작과 끝에서 시각을 구하고 두 시각의 차이를 나타내는 방법이다.
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로 빼서 중복을 제거했다.
여러 객체에 공통으로 적용할 수 있는 기능을 분리해서 재사용성을 높여주는 프로그래밍 기법
핵심 기능을 방해하거나 해치지 않으면서 공통 기능을 삽입하는 것을 말한다.
스프링이 제공하는 방식은 마지막 세 번째 방식으로, 프록시 객체를 활용한다.
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 명시자는 Advice를 적용할 메서드를 지정할 떄 사용한다.
@Pointcut("execution(public * chap07..*(..))")
private void publicTarget() {
}
기본 형식은 다음과 같다.
execution(수식어패턴? 리턴타입패턴 클래스이름패턴?메서드이름패턴(파라미터패턴))
각 패턴은 '*'을 이용하여 모든 값을 표현할 수 있다. 또한 '..'을 이용하여 0개 이상이라는 의미를 표현한다.
@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; // 프록시 대상 객체의 실행 결과 리턴
}
}
캐시를 구현한 공통 기능이다.
이 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);
@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
[스프링 1] 8장. DB 연동(2) (1) | 2022.11.17 |
---|---|
[스프링1] 8장. DB 연동 (0) | 2022.11.10 |
[스프링1] 6장. 빈 라이프 사이클과 범위 (0) | 2022.11.05 |
[스프링1] 5장. 컴포넌트 스캔 (0) | 2022.11.05 |
[스프링1] 4장. 의존 자동 주입 (0) | 2022.11.05 |