Spring AOP (4)

AspectJ란?

AspectJ는 PARC에서 개발한 자바 프로그래밍 언어용 관점 지향 프로그래밍 (AOP) 확장 기능이다. 이클립스 재단 오픈 소스 프로젝트에서 독립형 또는 이클립스로 통합하여 이용 가능하다. AspectJ는 최종 사용자를 위한 단순함과 이용성을 강조함으로써 폭넓게 사용되는 AOP에 대한 디 팩터 표준(사실 상 표준)이 되었다.

AspectJ란 순수 Spring AOP API에서 제공하지 않는 필드에 대한 Advisor를 지원하고, CTW, LTW과 같은 다양한 위빙 방법을이용할 수 있는 기능을 제공하여 프로그램의 퍼포먼스를 향상 시킬 수 있도록 해준다.
또한 @Aspect 어노테이션을 바탕으로 Aspect 로직을 작성 할 수 있어 기존의 xml방식보다는 더 편리하다.
현재는 Spring AOP + AspectJ는 거의 표준이라고 할 정도로 많이 사용이 되고 있는 추세다. AspectJ는 스프링 뿐만 아니라 AOP를 지원하지 않는 프레임 워크에서도 AOP를 지원할 수 있도록 도움을 주는 API이다.

Aspect란?

Aspect란 객체지향 언어의 클래스와 비슷한 개념이라고 생각하면 이해하기 쉽다.
그 자체로 애플리케이션의 도메인 로직을 담은 핵심기능은 아니지만, 많은 오브젝트에 걸쳐서 필요한 부가기능을 추상화 해놓은 것이다.
구조적으로 보자면 Aspect = PointCut + Advisor이다.

위빙 Weaving

위빙(Weaving) 이란? Aspect 클래스에 정의 한 Advice 로직을 타깃(Target)에 적용하는 것을 의미한다.
위빙 방법으로는 RTW, CTW, LTW 3가지가 있다.

런타임 시, 위빙 (RTW: Runtime Weaving)

Spring AOP에서 사용하는 위빙 방식이다. Proxy를 생성하여 실제 타깃(Target) 오브젝트의 변형없이 위빙을 수행한다.
실제 런타임 상, Method 호출 시에 위빙이 이루어 지는 방식이다.
소스파일, 클래스 파일에 대한 변형이 없다는 장점이 있지만, 포인트 컷에 대한 어드바이스 적용 갯수가 늘어 날수록 성능이 떨어진다는 단점이 있다. 또한 메소드 호출에 대해서만 어드바이스를 적용 할 수 있다.

Spring aop에 대한 이미지 검색결과

컴파일 시, 위빙 (CTW: Compile time Weaving)

AspectJ에는 AJC (AspectJ Compiler)라는 컴파일러가 있는데 Java Compiler를 확장한 형태의 컴파일러이다.
AJC를 통해 java파일을 컴파일 하며, 컴파일 과정에서 바이트 코드 조작을 통해 Advisor 코드를 직접 삽입하여 위빙을 수행한다.
장점으로는 3가지 위빙 중에서는 가장 빠른 퍼포먼스를 보여준다. (JVM 상에 올라갈 때 메소드 내에 이미 advise 코드가 삽입 되어있기 때문) 하지만 컴파일 과정에서 lombok과 같이 컴파일 과정에서 코드를 조작하는 플러그인과 충돌이 발생할 가능성이 아주 높다. (거의 같이 사용 불가)

CTW를 사용하기 위한 메이븐 설정

라이브러리 추가: (pom.xml)

1
2
3
4
5
6
7
8
9
10
11
12
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.8.6</version>
<scope>runtime</scope>
</dependency>

<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjtools</artifactId>
<version>1.8.6</version>
</dependency>

플러그인 설정: (pom.xml)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>aspectj-maven-plugin</artifactId>
<version>1.7</version>
<configuration>
<showWeaveInfo>true</showWeaveInfo>
<source>${aspectj-compiler.version}</source>
<target>${aspectj-compiler.version}</target>
<verbose>true</verbose>
<complianceLevel>${aspectj-compiler.version}</complianceLevel>
<outxml>true</outxml>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>test-compile</goal>
</goals>
</execution>
</executions>
</plugin>

(*IntelliJ를 사용하고 있다면 설정에서 컴파일러를 AJC 로 변경하여 손쉽게 CTW를 사용할 수 있다.)

위의 플러그인 설정은 mojohaus에 제공하는 aspects-maven-plugin을 사용

AspectJ Compiler(AJC)를 사용하여 AspectJ Aspect를 클래스에 적용

클래스 로드 시, 위빙 (LTW: Load time Weaving)

ClassLoader를 이용하여 클래스가 JVM에 로드 될 때 바이트 코드 조작을 통해 위빙이 되는 방식
RTW처럼 소스파일과 클래스 파일에 조작을 가하지 않아 컴파일 시간은 상대적으로 CTW보다 짧다. 하지만 오브젝트가 메모리에 올라가는 과정에서 위빙이 일어나기 때문에 런타임 시, 시간은 CTW보다 상대적으로 느리다.
Application Context에 객체가 로드될 때, aspectj weaver와 spring-instrument에 의한 객체 handling이 발생하기 때문에 performance가 저하된다.

그리고 설정이 가장 복잡하지 않은가 싶다..

아래 내용들은 LTW를 사용하기 위해 해줘야 하는 설정들이다.

1
<context:load-time-weaver/> <!-- 스프링 설정 파일에 추가 servlet.xml -->
1
@EnableLoadTimeWeaving //Java Configuration에서 해당 LTW기능을 사용하도록 설정해준다.
1
2
3
//IDE에서 개발 시, 해당 설정을 추가
-javaagent:/fullpath/aspectjweaver-1.8.1.jar
-javaagent:/fullpath/spring-instrument-4.0.6.RELEASE.jar
1
2
//톰캣 시작 시, jvmargs 옵션에 추가
-javaagent:/weavers/spring-instrument-4.0.6.RELEASE.jar

Annotations

JoinPoint 관련 Annotations

공통적으로 pointcut에 대한 표현식을 인자로 받는다.

  • @Before
    • 메소드 실행 이전 부분에 대한 Joinpoint 설정
  • @Around
    • ProceedingJoinPoint라는 파라미터를 이용하여 Target메소드 실행을 직접 제어 할 수 있다.
    • 메소드 실행 시점을 기준으로 AOP로직을 구현할 수 있어 가장 강력하게 사용이 가능하다.
  • @After
    • 메소드 실행 이후 부분의 대한 Joinpoint 설정
  • @AfterReturning
    • 메소드 실행 이후 return 실행 이후 부분에 대한 Joinpoint 설정
  • @AfterThrowable
    • 메소드 실행 시, Throw 이후 부분에 대한 Joinpoint 설정

Pointcut 관련 Annotation

  • @Pointcut
    • Pointcut에 대한 표현식을 값으로 가짐
    • @Pointcut이 적용된 메소드는 무조건 리턴타입이 void

Point Cut

포인트 컷이란? 메소드의 Joinpoint에 대한 Advice가 언제 실행될 지를 지정하는데 사용한다.
여러 메소드의 Joinpoint시점 여러개를 묶어 하나의 pointcut으로 만들 수 있다.

pointcut 개념

포인트 컷 표현식

Expression을 이용한 pointcut 표현식

pointcut 표현식에 대한 이미지 검색결과

  1. 접근 제어자 패턴 public이나 protected (private는 사용하지 말자 proxy구현 불가)

    • * 인 경우 모든 접근제어자에 대해 설정 가능
    • 생략 시, * 과 같은 효과
  2. 리턴 타입 : 메소드의 리턴 타입을 지정한다. (해당 타입으로 리턴되는 모든 메소드에 대한 Pointcut)

    • 필수 적으로 기재해야 한다.
    • * 을 통해 모든 리턴 타입에 대해 Pointcut을 설정 할 수 있다.
  3. Class 타입 : 클래스 타입 패턴 (해당 클래스에 대한 모든 메소드에 대해 Pointcut)

    • 단, 패키지 명도 기재해야 함
    • com.springframework.aop.. 처럼 ..으로 붙이는 경우 하위 패키지에 대한 모든 클래스의 모든 메소드에 대해 Pointcut을 처리
    • 필수 적으로 기재해야 한다.
  4. 메소드 명 : 특정 클래스 또는 패키지 하위의 메소드 명에 대해 Pointcut 설정

    • 필수 적으로 기재해야 한다.
    • 모든 메소드에 적용 하려면 * 을 사용
  5. 파라미터 타입 : 메소드의 파라미터 타입에 따른 Pointcut 설정

    • 필수 적으로 기재해야 한다.
    • ,로 구분하여 순서대로 파라미터를 기재한다.
    • 타입과 갯수에 상관없다면 .. 또는 * 를 기재한다.
  6. 예외 타입 : 예외 클래스에 대해 Joinpoint를 설정 할 수 있다. (생략가능)

예시

  • execution(int minus(int, int)): int 타입의 리턴 값, minus 라는 메소드 이름, 두 개의 int 파라미터를 가지는 메소드

  • execution(* minus(int, int)): 리턴 타입은 상관없이, minus 라는 메소드 이름, 두 개의 int 파라미터를 가지는 메소드

  • execution(* minus(..)): 리턴 타입과 파라미터의 종류 및 개수에 상관없이 minus 이름을 가진 메소드

  • **execution(* *(..))**: 리턴 타입, 파라미터, 메소드 이름에 상관없는 모든 메소드

  • **execution(* *())**: 리턴 타입, 메소드 이름에 상관없고 파라미터는 없는 모든 메소드

  • execution(* springbook.aop.Target.*(..)): springbook.aop.Target 클래스의 모든 메소드

  • execution(* springbook.aop..(..)): springbook.aop 패키지의 모든 메소드, 단 서브패키지의 클래스는 포함 안된다.

  • execution(* springbook.aop...(..)): springbook.aop 패키지의 모든 메소드, 서브패키지의 클래스까지 포함

  • **execution(* ..Target.(..))**: 패키지에는 상관없이 Target 클래스의 모든 메소드

  • *execution( *(..) throws Exception) **: Exception을 throw하는 모든 메소드

  • within(com.springbook.aop.service.*) : service 패키지 내의 모든 Joinpoint

  • within(com.springbook.aop.service..*) : service 패키지의 하위패키지 내의 모든 Joinpoint

  • this(com.springbook.aop.service.UserService) : UserService 인터페이스를 구현하는 프록시 객체의 모든 Joinpoint

  • target(com.springbook.aop.service.UserService) : UserService 인터페이스를 구현하는 대상 객체의 모든 Joinpoint

  • args(java.utils.List) : 파라미터가 1개이고 List타입인 모든 Joinpoint

  • @target(org.springframework.transaction.annotation.Transactional) :대상 객체가 @Transactional 어노테이션을 갖는 모든 결합점

  • @within(org.springframework.transaction.annotation.Transactional) : 대상 객체의 선언 타입이 @Transactional 어노테이션을 갖는 모든 결합점

  • @annotation(org.springframework.transaction.annotation.Transactional) : 실행 메소드가 @Transactional 어노테이션을 갖는 모든 결합점

  • @args(com.xyz.security.Classified) : 단일 파라미터를 받고, 전달된 인자 타입이 @Classified 어노테이션을 갖는 모든 결합점

  • bean(UserRepository) : “UserRepository” 빈

  • !bean(UserRepository) : “UserRepository” 빈을 제외한 모든 빈

  • bean(*) : 모든 빈

  • bean(user)* : 이름이 ‘user’로 시작되는 모든 빈

  • *bean(Repository) : 이름이 “Repository”로 끝나는 모든 빈

  • bean(user/*) : 이름이 “user/“로 시작하는 모든 빈

  • **bean(dataSource) || bean(DataSource) : 이름이 “dataSource” 나 “DataSource” 으로 끝나는 모든 빈

예시코드

Aspect 파일 사용

1
2
3
4
5
6
7
8
9
10
11
12
13
public aspect TestAdvisor {
pointcut advicePoint() : execution(* com.example.advice..*(..));

after(Joinpoint joinpoint) : advicePoint() {
System.out.println("처음으로 실행되는 로그");
}

after(Joinpoint joinpoint) : advicePoint() {
System.out.println("두번째로 실행되는 로그");
}

//order는 어떻게 주는건지 모르겠다.
}

@Aspect 어노테이션 사용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Aspect
public class TestAdvisor {

/**
* advice패키지 하위의 모든 클래스의 모든 메소드에 대한 pointcut
*/
@Pointcut("execution(* com.example.advice..*(..))")
public void pointcut(){

}

@order(2) //order가 높은 순으로 먼저 실행
@After(pointcut="pointcut()")
public void afterAdvice(Joinpoint joinpoint) {
System.out.println("처음으로 실행되는 로그");
}

@order(1)
@After(pointcut="pointcut()")
public void showLog(Joinpoint joinpoint) {
System.out.println("두번째로 실행되는 로그");
}
}

참고

http://blog.naver.com/PostView.nhn?blogId=tmondev&logNo=220564638014

http://netframework.tistory.com/entry/LTW-CTW%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-Transactional%EC%9D%98-%EC%82%AC%EC%9A%A9

https://steemit.com/kr-dev/@nhj12311/aop-aspectj-java-aop-5