개발관련/삽질

AOP(SpringAOP/AspectJ)

동팡 2019. 12. 2. 23:58

목차

AOP 개념

AOP 종류

AOP 용어

AOP Weaving

Spring AOP vs AspectJ

Do it AspectJ

버전 이슈사항

참고문헌

AOP 개념

AOP는 Aspect Oriented Programing의 약자로, 관점 지향 프로그래밍을 뜻한다. 공통 관심사(Aspect)의 분리(모듈화)를 통해 소스코드의 중복의 해소 및 어플리케이션의 책임 원칙을 좀덕 획일화한다. 하나의 예는 트랜잭션 잡업과 인증 절차를 예를들 수 있다. 즉, 기능 중에 주 기능에만 집중하고, 부가 기능은 Aspect로 다 분리 시킨다. 

 

AOP 전체 설명도

용어는 아래 용어 항목에서 참고한다.

Cross-Cutting Concern은 위 개념에서 얘기한 부가 기능을 뜻하며, Primary Concern은 주기능을 뜻한다. Existing App과 같이 구성된 것을 NewApp으로 변화 시키고, 개발자는 주기능과 부가기능을 모듈화 개발할 수 있는 행위를 AOP이다. 

대략적인 설명은 다음과 같다.

Cross-Cutting Concern, 즉 Aspect들을 Point-Cut을 통해 대상을 적용할 대상(Target)을 지정한 후, Weaving을 시켜 New App을 생성한다(이 문구는 아래의 글을 다 읽은 후, 다시 읽어보자).

AOP 종류

일단 Java 기준, AOP는 대표적으로 SpringAOP와 AspectJ 가 대표적이다. 

Spring AOP

IoC/DI/프록시 패턴/자동 프록시 생성 기법/빈 오브젝트의 후처리 조작기법 등을 통해 AOP를 지원하며, 이와 같은 AOP는 스프링과 기본 JDK를 통해 구현 가능하다. 즉 간접적인 방식을 통해 AOP를 지원한다. 해당 Spring AOP는 프록시 방식의 AOP이다. 즉 프록시 디자인 패턴을 사용한다. 

스프링 AOP의 프록시 빈 생성 절차는 다음과 같다. 

 1. 빈 설정파일을 통한 빈 오브젝트 생성

 2. (생성된 빈을 이용하여) 빈 후처리기를 통해 컨테이너 초기화 시점에서 자동으로 프록시 생성(DefaultAdvisorAutoProxyCreator.class/BeanPostProcessor.class), 

 3. 포인트컷을 활용하여, 프록시를 적용할 대상을 지정한다.

 4. 프록시 빈 오브젝트 생성 및 IoC 컨테이너에 등록

AspectJ

바이트코드 생성과 조작을 통해 AOP를 실현한다. Target 오브젝트를 뜯어 고쳐서 부가기능을 직접 넣어주는 직접적인 방식이다. 

AOP 용어

 

Target: 부가기능을 부여할 대상을 뜻한다. 즉, 주기능을 얘기한다.

Aspect: AOP의 기본 모듈이다. 한개 또는 그 이상의 포인트컷과 어드바이스의 조합으로 만들어진다.

Advice: 실질적으로 부가 기능의 구현체이다. Advice의 유형은 다음과 같다. Before, After, Around, AfterReturning, AfterThrowing이 있다. 

 - Before: Target을 실행하기전에 부가 기능 실행

 - After: Target실행 후 (해당 Target Exception 또는 정상리턴 여부 상관없이) 실행

 - Around: Before + AfterReturning

 - AfterReturning: Target 실행 후 성공적인 리턴할 때

 - AfterThrowing: Target 실행하다, Exception 던질 때

Join-Point: Advice가 적용될 위치를 표시한다(ex:메소드 실행 단계).

Point-Cut: Advice를 적용할 Target를 선별하는 역할을 한다. Annotation 또는 메소드의 정규식을 통해 표현한다.

Cross-Cutting Concertn: 횡단 공통 관심사이다. 이것은 이미지를 통해 연상하는 것이 더 빠르다. 

 

Cross-Cutting Concerns 설명

AOP Weaving

Aspect(부가기능)와 Application(핵심기능)의 Linking을 하는 과정이다. 해당 객체들을 묶어 새로운 객체를 생성한다.

Spring AOP

이미, 위에서 Weaving 절차를 설명하였다. Spring AOP의 Weaving 절차는 RunTime이다(프록시 빈 생성하는 것, IoC 컨테이너 초기화 작업할 때, 그러니까 WAS 가동할 때). 

인터페이스 기준으로 하는 JDK Dynamic Proxy와 Class 기준으로 하는 CGLib Proxy가 존재한다. CGLib Proxy의 경우, AspectJ의 Weaving 처럼 바이트 코드를 조작한다. SpringAOP는 JDK Dynamic Proxy 패턴을 선호한다. 또한 Proxy 패턴 자체가 인터페이스를 끼고하는 페턴이다. 

AspectJ

실제, AJC(Apsect Compiler)를 이용하여 Woven System 생성한다. 즉 부가기능과 핵심기능이 합쳐진 클래스 파일을 생성한다. AspectJ의 Weaving 타입은 아래와 같다.

Compile-Time Weaving: Aspect의 클래스와 Aspect를 사용하는 class들을 AJC를 통해 컴파일을 한다. JAR를 이용하여 Weaving을 하는 경우, Post-Compile Weaving(Binary Weaving)을 사용하며, 일반 소스 코드의 경우, 일반 Compile-Time Weaving을 사용한다.

Load-Time Weaving: 클래스로더를 통해 클래스가 JVM에 로딩되는 시점에 클래스의 바이트 코드를 조작한다. 즉, 객체를 메모리에 적재할 때 Weaving을 실현한다. 때문에, 다른 Weaving보다 속도 측면에서는 느리다.

AspectJ Weaving

Spring AOP vs AspectJ

일단 AspectJ 승리

1. 속도: 일단 AspectJ가 8~35배 빠르다. SpringAOP는 Aspect당 기타 AOP 관련 메소드를 추가적으로 호출하기 때문에 바이트코드를 직접적으로 건드린 AspectJ에 비하면 느릴 수 밖에 없다.

2. AspectJ는 강력한 기능을 제공한다. 

SpringAOP vs AspectJ for JointPoint

3. 기능 차이점

SpringAOP vs AspectJ

1. Spring AOP는 퓨어 자바를 통해 구현 가능

2. AsepctJ는 Load-Time Weaving을 하지 않는한, AJC를 통한 컴파일 절차가 필요하다. 

3. Spring AOP는 method level weaving만 가능(근데 보편적으로 method level weaving만 사용....)

4. Spring AOP는 Spring 프레임워크에서 한정적이지만 AspectJ는 모든 JVM에서 가능하다.

5. Spring AOP는 execution 포인트컷만(위에서 설명한 메소드 정규식) 사용가능

6. AspectJ가 빠름

7. Spring AOP 복잡도 승리(프록시 패턴을 활용하여 복잡도가 AspectJ보다 훨씬 낮다.)

Do it AspectJ

AspectJ를 실제 구현해보자

pom.xml

	<dependency>
		<groupId>org.aspectj</groupId>
		<artifactId>aspectjrt</artifactId>
		<version>${org.aspectj-version}</version>
	</dependency>
	<dependency>
   		<groupId>org.aspectj</groupId>
   		<artifactId>aspectjweaver</artifactId>
   		<version>${org.aspectj-version}</version>
	</dependency>

NoNameAop.class 

설명

 - order를 통해 Transacional 보다 늦게 실행하게 된다, 해당 AOP가 실패하면 Transactional에서 rollback처리한다.

 - AsyncUriAnnotation Annotaion만 해당 Aspect를 구동한다.

 - args를 통해 Target의(메소드의) 인자 값을 파싱한다.

 - joinPoint.proceed()를 통해 그냥 Target을 실행할 수 있지만,

 - 부가기능 로직에서 파라메터를 수정했기 때문에 아래와 같이 new Object[]를 하여 전송한다.

 - 리턴 값이 없을경우 메소드를 void로 만들 수 있다. 또는 리턴 값이 있으면 그냥 리턴을 해주면 된다.

 - 그러나, 난 리턴값도 수정할 것이다.ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ 변경된 ret 값을 리턴한다. 

@Aspect
@Component
@Order(300)//tx:order:200
public class NoNameAop {
	
	private final AuthAsyncService authAsyncService;
	private final AuthCombiner authCombiner;
	private final AuthService authService;
	private static final Logger logger = Logger.getLogger( NoNameAop.class );
	
	@Autowired
	public UriAuthSupportAop( AuthAsyncService authAsyncService, AuthCombiner authCombiner, AuthService authService ) {
		this.authAsyncService = authAsyncService;
		this.authCombiner = authCombiner;
		this.authService = authService;
	}
	
	@Pointcut("@annotation(net.glaso.jwt.business.common.annotation.AsyncUriAnnotation) && " +
			"args(identifier, request)")
	private void asyncAuthPointCut( String identifier, HttpServletRequest request ){}

	@Around("asyncAuthPointCut(identifier, request)")
	public Object procAsyncToAuthUri( ProceedingJoinPoint joinPoint, String identifier, HttpServletRequest request ) throws Throwable {
		
        // .. Aspect 부가기능 로직
        
		// MAIN PROCESS
		Object ret = joinPoint.proceed( new Object[] { authCombiner.getUidIfIsNotUid( request, identifier ), request } );
		// MAIN PROCESS
		
		// .. Aspect 부가기능 로직
	
		return ret;
	}
}

PointCut Sample

1. net.glaso.jwt.business 하위 패키지 중 service 패키지의 post로 시작하는 메소드를 지정(전달인자는 아무거나 상관없고, 메소드의 리턴타입도 아무거나 상관없다.)

2. UserService클래스의 generate로 시작하는 메소드 지정

@Pointcut("execution(* net.glaso.jwt.business..service..post*(..))")
@Pointcut("execution(* net.glaso.jwt.business.user.service.UserService.generate*(..))")

버전 이슈사항

성공
Java 6 / Spring 4.1.7.RELEASE / AspectJ-1.6.10 / Tomcat 6.0.48 (테스트 완료)
Java 7 / Spring 4.1.7.RELEASE / AspectJ-1.6.10 / Tomcat 6.0.48 (테스트 완료)
Java 7 / Spring 4.1.7.RELEASE / AspectJ-1.7.4 / Tomcat 6.0.48 (테스트 완료)
Java 8 / Spring 4.1.7.RELEASE / AspectJ-1.7.4 / Tomcat 6.0.48 (테스트 완료)
Java 8 / Spring 4.1.7.RELEASE / AspectJ-1.8.9 / Tomcat 6.0.48 (테스트 완료)

 

실패
Java 8 / Spring 4.1.7.RELEASE / AspectJ-1.6.6 / Tomcat 6.0.48
- Exception Message: java.lang.CharSequence': Invalid byte tag in constant pool: 15
- 관련 자료
https://www.eclipse.org/aspectj/doc/released/README-180.html
https://stackoverflow.com/questions/23801950/spring-4-and-java-8-invalid-byte-tag-exception
- 원인: Java8을 지원하기 위해서는 AspectJ 1.8 이상을 사용해야한다(근데 위의 1.7.4 버전은 테스트 성공...).

Java 8 / Spring 4.1.7.RELEASE / AspectJ-1.8.10 / Tomcat 6.0.48
- Exception Message: java.lang.IllegalStateException: Expected raw type form of org.springframework.web.servlet.handler.AbstractHandlerMethodMapping$Match
- 관련 자료
https://jira.spring.io/si/jira.issueviews:issue-html/SPR-15019/SPR-15019.html
https://github.com/spring-projects/spring-framework/issues/19586
- 원인: AspectJ-1.8.9에서 1.8.10으로 업데이트를 진행 하면서, IllegalStateException을 회귀 하면서 생긴 Spring의 버그 해당 사항은 Spring 4.2.9, Spring 4.3.5, Spring 5.0M4에서 패치되었다. 1.8.9를 사용하거나 Spring을 패치한다.

참고문헌

https://docs.spring.io/spring/docs/2.5.x/reference/aop.html 
https://www.baeldung.com/spring-aop-pointcut-tutorial 
https://www.baeldung.com/aspectj 
https://www.baeldung.com/spring-aop 
https://www.baeldung.com/spring-aop-vs-aspectj 
https://howtodoinjava.com/spring-aop-tutorial/ 
https://web.archive.org/web/20150520175004/https://docs.codehaus.org/display/AW/AOP+Benchmark 
https://www.eclipse.org/aspectj/doc/released/devguide/ltw.html