본문 바로가기
Web Programming/springboot

Spring - AOP 기본개념 및 주요 기능, 예제와 함께 이해하기

by 맑은안개 2022. 6. 28.

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

PointcutAdvice의 실행 위치(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 ( 사용자 행위 감시 )
  • 트랜잭션 매니징
  • 토큰 체크
  • 익셉션 핸들링
  • 캐싱
반응형