AspectJ Weaver를 사용한 애노테이션 기반의 스프링 AOP 구현 방법

2021. 4. 22. 02:51 Spring Framework/Spring Core

AspectJ Weaver를 사용한 애노테이션 기반의 스프링 AOP 구현 방법

AOP란?

- Aspect Oriented Programming : 관점 지향 프로그래밍
- OOP 와 분리된 개념이 아니라, OOP에 기초를 두는 프로그래밍 방식
- 하나의 프로그램을 관점(혹은 관심사)라는 논리적인 단위로 분리하여 관리하는 개념
- 로깅, 감사, 선언적 트랜젝션, 보안, 캐싱 등 다양한 곳에서 사용된다.

AOP 용어

- Joint Point : 모듈이 삽입되어 동작하게 되는 특정 위치(메서드 호출 등)
- 
Point Cut : 다양한 Joint Point 중에 어떤 것을 사용할지 선택
- Advice : Joint Point에 삽입되어 동작할 수 있는 코드
- 
Weaving : Advice를 핵심 로직 코드에 적용하는 것
- Aspect : Point Cut + Advice

Spring AOP Advice 종류

- before : 메서드 호출 전에 동작하는 Advice
- 
after-returning : 예외 없이 호출된 메서드의 동작이 완료되면 동작하는 Advice
- 
after-throwing : 호출된 메서드 동작 중 예외가 발생했을 때 동작하는 Advice
- 
after : 예외 발생 여부에 관계없이 호출된 메서드의 동작이 완료되면 동작하는 Advice
- 
around : 메서드 호출 전과 후에 동작하는 Advice

 

Spring AOP 구현

AspectJ Weaver를 사용한 Spring AOP 구현 방법은 XML을 이용하는 것과 @AspectJ 애노테이션을 이용하는 것으로 나뉜다.

본 글에서는 애노테이션 기반의 구현 방법을 살펴본다.

 

관련 글 - AspectJ Weaver를 사용한 XML 기반의 스프링 AOP 구현 방법

 

1) 공통 사항

① AspectJ Weaver 라이브러리 추가

<!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver -->
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.5</version>
</dependency>

 

② 관심사로 설정할 코드 작성

@Component
public class TestBean {
	
	public int method1() {
		System.out.println("method1 호출");
		
		return 100;
	}
}

관심사로 설정할 코드를 작성한다.

TestBean 클래스의 method1() 메소드 호출을 관심사로 설정할 것이다.

다양한 케이스를 테스트하기 위해 int형 데이터를 리턴하도록 하였다.

 

public class MainClass {

	public static void main(String[] args) {
		AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(BeanConfigClass.class);
		
		TestBean bean1 = ctx.getBean(TestBean.class);
		
		int a1 = bean1.method1();
		System.out.printf("a1 : %d\n", a1);
		
		ctx.close();
	}
}

main 메소드에서 ApplicationContext 객체를 생성하고 TestBean 빈 객체를 가져온 뒤 TestBean 클래스의 method1() 메소드를 호출한다.

 

🖥 실행 결과

method1 호출
a1 : 100

Spring AOP를 사용해 method1()의 호출을 관심사로 설정하여 method1() 호출 전, 후에 특정 메소드를 호출할 수 있도록 구현할 것이다.

 

③ Advisor 클래스 생성

import org.aspectj.lang.annotation.Aspect;

@Aspect
@Component
public class AdvisorClass {

}

Advice들을 모아놓은 것을 Advisor라고 한다.

@Aspect 애노테이션을 붙여 advisor 클래스를 생성한다.

이 클래스에 method1()의 호출 전, 후에 호출할 메소드를 구현할 것이다.

 

2) 애노테이션을 이용한 구현 방법

스프링 설정 클래스

@Configuration
@ComponentScan(basePackages = {"com.atoz_develop.beans", "com.atoz_develop.advisor"})
@EnableAspectJAutoProxy
public class BeanConfigClass {

}

스프링 설정 클래스에 @EnableAspectJAutoProxy를 붙여준다.

@Aspect가 붙어있는 advisor 클래스의 애노테이션을 분석해서 AOP 셋팅을 하도록 하는 애노테이션이다.

스프링 XML 설정 사용 시에는 <aop:aspectj-autoproxy/>를 추가해주면 된다.

📝 AspectJ Weaver 애노테이션 AOP 설정하기
스프링 자바 설정 사용 시 : @EnableAspectJAutoProxy
스프링 XML 설정 사용 시 : <aop:aspectj-autoproxy/>

AOP를 애노테이션으로 설정하므로 스프링 설정 클래스에 해줄 것은 이게 끝이다.

AspectJ Weaver를 사용한 XML 기반의 AOP를 구현하는 방법과 달리 AOP 설정을 스프링 설정 파일에 작성하지 않기 때문이다.

이제 before, after, around, after-returning, after-throwing advice 메소드를 차례로 구현하고 테스트해보자.

 

① Before Advice - 메소드 호출 전에 동작

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@Aspect
@Component
public class AdvisorClass {
	
	// before advice
	@Before("execution(* method1())")
	public void beforeMethod() {
		System.out.println("beforeMethod 호출");
	}
}

advisor 클래스에서 @Before 애노테이션을 사용해 before advice 메소드를 구현한다.

@Before() 안에 execution 명시자를 사용해서 특정 관심사를 지정한다.

execution(* method1())과 같이 설정하면 모든 패키지, 모든 클래스의 method1() 메소드 호출이 관심사가 된다.

 

🔗 참고 - [AspectJ Weaver] execution 지시자 설정 방법 정리

 

🖥 실행 결과

beforeMethod 호출
method1 호출
a1 : 100

 

② After Advice - 예외 발생 여부에 관계없이 호출된 메소드의 동작이 완료되면 동작

import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@Aspect
@Component
public class AdvisorClass {

	// before advice
	@Before("execution(* method1())")
	public void beforeMethod() {
		System.out.println("beforeMethod 호출");
	}

	// after advice
	@After("execution(* method1())")
	public void afterMethod() {
		System.out.println("afterMethod 호출");
	}
}

advisor 클래스에서 @After 애노테이션을 사용해 after advice 메소드를 구현한다.

 

🖥 실행 결과

beforeMethod 호출
method1 호출
afterMethod 호출
a1 : 100

 

③ Around Advice - 메소드 호출 전과 후에 동작

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@Aspect
@Component
public class AdvisorClass {

	// before advice
	@Before("execution(* method1())")
	public void beforeMethod() {
		System.out.println("beforeMethod 호출");
	}

	// after advice
	@After("execution(* method1())")
	public void afterMethod() {
		System.out.println("afterMethod 호출");
	}

	// around advice
	@Around("execution(* method1())")
	public Object aroundMethod(ProceedingJoinPoint pjp) throws Throwable{
		System.out.println("aroundMethod 호출 1");
		Object result = pjp.proceed();
		System.out.println("aroundMethod 호출 2");
		return result;
	}
}

advisor 클래스에서 @Around 애노테이션을 사용해 around advice 메소드를 구현한다.

이 때는 org.aspectj.lang.ProceedingJoinPoint를 메소드의 인자로 받아서 이 객체의 proceed()를 호출하여 작업 중간에 원래 메소드를 호출하는 방식으로 구현한다.

원래 메소드가 리턴값이 있을 경우 Object 타입으로 받아서 반환한다.

 

🖥 실행 결과

aroundMethod 호출 1
beforeMethod 호출
method1 호출
aroundMethod 호출 2
afterMethod 호출
a1 : 100

 

④ After-Returning Advice - 예외 없이 호출된 메소드의 동작이 완료되면 동작

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@Aspect
@Component
public class AdvisorClass {

	// before advice
	@Before("execution(* method1())")
	public void beforeMethod() {
		System.out.println("beforeMethod 호출");
	}

	// after advice
	@After("execution(* method1())")
	public void afterMethod() {
		System.out.println("afterMethod 호출");
	}

	// around advice
	@Around("execution(* method1())")
	public Object aroundMethod(ProceedingJoinPoint pjp) throws Throwable{
		System.out.println("aroundMethod 호출 1");
		Object result = pjp.proceed();
		System.out.println("aroundMethod 호출 2");
		return result;
	}

	// after-returning advice
	@AfterReturning("execution(* method1())")
	public void afterReturningMethod() {
		System.out.println("afterReturning 호출");
	}
}

advisor 클래스에서 @AfterReturning 애노테이션을 사용해 after-returning advice 메소드를 구현한다.

 

🖥 실행 결과

aroundMethod 호출 1
beforeMethod 호출
method1 호출
aroundMethod 호출 2
afterMethod 호출
afterReturning 호출
a1 : 100

 

⑤ After-Throwing Advice - 호출된 메서드 동작 중 예외가 발생했을 때 동작

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@Aspect
@Component
public class AdvisorClass {

	// before advice
	@Before("execution(* method1())")
	public void beforeMethod() {
		System.out.println("beforeMethod 호출");
	}

	// after advice
	@After("execution(* method1())")
	public void afterMethod() {
		System.out.println("afterMethod 호출");
	}

	// around advice
	@Around("execution(* method1())")
	public Object aroundMethod(ProceedingJoinPoint pjp) throws Throwable{
		System.out.println("aroundMethod 호출 1");
		Object result = pjp.proceed();
		System.out.println("aroundMethod 호출 2");
		return result;
	}

	// after-returning advice
	@AfterReturning("execution(* method1())")
	public void afterReturningMethod() {
		System.out.println("afterReturning 호출");
	}

	// after-throwing advice
	@AfterThrowing("execution(* method1())")
	public void afterThrowingMethod() {
		System.out.println("afterThrowing 호출");
	}
}

advisor 클래스에서 @AfterThrowing 애노테이션을 사용해 after-throwing advice 메소드를 구현한다.

 

@Component
public class TestBean {
	
	public int method1() {
		System.out.println("method1 호출");
		
		int a1 = 10 / 0;	// 예외 발생
		
		return 100;
	}
}

테스트를 위해 method1()에서 예외를 발생시키는 코드를 추가한다.

 

🖥 실행 결과

aroundMethod 호출 1
beforeMethod 호출
method1 호출
afterMethod 호출
afterThrowing 호출
Exception in thread "main" java.lang.ArithmeticException: / by zero
	at com.atoz_develop.beans.TestBean1.method1(TestBean1.java:11)
	at com.atoz_develop.beans.TestBean1$$FastClassBySpringCGLIB$$61d1459a.invoke(<generated>)
	at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:749)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
	at org.springframework.aop.framework.adapter.MethodBeforeAdviceInterceptor.invoke(MethodBeforeAdviceInterceptor.java:56)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint.proceed(MethodInvocationProceedingJoinPoint.java:88)
	at com.atoz_develop.advisor.AdvisorClass.aroundMethod(AdvisorClass.java:32)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:566)
	at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethodWithGivenArgs(AbstractAspectJAdvice.java:644)
	at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod(AbstractAspectJAdvice.java:633)
	at org.springframework.aop.aspectj.AspectJAroundAdvice.invoke(AspectJAroundAdvice.java:70)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.aop.aspectj.AspectJAfterAdvice.invoke(AspectJAfterAdvice.java:47)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.aop.framework.adapter.AfterReturningAdviceInterceptor.invoke(AfterReturningAdviceInterceptor.java:55)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.aop.aspectj.AspectJAfterThrowingAdvice.invoke(AspectJAfterThrowingAdvice.java:62)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:93)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:688)
	at com.atoz_develop.beans.TestBean1$$EnhancerBySpringCGLIB$$dba43ddc.method1(<generated>)
	at com.atoz_develop.main.MainClass.main(MainClass.java:29)

Process finished with exit code 1

관심사로 설정한 method1()  호출 중 예외가 발생하자 after-returning advice 메소드 대신 after-throwing advice 메소드가 호출되는 것을 볼 수 있다.

테스트 후 method1()에서 예외를 발생시키기 위해 추가한 코드를 삭제하는 것을 잊지 말자.

 

관련 글

AspectJ Weaver를 사용한 XML 기반의 스프링 AOP 구현 방법

[AspectJ Weaver] execution 지시자 설정 방법 정리

 

References

인프런 - 소프트캠퍼스 스프링 프레임워크 개발자를 위한 실습을 통한 입문 과정

 

출처 : atoz-develop.tistory.com/entry/AspectJ-Weaver%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%9C-%EC%95%A0%EB%85%B8%ED%85%8C%EC%9D%B4%EC%85%98-%EA%B8%B0%EB%B0%98%EC%9D%98-%EC%8A%A4%ED%94%84%EB%A7%81-AOP-%EA%B5%AC%ED%98%84-%EB%B0%A9%EB%B2%95?category=869243