이전 게시글을 통해 AOP가 무엇인지, 자바를 활용해 AOP를 구현하는 여러 방법이 무엇이 있는지 알아보았다. 이번 시간에는 AspectJ를 실습해보며 이해해보려고 한다. [AOP 게시글 확인]
AspectJ란 AOP를 자바에서 사용하기 위한 구현체이며 완전한 AOP 솔루션을 제공하는 것을 목표로 한다. 여러 구현체가 있는데 사실상 자바 표준이라고 한다.
Dynamic Proxy
나 CGLib
처럼 런타임 위빙 방식의 프록시 패턴으로 동작해 간접적인 방법으로 AOP를 구현하는 것이 아니라 Compile Time Weaving
, Load Time Weaving
, Post-compile weaving
(컴파일 후 위빙)을 지원하여 컴파일 시점이나 클래스파일이 로드되는 시점에 바이트코드를 조작하여 위빙을 해버린다.
Spring의 AOP
도 포인트컷 표현식 사용시 AspectJ의 AspectHExpressionPointcut를 차용해서 사용할만큼 매우성숙하고 발전한 AOP 기술이다.
포인트컷에 정의된 지점에서 실행되는 작업을 Advice라고 한다.
클래스의 인스턴스 생성 시점', '메소드 호출 시점(앞, 뒤 등)', '예외 발생 시점'과 같이 Aspect를 적용할 수 있는 특정 시점을 조인포인트라고 한다.
조인 포인트 중에서 특정 조건을 만족하는 지점을 지정하는 표현식이다.
여러 객체에 공통으로 적용되는 공통 관심 사항을 Aspect라고 한다.
우리가 위빙할 대상을 Target이라고 한다.
스프링을 사용하지 않고 자바에서 AspectJ만 실습해보자
AspectJ는 AspectJ 코드 기반 스타일과 어노테이션 기반으로 AOP구현이 가능하다.
이번 실습에는 AspectJ5부터 제공하는 어노테이션 기반 AspectJ를 사용하겠다.
AspectJ 런타임 의존성 외에도 로드 시간에 Java 클래스에 대한 advice를 제공하기 위해 aspectjweaver.jar
도 포함해야한다.
<!-- https://mvnrepository.com/artifact/org.aspectj/aspectjrt -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.9.7</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.7</version>
</dependency>
AspectJ는 컴파일 타임, 로드 타임, 포스트 컴파일 타임의 위빙 방식을 지원한다.
이 중 로드 타임 위빙 방식을 사용하겠다. AspectJ Compiler는 컴파일 타임 시에만 사용하므로 로드 타임 방식에서는 쓰이지 않는다.
<build>
<plugins>
<!-- aspectj Load-Time Weaving 활성화를 위한 플러그인 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version>
<configuration>
<argLine>
<!-- aspectj version 명시 -->
-javaagent:"${settings.localRepository}"/org/aspectj/aspectjweaver/${aspectj.version}/aspectjweaver-${aspectj.version}.jar
</argLine>
<useSystemClassLoader>true</useSystemClassLoader>
<forkMode>always</forkMode>
</configuration>
</plugin>
</plugins>
</build>
resources/META-INF/aop.xml
파일을 생성하고 다음과 같은 내용을 작성한다.
aspect 들이 위치한 디렉토리와, 주입 대상 타켓들이 위치한 디렉토리를 include 한다.
<!-- AspectJ에서 load-time weaving 사용 시 설정 -->
<!-- aspect 들이 위치한 디렉토리와, 주입 대상 타켓들이 위치한 디렉토리를 include 한다 -->
<aspectj>
<aspects>
<aspect name="aspectj.aspect.LogAspect"/> <!-- LogAspect -->
<weaver options="-verbose -showWeaveInfo">
<include within="aspectj.aspect.*"/>
<include within="aspectj.targets.*"/>
</weaver>
</aspects>
</aspectj>
@PrintLog
어노테이션을 메서드 위에 선언 시 메서드 호출 앞, 뒤로 Log를 출력해주도록 AOP를 구현할 예정이다. 이를 위해 @PrintLog
어노테이션을 작성해준다.
옵션은 printBefore, printAfter, printAround 을 생성할 것이며 true, false 지정이 가능하도록 한다.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface PrintLog {
public boolean printAround() default true;
public boolean printBefore() default true;
public boolean printAfter() default true;
}
앞서 작성한 @PrintLog
어노테이션을 사용해 Rabbit클래스를 작성한다.
eat()
호출 시 AspectJ를 통해 around, before, after 모두 출력하게끔 하고 drink()
호출 시 before, after 로그만 출력하게끔 할 예정이다.
public class Rabbit {
@PrintLog
public void eat() {
// around, before, after 로그 출력
System.out.println("토끼가 음식을 먹습니다.");
}
@PrintLog(printAround = false)
public void drink() {
// before, after 로그 출력
System.out.println("토끼가 물을 마십니다.");
}
}
어노테이션의 파라미터를 받아와 해당 값에 따라 로그를 출력하거나 출력하지 않도록 Aspect를 작성하였다.
import aspectj.annotations.PrintLog;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
@Aspect //aspect 임을 알림
public class LogAspect {
@Pointcut("@annotation(printLog)")
public void callAt(PrintLog printLog) {
}
@Around("callAt(printLog)")
public Object aroundLog(ProceedingJoinPoint pjp, PrintLog printLog) throws Throwable {
if(!printLog.printAround()) return pjp.proceed();
String name = pjp.getSignature().toShortString();
System.out.println("** " + name + " around log 실행**");
Object result = pjp.proceed();
System.out.println("** " + name + " around log 끝**");
return result;
}
@Before("callAt(printLog)")
public void beforeLog(PrintLog printLog) {
if(printLog.printBefore()) {
System.out.println("-- before log --");
}
}
@After("callAt(printLog)")
public void afterLog(PrintLog printLog) {
if(printLog.printAfter()) {
System.out.println("-- after log --");
}
}
}
package aspectj;
import aspectj.targets.Rabbit;
import org.junit.Test;
public class AspectJTests {
@Test
public void aspectjAnnotationBaseExample() {
Rabbit rabbit = new Rabbit();
rabbit.drink();
System.out.println();
rabbit.eat();
}
}
로드 타임 방식이라 로드 타임에 위빙 되는 것을 확인할 수 있다.
[AppClassLoader@73d16e93] info AspectJ Weaver Version 1.9.7 built on Thursday Jun 24, 2021 at 16:14:45 PDT
[AppClassLoader@73d16e93] info register classloader jdk.internal.loader.ClassLoaders$AppClassLoader@73d16e93
[AppClassLoader@73d16e93] info using configuration /Users/suhongkim/projects/java-aop/target/classes/META-INF/aop.xml
[AppClassLoader@73d16e93] info register aspect aspectj.aspect.LogAspect
[AppClassLoader@73d16e93] weaveinfo Join point 'method-execution(void aspectj.targets.Rabbit.eat())' in Type 'aspectj.targets.Rabbit' (Rabbit.java:9) advised by before advice from 'aspectj.aspect.LogAspect' (LogAspect.java)
[AppClassLoader@73d16e93] weaveinfo Join point 'method-execution(void aspectj.targets.Rabbit.eat())' in Type 'aspectj.targets.Rabbit' (Rabbit.java:9) advised by around advice from 'aspectj.aspect.LogAspect' (LogAspect.java)
[AppClassLoader@73d16e93] weaveinfo Join point 'method-execution(void aspectj.targets.Rabbit.eat())' in Type 'aspectj.targets.Rabbit' (Rabbit.java:9) advised by after advice from 'aspectj.aspect.LogAspect' (LogAspect.java)
[AppClassLoader@73d16e93] weaveinfo Join point 'method-execution(void aspectj.targets.Rabbit.drink())' in Type 'aspectj.targets.Rabbit' (Rabbit.java:15) advised by before advice from 'aspectj.aspect.LogAspect' (LogAspect.java)
[AppClassLoader@73d16e93] weaveinfo Join point 'method-execution(void aspectj.targets.Rabbit.drink())' in Type 'aspectj.targets.Rabbit' (Rabbit.java:15) advised by around advice from 'aspectj.aspect.LogAspect' (LogAspect.java)
[AppClassLoader@73d16e93] weaveinfo Join point 'method-execution(void aspectj.targets.Rabbit.drink())' in Type 'aspectj.targets.Rabbit' (Rabbit.java:15) advised by after advice from 'aspectj.aspect.LogAspect' (LogAspect.java)
-- before log --
토끼가 물을 마십니다.
-- after log --
** Rabbit.eat() around log 실행**
-- before log --
토끼가 음식을 먹습니다.
** Rabbit.eat() around log 끝**
-- after log --
Process finished with exit code 0
AspectJ는 스프링에 종속된 기능이 아님을 알게 되었고 AspectJ의 동작 방식에 대해 이해하게 되었다.
참고로 스프링에서 AspectJ
를 사용할 경우 실제 @AspectJ
문법과 애스펙트 정의 방법을 차용했을 뿐, AspectJ AOP를 사용하는 것은 아니다. 런타임 위빙 방식으로 프록시 패턴으로 동작한다.
https://hwannny.tistory.com/98
https://www.baeldung.com/aspectj
spring AOP와 AspectJ의 차이
https://kils-log-of-develop.tistory.com/638