What is AOP?
Spring의 핵심기능인 AOP(Aspect Oriented Programming)을 이해하기 위해 다음의 예제를 살펴보자.
(예시로 들었지만 실제운영환경에서 다음과 같은 코드를 많이 마주치곤 한다.)
public class Foo {
public void makeFoo() {
System.out.println("makeFoo started..");
// some logic
System.out.println("makeFoo ended..");
}
public void printFoo(String msg) {
System.out.println("printFoo started..");
// some logic
System.out.println("printFoo ended..");
}
}
public class Bar{
public void makeBar() {
System.out.println("makeBar started..");
// some logic
System.out.println("makeBar ended..");
}
...
}
모든 비지니스 로직 함수에 로직이 실행되는 전, 후 시점에 Logging을 하였다.
위와 같은 코딩은 반복코드라는 점에서 비효율적
이고, 함수의 순기능 역할만 하지 않는데 있어 클린
하지 않다고 할 수 있다. 재사용성
면에서는 특정 클래스에 특정 함수 별 기능을 확장할 수 없다.
AOP의 필요성
위의 예제에서 처럼, 사용자는 모든 비지니스 함수가 실행 될 때(event), 특정 행위(action)를 하길 원할 수 있다.
이런 행위에 대상(Target Object
)은 클래스
혹은 메소드
로 특정할 수 있다. 특정하기 위한 방법으로 Pointcut
을 사용하는데 이는 다음과 같이 문자열 표현식
을 갖는다.
( PointCut expressions - 포인트컷 표현식은 다음 블로그에서 자세히 다룬다. )
@Pointcut("execution(* transfer(..))")// the pointcut expression
private void anyOldTransfer() {}// the pointcut signature
이런 특정 행위들을 한데모아(Aspect Class) 모듈
형태로 분리하여 별도로
관리 할 수 있다. 가독성
은 물론 재사용성
을 제고하는데 큰 도움이 된다.
Environment for example
- Windows
- Visual Studio Code
- Gradle
- SpringBoot 2.7.0
1. Example
📃build.gradle
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-aop:2.7.0'
implementation 'org.springframework.boot:spring-boot-starter'
..기타..
}
📃DemoApplication.java
@SpringBootApplication
public class DemoApplication implements ApplicationRunner {
@Autowired
DemoAction demoAction;
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
@Override
public void run(ApplicationArguments args) throws Exception {
demoAction.startAction();
}
}
📃DemoAction.java
@Component
public class DemoAction {
public void biz() {
System.out.println("Before demoAction");
System.out.println("do logic");
System.out.println("After demoAction");
}
}
비지니스 로직인 biz()
함수를 실행하고 로직 전, 후 시점에 로그를 출력한다. 다음과 같이 반복
되는 패턴의 내용을 AOP 기능을 사용하여 분리
한다.
📃CommonAspect.java
@Around
@Aspect
@Component
public class CommonAspect {
@Around("execution(* biz())")
public void aroundMethod(ProceedingJoinPoint proceedingJoinPoint) {
System.out.println("Before "+proceedingJoinPoint.getSignature().toShortString());
try {
proceedingJoinPoint.proceed();
} catch (Throwable e) {
e.printStackTrace();
}
System.out.println("After "+proceedingJoinPoint.getSignature().toShortString());
}
}
@Aspect
어노테이션을 사용하여 Aspect class임을 선언한다.@Around
어노테이션은ProceedingJoinPoint
를 인자로 갖는다. 해당 클래스는PointCut
대상 오브젝트(Method)를 실행할 수 있다.@Around
를 사용하여 함수 실행 전, 후 시점에 로그를 출력했다. 이로써, 기존 로직 함수에반복
하여 구현했던 로그를 삭제할 수 있다.
@Component
public class DemoAction {
public void biz() {
// System.out.println("Before demoAction");
System.out.println("do logic");
// System.out.println("After demoAction");
}
}
- 위와 같이 반복코드인 로그출력 문을 주석처리 하고 실행해보자.
실행결과
Before DemoAction.biz()
do logic
After DemoAction.biz()
Pointcut
Pointcut
은 Advice
의 실행 위치(Join point
)를 나타낸다. 즉 실행 시킬 클래스의 메소드를 지정한다.
위에서 살펴본 @Around
를 @Pointcut
으로 다음과 같이 사용할 수 있다.
// CommonAspect.java
@Pointcut("execution(* biz())")
public void pointCutAllBizMethod(){}
@Around("pointCutAllBizMethod()")
public void aroundMethod(ProceedingJoinPoint proceedingJoinPoint) {
System.out.println("Before "+proceedingJoinPoint.getSignature().toShortString());
try {
proceedingJoinPoint.proceed();
} catch (Throwable e) {
e.printStackTrace();
}
System.out.println("After "+proceedingJoinPoint.getSignature().toShortString());
}
...
&&
,||
조건연산자를 사용하여 유연한Pointcut
적용이 가능하다.
// DemoAction.java
public void foo() {
System.out.println("Foo is started");
}
// DemoApplication.java
@Override
public void run(ApplicationArguments args) throws Exception {
demoAction.biz();
demoAction.foo();
}
// CommonAspect.java
@Pointcut("execution(* biz())")
public void accessAllBiz(){}
@Pointcut("execution(* foo())")
public void accessAllfoo(){}
@Around("accessAllBiz() || accessAllfoo()")
public void aroundMethod(ProceedingJoinPoint proceedingJoinPoint) {
System.out.println("Before "+proceedingJoinPoint.getSignature().toShortString());
try {
proceedingJoinPoint.proceed();
} catch (Throwable e) {
e.printStackTrace();
}
System.out.println("After "+proceedingJoinPoint.getSignature().toShortString());
}
...
실행결과
Before DemoAction.biz()
do logic
After DemoAction.biz()
Before DemoAction.foo()
Foo is started
After DemoAction.foo()
2. Advice
위에서 살펴본 @Around
외 다른 Advice를 살펴본다.
@Before
Jointpoint
가 실행되기 전 실행된다. ( ❗ Advice에서 Jointpoint실행을 제어할 수 없다. )
// CommonAspect.java
@Before("execution(* com.example.demo.action.DemoAction.biz())")
public void checkSomethingBefore() {
System.out.println("Check something before...");
}
@AfterReturning
Jointpoint
실행후 리턴된 데이터를 받을 수 있다. ( ❗ Joinpoint에서 예외 발생시 실행되지 않는다. )
// DemoAction.java
public String getSayHello() {
return "Hello World";
}
// DemoApplication.java
@Override
public void run(ApplicationArguments args) throws Exception {
demoAction.getSayHello();
}
// CommonAspect.java
@AfterReturning(
pointcut ="execution(* com.example.demo.action.DemoAction.get*(..))",
returning="retVal")
public void checkReturnValue(Object retVal) {
if(retVal instanceof String) {
System.out.println("the return value is "+(String)retVal);
}
}
@AfterThrow
Joinpoint
에서 예외 발생 시 실행된다.
// DemoAction.java
// index 참조 오류 발생
public void accessArray() {
int[] s = {1,2,3};
System.out.println(s[4]);
}
// DemoApplication.java
@Override
public void run(ApplicationArguments args) throws Exception {
demoAction.accessArray();
}
>> 실행결과 >>
Caused by: java.lang.ArrayIndexOutOfBoundsException: 4
// CommonAspect.java
@AfterThrowing(
pointcut = "execution(* com.example.demo.action.DemoAction.*(..))",
throwing = "ex")
public void handleThrowExceptionOnAll(Exception ex) {
System.out.println("handleThrowExceptionOnAll");
ex.printStackTrace();
}
>> 실행결과 >>
handleThrowExceptionOnAll
java.lang.ArrayIndexOutOfBoundsException: 4
at com.example.demo.action.DemoAction.accessArray(DemoAction.java:30)
at com.example.demo.action.DemoAction$$FastClassBySpringCGLIB$$d63ee897.invoke(<generated>)
...
@After
Jointpoint
가 호출되고 무조건 실행된다.
// CommonAspect.java
@After("execution(* com.example.demo.action.DemoAction.get*(..))")
public void finallyCheckReturnValue(){
System.out.println("check something one more");
}
the return value is Hello World
check something one more
3. Joinpoint 파라미터 접근
Pointcut
에 전달된 파라미터
를 Advice
에서 인자로 받을 수 있다.
// DemoAction.java
public void printMessage(String msg) {
System.out.println(msg);
}
// DemoApplication.java
@Override
public void run(ApplicationArguments args) throws Exception {
demoAction.printMessage("Hello World");
}
// CommonAspect.java
@Before(
"execution(* com.example.demo.action.DemoAction.printMessage(..)) && " +
"args(msg)")
public void checkParameterBefore(String msg){
System.out.println("checkParameterBefore >> " + msg);
}
>> 실행결과 >>
checkParameterBefore >> Hello World
Hello World
4. USE CASES
AOP를 이용하여 많은 기능을 구현할 수 있다. 그 중 대표적인 것들을 몇가지 정리해보았다.
- 퍼포먼스 모니터링
- Audit ( 사용자 행위 감시 )
- 트랜잭션 매니징
- 토큰 체크
- 익셉션 핸들링
- 캐싱
'Web Programming > springboot' 카테고리의 다른 글
Springboot 콘솔 배너 변경하기 ( 배너 생성 사이트 소개 ) (0) | 2022.07.25 |
---|---|
문제해결: Caused by: java.lang.IllegalArgumentException: error at ::0 formal unbound in pointcut (0) | 2022.07.05 |
VS Code에서 Spring실행 시 자바 옵션 전달하기(Java heap size 변경) (0) | 2022.06.23 |
VSCode에서 Spring boot + JPA(mysql) on Windows 10 + Docker 튜토리얼 (0) | 2021.10.20 |
VSCode에서 SpringBoot with gradle 프로젝트 생성 & 실행 (4) | 2021.04.27 |