2023. 12. 17. 18:26ㆍ카테고리 없음
과제
스프링에서 애너테이션을 이용해 AOP 하기
해결책
애스펙스를 정의하려면 일단 자바 클래스에 @Aspect를 붙이고 메서드별로 적절한 애너테이션을 붙여 어드바이스로 만든다.
어드바이스 애너테이션은 @Before, @After, @AfterReturning, @AfterThrowing, @Around 5개 중 하나를 쓸 수 있다.
IoC 컨테이너에서 애스펙스 애너테이션 기능을 활성화하려면 구성 클래스 중 하나에 @EnableAspectJAutoProxy를 붙인다. 기본적으로 스프링은 인터페이스 기반의 JDK 동적 프록시를 생성하여 AOP를 적용한다.
풀이
스프링에서는 AspectJ와 동일한 애너테이션으로 애너테이션 기반의 AOP를 구현한다. 포인트컷을 파싱, 매치하는 AspectJ 라이브러리를 그대로 빌려왔다. 하지만 AOP 런타임 자체는 순수 스프링 AOP이기 때문에 AspectJ 컴파일러나 위버와는 아무런 의존 관계가 없다.
예
public interface ArithmeticCalculator {
public double add(double a, double b);
public double sub(double a, double b);
public double mul(double a, double b);
public double div(double a, double b);
}
public interface UnitCalculator {
double kilogramToPound(double kilogram);
double kilometerToMile(double kilometer);
}
@Component("arithmeticCalculator")
public class ArithmeticCalculatorImpl implements ArithmeticCalculator{
@Override
public double add(double a, double b) {
double result = a + b;
System.out.println(a + " + " + b + " = " + result);
return result;
}
@Override
public double sub(double a, double b) {
double result = a - b;
System.out.println(a + " - " + b + " = " + result);
return result;
}
@Override
public double mul(double a, double b) {
double result = a * b;
System.out.println(a + " * " + b + " = " + result);
return result;
}
@Override
public double div(double a, double b) {
double result = a / b;
System.out.println(a + " / " + b + " = " + result);
return result;
}
}
@Component("unitCalculator")
public class UnitCalculatorImpl implements UnitCalculator{
@Override
public double kilogramToPound(double kilogram) {
double pound = kilogram * 2.2;
System.out.println(kilogram + " kilogram = " + pound + " pound");
return pound;
}
@Override
public double kilometerToMile(double kilometer) {
double mile = kilometer * 0.62;
System.out.println(kilometer + " kilometer = " + mile + " mile");
return 0;
}
}
각 구현체 모두 @Component를 붙여 빈 인스턴스를 생성한다.
애스펙트, 어드바이스, 포인트컷 선언하기
에스펙트는 여러 타입과 객체에 공통 관심사를 모듈화한 자바 클래스로, @Aspect를 붙여 표시한다. AOP에서 말하는 애스펙트란 어디에서(포인트컷) 무엇을 할 것인지(어드바이스)를 합쳐 놓은 개념이다.
어드바이스는 @Advice를 붙인 단순 자바 메서드로, AspectJ는 @Before, @After, @AfterReturning, @AfterThrowing, @Around 다섯개 어드바이스 애너테이션을 지원한다. 포인트컷은 어드바이스에 적용할 타입 및 객체를 찾는 표현식이다.
@Before 어드바이스
Before 어드바이스는 특정 프로그램 실행 지점 이전의 공통 관심사를 처리하는 메서드로, @Before를 붙이고 포인트컷 표현식을 애너테이션값으로 지정한다.
@Component
@Aspect
public class CalculatorLoggingAspect {
private Log log = LogFactory.getLog(this.getClass());
@Before("execution(* ArithmeticCalculator.add(..))")
public void logBefore() {
log.info("The method add() begins");
}
}
이 포인트컷 표현식은 ArithmeticCalculator 인터페이스의 add() 메서드 실행을 가리킨다.
앞부분의 와일드카드(*)는 모든 수정자(public, protected, private), 모든 반환형을 매치함을 의미한다. 인수 목록 부분에 쓴 두점(..)은 인수 개수는 몇 개라도 좋다는 뜻이다.
애스펙트 로직을 실행하려면 logback.xml 파일을 적절히 설정한다.
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<layout class="ch.qos.logback.classic.PatternLayout">
<Pattern>%d [%15.15t%] %-5p %30.30c - %m%n</Pattern>
</layout>
</appender>
<root level="INFO">
<appender-ref ref="STDOUT" />
</root>
</configuration>
다음 자바 구성 클래스에 @EnableAspectJAutoProxy를 붙여 POJO 계산기 구현체, 애스펙트 스캔하기
@Configuration
@EnableAspectJAutoProxy
@ComponentScan(basePackages = "com.spring.study.chapter02.aop")
public class CalculatorConfiguration {
}
테스트
public class AspectExample {
public static void main(String[] args) {
ApplicationContext context =
new AnnotationConfigApplicationContext(CalculatorConfiguration.class);
ArithmeticCalculator arithmeticCalculator =
context.getBean("arithmeticCalculator", ArithmeticCalculator.class);
arithmeticCalculator.add(1, 2);
UnitCalculator unitCalculator =
context.getBean("unitCalculator", UnitCalculator.class);
unitCalculator.kilogramToPound(1);
}
}
=>
생성
12월 17, 2023 5:43:14 오후 com.spring.study.chapter02.aop.calculation._interface.CalculatorLoggingAspect logBefore
정보: The method add() begins
1.0 + 2.0 = 3.0
1.0 kilogram = 2.2 pound
포인트컷으로 매치한 실행 지점을 조인포인트라고 한다. 포인트컷은 여러 조인포인트를 매치하기 위해 지정한 표현식이고 이렇게 매치된 조인포인트에서 해야 할 일이 바로 어드바이스다.
어드바이스가 현재 조인포인트의 세부적인 내용에 액세스 하려면 JoinPoint형 인수를 어드바이스 메서드에 선언해야 한다. 그러면 메서드명, 인수값 등 자세한 조인포인트 정보를 조회할 수 있다.
클래스명, 메서드명에 와일드카드를 써서 모든 메서드에 포인트컷을 적용해보자.
@Before("execution(* *.*(..))")
public void loggBefore(JoinPoint joinPoint) {
log.info("The method " + joinPoint.getSignature().getName()
+ "() begins with " + Arrays.toString(joinPoint.getArgs()))
}
@After 어드바이스
After 어드바이스는 조인포인트가 끝나면 실행되는 메서드로, @After를 붙여 표시한다.
조인포인트가 정상 실행되든, 도중에 예외가 발생하든 상관없이 실행된다.
@After("execution(* *.*(..))")
public void logAfter(JoinPoint joinPoint) {
log.info("The method " + joinPoint.getSignature().getName()
+ "() ends");
}
public static void main(String[] args) {
ApplicationContext context =
new AnnotationConfigApplicationContext(CalculatorConfiguration.class);
ArithmeticCalculator arithmeticCalculator =
context.getBean("arithmeticCalculator", ArithmeticCalculator.class);
arithmeticCalculator.add(1, 2);
UnitCalculator unitCalculator =
context.getBean("unitCalculator", UnitCalculator.class);
unitCalculator.kilogramToPound(1);
}
=>
생성
1.0 + 2.0 = 3.0
12월 17, 2023 5:59:38 오후 com.spring.study.chapter02.aop.calculation._aspect.log.CalculatorLoggingAspect logAfter
정보: The method add() ends
12월 17, 2023 5:59:38 오후 com.spring.study.chapter02.aop.calculation._aspect.log.CalculatorLoggingAspect logAfter
정보: The method kilogramToPound() ends
1.0 kilogram = 2.2 pound
@AfterReturning 어드바이스
After 어드바이스는 조인포인트 실행의 성공 여부와 상관없이 작동한다. logAfter() 메서드에서 조인포인트가 값을 반환할 경우에만 로깅하고 싶다면, @AfterReturning으로 대체하면 된다.
@AfterReturning(
pointcut = "execution(* *.*(..))",
returning = "result"
)
public void logAfterReturning(JoinPoint joinPoint, Object result) {
log.info("The method "+ joinPoint.getSignature().getName() +
"() ends with " + result);
}
After Returning 어드바이스로 조인포인트가 반환한 결괏값을 가져오려면 @AfterReturning의 returning 속성으로 지정한 변수명을 어드바이스 메서드의 인수로 지정한다.
스프링 AOP는 런타임에 조인포인트의 반환값을 이 인수에 넣어 전달한다.
public class AspectExample {
public static void main(String[] args) {
ApplicationContext context =
new AnnotationConfigApplicationContext(CalculatorConfiguration.class);
ArithmeticCalculator arithmeticCalculator =
context.getBean("arithmeticCalculator", ArithmeticCalculator.class);
arithmeticCalculator.add(1, 2);
UnitCalculator unitCalculator =
context.getBean("unitCalculator", UnitCalculator.class);
unitCalculator.kilogramToPound(1);
}
}
=>
생성
1.0 + 2.0 = 3.0
12월 17, 2023 6:07:54 오후 com.spring.study.chapter02.aop.calculation._aspect.log.CalculatorLoggingAspect logAfterReturning
정보: The method add() ends with 3.0
12월 17, 2023 6:07:55 오후 com.spring.study.chapter02.aop.calculation._aspect.log.CalculatorLoggingAspect logAfterReturning
정보: The method kilogramToPound() ends with 2.2
1.0 kilogram = 2.2 pound
@AfterThrowing 어드바이스
After Throwing 어드바이스는 조인포인트 실행 도중 예외가 날 경우에만 실행된다.
@AfterThrowing(
pointcut = "execution(* *.*(..))",
throwing = "res"
)
public void logAfterReturning(JoinPoint joinPoint, Throwable res) {
log.info("The method "+ joinPoint.getSignature().getName() +
"() ends with " + res);
}
작동 원리는 @AfterReturning과 같다. 발생한 예외는 @AfterThrowing의 throwing 속성에 담아 전달한다.
@Around어드바이스
마지막 Around는 가장 강력한 어드바이스이다. 이 어드바이스는 조인포인트를 완전히 장악하기 때문에 앞서 살펴본 어드바이스 모두 Around 어드바이스로 조합할 수 있다.
@Around("execution(* *.*(..))")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("the method() {}" + joinPoint.getSignature().getName()
+" begins with {}" + Arrays.toString(joinPoint.getArgs()));
try{
Object result = joinPoint.proceed();
log.info("the method : {}" + joinPoint.getSignature().getName()
+ "ends with : {}" + result);
return result;
} catch (IllegalArgumentException e) {
log.error("Illegal argument : {}" + Arrays.toString(joinPoint.getArgs())
+ "method with : {}" + joinPoint.getSignature().getName());
throw e;
}
}