Intro To AspectJ #3

ssongkim·2022년 1월 27일
1
post-thumbnail

Introduction

이전 게시글을 통해 AOP가 무엇인지, 자바를 활용해 AOP를 구현하는 여러 방법이 무엇이 있는지 알아보았다. 이번 시간에는 AspectJ를 실습해보며 이해해보려고 한다. [AOP 게시글 확인]

Overview

AspectJ란 AOP를 자바에서 사용하기 위한 구현체이며 완전한 AOP 솔루션을 제공하는 것을 목표로 한다. 여러 구현체가 있는데 사실상 자바 표준이라고 한다.

Dynamic ProxyCGLib처럼 런타임 위빙 방식의 프록시 패턴으로 동작해 간접적인 방법으로 AOP를 구현하는 것이 아니라 Compile Time Weaving, Load Time Weaving, Post-compile weaving(컴파일 후 위빙)을 지원하여 컴파일 시점이나 클래스파일이 로드되는 시점에 바이트코드를 조작하여 위빙을 해버린다.

Spring의 AOP도 포인트컷 표현식 사용시 AspectJ의 AspectHExpressionPointcut를 차용해서 사용할만큼 매우성숙하고 발전한 AOP 기술이다.

AOP 용어

Advice

조인 포인트에 삽입되어져 실질적으로 동작하는 코드를 Advice라고 한다.

JoinPoint

클래스의 인스턴스 생성 시점', '메소드 호출 시점(앞, 뒤 등)', '예외 발생 시점'과 같이 특정 시점을 조인포인트라고 한다.

  • 1) @Around
  • 2) @Before
  • 3) @After
  • 4) @AfterThrowing
  • 5) @AfterReturning

Pointcut

JoinPoint를 적용할 영역을 지정한다.

Aspect

여러 객체에 공통으로 적용되는 공통 관심 사항을 Aspect라고 한다.

Target

우리가 위빙할 대상을 Target이라고 한다.

실습해보기

스프링을 사용하지 않고 자바에서 AspectJ만 실습해보자
AspectJ는 AspectJ 코드 기반 스타일과 어노테이션 기반으로 AOP구현이 가능하다.
이번 실습에는 AspectJ5부터 제공하는 어노테이션 기반 AspectJ를 사용하겠다.

1. 의존성 추가

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>

2. 사용할 위빙 방식 설정

AspectJ는 컴파일 타임, 로드 타임, 포스트 컴파일 타임의 위빙 방식을 지원한다.
이 중 로드 타임 위빙 방식을 사용하겠다. AspectJ Compiler는 컴파일 타임 시에만 사용하므로 로드 타임 방식에서는 쓰이지 않는다.

2-1 Load-Time Weaving 설정

1) 플러그인 추가

<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>

2) Load-Time Weaving 활성화

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>

3. 어노테이션 생성

@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;
}

4. Rabbit 클래스 작성

앞서 작성한 @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("토끼가 물을 마십니다.");
    }
}

5. annotation-style AspectJ 작성

어노테이션의 파라미터를 받아와 해당 값에 따라 로그를 출력하거나 출력하지 않도록 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 --");
        }
    }
}

6. 예제 테스트 코드 작성

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();
    }
}

7. 예제 결과

로드 타임 방식이라 로드 타임에 위빙 되는 것을 확인할 수 있다.

[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

profile
鈍筆勝聰✍️

0개의 댓글