이전 글
[Spring] Spring AOP
AOP AOP : Aspect Oriented Programming의 약자로, 여러 객체에 공통으로 적용할 수 있는 기능을 분리해서 재사용성을 높여주는 프로그래밍 기법입니다. AOP는 핵심 기능과 공통 기능의 구현을 분리함으로
muscleking3426.tistory.com
스프링 AOP 구현
스프링 AOP를 이용해서 공통 기능을 구현하고 적용하는 방법은 단순합니다.
- Aspect로 사용할 클래스에 @Aspect를 붙인다.
- @Pointcut으로 공통 기능을 적용할 Pointcut을 정의한다.
- 공통 기능을 구현한 메서드에 @Around를 적용한다.
@Aspect, @Pointcut, @Around를 이용한 AOP 구현
개발자는 공통 기능을 제공하는 Aspect 구현 클래스를 만들고 자바 설정을 이용해서 Aspect를 어디에 적용하지 설정하면 됩니다. Aspect는 @Aspect을 이용해서 구현합니다. 프록시는 스프링이 알아서 만들어줍니다.
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 mesaure(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\n"
, joinPoint.getTarget().getClass().getSimpleName()
, sig.getName()
, Arrays.toString(joinPoint.getArgs())
, finish - start);
}
}
}
@Aspect를 적용한 클래스는 Advice와 Pointcut을 함께 제공합니다.
@Pointcut은 공통 기능을 적용할 대상을 설정합니다.
chap07 패키지와 그 하위 패키지에 위치한 타입의 public 메서드를 Pointcut으로 설정합니다.
@Around는 Around Advice를 설정합니다. @Around값이 "publicTarget()"인데 이는 publicTarget() 메서드에 정의한 PointCut에 공통 기능을 적용한다는 것을 의미합니다. publicTarget() 메서드는 chap07 패키지와 그 하위 패키지에 위치한 public 메서드를 Pointcut으로 설정하고 있으므로, chap07 패키지나 그 하위 패키지에 속한 빈 객체의 public 메서드에 @Around가 붙은 mesaure() 메서드를 적용합니다.
measure() 메서드의 ProceedingJoinPoint 타입 파라미터는 프록시 대상 객체의 메서드를 호출할 때 사용합니다. 위의 코드 proceed() 메서드를 사용해서 실제 대상 객체의 메서드를 호출합니다. 이 메서드를 호출하면 대상 객체의 메서드가 실행되므로 이 코드이전과 이후에 공통 기능을 위한 코드를 위치시키면 됩니다. proceed() 메서드를 호출하기 전과 후에 현재 시간을 구한 뒤 출력하고 있는 것을 볼 수 있습니다.
ProceedingJoinPoint의 getSignature(), getTarget(), getArgs() 등의 메서드를 사용하는 것을 볼 수 있는데, 각 메서드는 호출한 메서드의 시그니처, 대상객체, 인자 목록을 구하는 데 사용됩니다. 이 메서드들을 이용해서 대상 객체의 클래스 이름과 메서드 이름을 출력합니다.
자바에서 메서드 이름과 파라미터를 합쳐서 메서드 시그니처라고 합니다. 메서드 이름이 다르거나 파라미터 타입, 개수가 다르면 시그니처가 다르다고 표현합니다. 자바에서 메서드의 리턴 타입이나 익셉션 타입은 시그니처에 포함되지 않습니다.
공통 기능을 적용하는데 필요한 코드를 구현해 봤습니다.
스프링 설정 클래스를 작성해 보겠습니다.
package chap07;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@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() 메서드를 설정했습니다. publicTarget() 메서드의 @Pointcut은 chap07 패키지나 그 하위 패키지에 속한 빈 객체의 public 메서드를 설정합니다.
위 AppCtx 설정 클래스에서 설정한 Calculator 타입이 chap07 패키지에 속하므로 calculator 빈에 ExeTimeAspect 클래스에 정의한 공통 기능인 mesaure()를 적용합니다.
@Enable 류의 어노테이션
스프링은 이름이 @Enable로 시작하는 다양한 어노테이션을 제공합니다. @Enable로 시작하는 어노테이션은 관련 기능을 적용하는데 필요한 다양한 스프링 설정을 대신 처리합니다.
예를 들어, @EnableAspectJAutoProxy는 프록시 생성과 관련된 AnnotationAwareAspectJAutoProxyCreator 객체를 빈으로 등록합니다. 웹 개발과 관련된 @EnableWebMvc 웹 개발의 다양한 설정을 등록합니다.
다음은 calculator 빈에 공통 기능이 적용되는지 AppCtx 설정을 사용해서 스프링 컨테이너를 생성해 보겠습니다.
package main;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import chap07.Calculator;
import config.AppCtx;
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();
}
}
package chap07;
public class RecCalculator implements Calculator {
@Override
public long factorial(long num) {
try {
if (num == 0) {
return 1;
} else
return num * factorial(num - 1);
} finally {
}
}
}
RecCalculator.factorial([5]) 실행시간 : 16042
cal.factorial(5) = 120
com.sun.proxy.$Proxy17
MainAspect 동작과정을 설명해 보겠습니다.
1. MainAspect - factorial(5) 호출
2. $Proxy17 - mesaure() 호출
3. ExeTimeAspect - proceed() 호출
4. ProceedingJoinPoint - factroial(5) 호출
5. RecCalculator.facorial(5) 리턴
5 -> 1까지 facorial(5)의 결과 값을 리턴하게 됩니다.
AOP를 적용하지 않았으면 cal.getClass().getName() 은 프록시 객체가 아닌 RecCalculator 타입이었을 겁니다.
AppCtx 클래스에서 exeTimeAspect() 메서드를 주석처리하고 실행해 보겠습니다.
cal.factorial(5) = 120
chap07.RecCalculator
결과 타입을 보면 cal.getClass().getName()이 RecCalculator 타입임을 알 수 있습니다.
ProceedingJoinPoint의 메서드
Around Advice에서 사용할 공통 기능 메서드는 대부분 파라미터로 전달받은 ProceedingJoinPoint의 proceed() 메서드만 호출하면 됩니다. ExeTimeAspect 클래스도 proceed() 메서드를 호출하였습니다.
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 mesaure(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);
}
}
}
그리고, 객체에 대한 정보, 실행되는 메서드에 대한 정보, 메서드를 호출할 때 전달된 인자에 대한 정보가 필요할 때가 있습니다. 이와 같은 정보에 접근할 수 있도록 ProceedingJoinPoint 인터페이스는 아래와 같은 메서드를 제공합니다.
- Signature getSignature() : 호출되는 메서드에 대한 정보를 구한다.
- Object getTarget() : 대상객체를 구한다.
- Object[] getArgs() : 파라미터 목록을 구한다.
그렇다면 Signature 인터페이스는 어떤 메서드를 제공할까요?
- String getName() : 호출되는 메서드의 이름을 구한다.
- String toLongString() : 호출되는 메서드를 완전하게 표현한 문장을 구한다(메서드의 리턴 타입, 파라미터 타입이 모두 표시된다.)
- String toShortString() : 호출되는 메서드를 축약해서 표현한 문장을 구한다.(기본 구현은 메서드의 이름만을 구한다.)
이번 포스팅에서는 @Aspect, @Pointcut, @Around를 이용해서 AOP를 구현해 보고, 스프링 설정 클래스에 @Aspect가 적용된 클래스를 빈으로 등록하고 , 프록시 객체를 이용해 @Apspect가 적용된 클래스를 찾아, 공통 기능을 담당하는 RecCalculator의 factorial 메서드를 호출하는 과정을 살펴보았습니다.
저 역시 너무 어려운 개념이라 한 번만 봐서는 이해가 잘 되지 않는 것 같습니다. 다른 자료를 찾아보고 반복학습하는 과정이 필요한 것 같습니다.
'Spring' 카테고리의 다른 글
[Spring] AOP - execution 명시자 표현식 (0) | 2024.02.05 |
---|---|
[Spring] AOP - 프록시 생성방식 (0) | 2024.02.05 |
[Spring] Spring AOP (0) | 2024.01.31 |
[Spring] AOP 프로그래밍 - 프록시(Proxy) 객체 (0) | 2024.01.30 |
[Spring] 빈 객체의 생성과 관리범위 (0) | 2024.01.29 |