본문 바로가기
프로그래밍/Spring Boot

[SpringBoot] AOP를 활용하는 다양한 방법

by 카프카뮈 2021. 8. 30.

https://kafcamus.tistory.com/39

 

[SpringBoot] 관점 지향 프로그래밍(AOP)이란?

이 글은 다음의 목적을 바탕으로 작성되었다. AOP가 무엇인지 알아본다. AOP를 활용하는 방법을 알아본다. AOP란? AOP는 관점 지향 프로그래밍(Aspect-Oriented Programming)의 약자이다. AOP는 횡단 관심사(cr

kafcamus.tistory.com

저번 글에서는 다음과 같은 내용을 다루었다.

  • AOP가 무엇인지 알아본다.
  • Spring에서 AOP를 활용하는 방법을 알아본다.

이번 글에서는, 이에 더해 실제 AOP를 활용한 간단한 예시를 작성해보려고 한다.

  • 커스텀 어노테이션을 구현한다.
  • 어노테이션이 있는 메소드 실행 시, AOP를 실행한다.
  • 결과적으로 로깅 로직을 Aspect로 분리한다.

이 글에서는 로깅에 대한 로직 추가가 번거로워 로그를 System.out으로 출력하도록 짰다.

로그를 어떻게 넣는지가 궁금하다면, 이 글을 참조해서 의존성과 lombok 어노테이션을 추가하면 된다!

 

8. 로그 파일 만들기, 인터셉터 구현하기

코드 확인을 위해, 관련 PR 링크를 첨부한다. #5 로깅 구현 및 로깅을 위한 인터셉터 구현하기 by include42 · Pull Request #10 · include42/spring-books-diary Resolved: #5 실수로 Develop 브랜치에 커밋해..

kafcamus.tistory.com


Spring AOP Gradle에 추가하기

스프링 프레임워크에서 AOP 기능을 활용하고 싶다면, spring-aop 모듈을 의존 목록에 추가하면 된다.

SpringBoot를 사용하는 경우, 아래처럼 spring-boot-starter-aop를 추가하면 된다. (Gradle 기준)

//build.gradle

dependencies {
    ...
    //적당한 곳에 추가해 준다!
    compile('org.springframework.boot:spring-boot-starter-aop')
}

목적: 로깅을 Aspect로 분리하기

이제 간단히 프로젝트의 목적을 정해보자.

만약 아래와 같은 상황이 생긴다면 어떻게 할까?

  • 로직이 제대로 실행되는지 궁금해서, 메소드 인자값과 반환값을 로그로 남기고 싶다.
  • 다만 특정 패키지에 속하는 클래스의 메소드만 로그로 남기고 싶다.
  • 혹은, 특정 어노테이션이 붙은 메소드의 실행도 로그로 남기고 싶다.

AOP를 쓰지 않고 위의 문제를 구현하려면, 비즈니스 로직이 들어가는 코드들마다 관련된 로그 코드를 남겨야 한다.

그런데 만약 로그를 남겨야 하는 메소드의 목록이 바뀌면? 해당 메소드에 가서 코드를 지우거나 추가해야 한다.

게다가 특정한 예외에 대해 대응하는 건 더 어려워서, 결국 코드를 try-catch로 감싸거나 혹은 애꿎은 상위 레이어에까지 손을 댈 수 있는 상황이다.

 

하지만 우리에겐 AOP가 있다!

AOP 전략을 다음과 같이 잡아보자.

  • 특정한 어노테이션을 만들어서, 해당 어노테이션이 있는 메소드 실행을 Pointcut으로 지정한다.
  • 또한, 특정한 패키지를 지정해서 해당 패키지의 메소드가 실행되는 경우도 Pointcut으로 지정한다.
  • Aspect가 적용되는 타이밍은 메소드가 시작될 때와 끝날 때이다. Advice를 설정한다.

커스텀 어노테이션 만들기

일단 커스텀 어노테이션을 만들어 보자! 어노테이션으로 파일을 만들면 된다.

로그를 남기고 싶은 메소드에는, @LogDebug라는 어노테이션을 붙인다고 치자.

//@LogDebug

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogDebug {

}

위 코드를 통해 LogDebug라는 어노테이션을 선언했다. 

타겟은 메소드이며, Runtime중에 실행되도록 설정하였다.

그럼 이제 이에 맞는 Aspect와 Advice를 설정해 보자.


Aspect 만들기

일단, 아래와 같은 코드를 작성한다. 로그 작업을 도울 Aspect 클래스 파일이다.

@Aspect
@Component
public class ExampleAspect {

}

@Aspect라는 어노테이션을 달아 역할을 명시하였고, 컴포넌트 어노테이션을 달아 IoC에서 관리하도록 설정한다.

이제 여기에 각각의 PointCut과 Advice에 대한 메소드를 작성해 주면 된다!

PointCut

일단 먼저, PointCut들을 만들어 보자.

// aop.service 아래의 메소드가 실행되는 경우를 지정한 pointcut
@Pointcut("execution(* com.example.aop.service..*.(..))")
private void cut() {} 

// LogDebug 어노테이션이 붙은 메소드가 실행되는 경우를 지정한 pointcut
@Pointcut("@annotation(com.example.aop.annotation.LogDebug)")
private void logDebug() {}

위의 cut 메소드는, aop.service 디렉토리 아래에 있는 클래스들의 메소드를 지정한다.

만약 해당 메소드 중 하나가 실행될 경우, 이 Pointcut에 해당하는 것으로 인식한다.

 

두번째 메소드는, @LogDebug 어노테이션이 붙은 메소드가 실행되는 경우를 지정한다.

해당 메소드가 실행될 경우, 마찬가지로 이 Pointcut에 해당하는 것으로 인식한다.

 

만약 Pointcut을 더 자세히 설정하고 싶다면, 이 링크를 참조하는 것을 권한다.

이제, Advice를 바탕으로 실제 Aspect 메소드를 만들어 보자!

Before

메소드가 실행되기 전에 먼저 실행되도록 Before Advice에 해당하는 Aspect를 만들어 보자.

// 만약 두 포인트컷을 모두 지정하고 싶다면 @Before("cut() && logDebug()") 라고 쓰면 됨
@Before("logDebug()")
public void before(JoinPoint joinPoint) {
    Object[] args = joinPoint.getArgs();
    
    for(Object arg : args) {
        System.out.println("arg is : " + arg);
    }
}

만약 인자의 클래스를 더 정확히 확인해서 출력하고 싶다면, 아래처럼 활용해도 된다.

아래 advice는 인자 중 User 클래스인 인자만 출력하며,

LogDebug 어노테이션이 붙은 메소드 혹은 정해진 경로에 있는 메소드가 실행 시 Aspect가 호출된다.

@Before("cut() && logDebug()")
public void before(JoinPoint joinPoint) {
    Object[] args = joinPoint.getArgs();
    
    for(Object arg : args) {
        if(arg instanceof User) {
            System.out.println("arg is : " + arg);
        }
    }
}

After

메소드가 값을 반환한 뒤 실행되도록 AfterReturning Advice에 해당하는 Aspect를 만들어 보자.

@ArterReturning("cut() && logDebug()", returning = "returnObj")
public void afterReturn(JoinPoint joinPoint, Object returnObj) {
    User user = User.class.cast(returnObj);
    System.out.println("return is : " + user);
}

@AfterReturning에는 returning이라는 옵션을 설정해 줄 수 있다.

여기에 설정된 값을 함수 인자로 지정하면, 해당 값으로 AOP가 적용되는 메소드의 반환값이 전달된다.

이를 통해 해당 값을 조작하거나 로깅할 수 있다.

Around

이번엔 메소드가 시작되어서 종료될 때까지 일련의 JoinPoint에서 모두 작동하도록 짜보자!

아래 코드는 Around Advice에 해당한다. 흔히 예제 코드로 쓰이는, 메소드의 실행 시간을 재는 코드이다.

@Around("cut() && logDebug()")
public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
    long start = System.currentTimeMillis();

    Object proceed = joinPoint.proceed();

    long executionTime = System.currentTimeMillis() - start;

    System.out.println(joinPoint.getSignature() + " executed in " + executionTime + "ms");
    return proceed;
}

만약 위의 Before나 AfterReturning과 같이 인자에 접근하고 싶다면 어떻게 하면 될까? 아래 코드를 보자.

@Around("cut() && logDebug()")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
    Object[] args = joinPoint.getArgs();
    
    for(Object arg : args) {
        if(arg instanceof User) {
            System.out.println("arg is : " + arg);
        }
    }
    Object result = joinPoint.proceed();

    System.out.println("return is : " + result);
    return result;
}

이전에 Before Advice에서 했던 것처럼 getArgs를 통해 인자에 접근할 수 있다!

게다가 joinPoint에서 Proceed를 하면 해당 메소드가 실행되는데, 이때 반환값을 받게 된다. 이 값을 통해 반환값을 확인할 수 있다. 다만 이 결과값은 Around Advice를 사용하면 꼭 반환해줘야 한다!! 프록시 패턴으로 구현되기 때문에 반환해주지 않으면 의도치 않은 오류를 맞을 수 있다는 점을 명심하자.

또한 필요한 경우, joinPoint에서 getSignature 메소드를 호출해서 해당 메소드의 시그니처(리턴 타입, 이름, 매개변수) 정보를 전달받을 수도 있다.

After & After Throwing

상대적으로 많이 사용되는 세 가지를 소개했다. 다만 아직 두 가지 Advice, After와 After Throwing이 남았다.

이 경우는 spring docs에 소개된 예제로 간단히만 설명하고 넘어가려고 한다.

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

@Aspect
public class AfterThrowingExample {

  @AfterThrowing("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
  public void doRecoveryActions() {
    // ...
  }

}

AfterThrowing의 공식 문서 예제이다. 여기서는 포인트컷 메소드를 따로 지정하지 않고, 직접 경로로 지정하고 있다.

만약 해당하는 메소드에서 예외가 발생할 경우, 해당 Aspect 메소드가 호출된다.

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

@Aspect
public class AfterFinallyExample {

  @After("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
  public void doReleaseLock() {
    // ...
  }

}

이 코드는 After의 공식 문서 예제이다. AfterReturning은 예외가 발생한 경우 호출되지 않는 데에 비해, 이 advice는 예외 발생 여부(즉, 반환 로직 실행 여부)와 상관없이 메소드가 종료되면 무조건 호출된다.


더 알아보기: @Order

지금까지 AOP를 사용해 간단한 로깅을 수행하고, 메소드의 다양한 값에 접근하는 방법을 배웠다.

그런데, 지금까지의 코드를 보다 보면 다음과 같은 의문이 들 수 있다.

만약 여러 개의 Aspect 클래스가 존재한다면,
어느 Aspect 클래스가 먼저 실행될까?

이러한 경우에 어떤 Aspect가 먼저 적용되는지는 스프링 프레임워크나 자바 버전에 따라 달라질 수 있다.

그러나 이 경우 우선순위에 맞지 않는 연산이 일어나 혼란이 발생할 수 있다.

이를 해결하기 위해, spring aop는 @order라는 어노테이션을 제공한다.

@Aspect
@Order(1)
public class FirstAspect {
    ...
}

@Aspect
@Order(2)
public class SecondAspect {
    ...
}

위처럼 Aspect에 Order 어노테이션을 붙이고, 우선순위를 기재할 수 있다.

이 경우, 숫자가 큰 것부터 프록시 패턴을 적용한다. 이렇게 말하면 헷갈릴 수 있는데,

실제 수행은 SecondAspect가 먼저 수행된다!

대상 객체를 먼저 Order 2인 Aspect가 감싸고, 이를 다시 Order 1인 Aspect가 감싸기 때문에,

만약 AOP가 동작할 경우 대상 메소드 수행 -> 메소드를 감싼 SecondAspect 수행 -> 메소드를 감싼 FirstAspect 수행 순서대로 진행되는 것이다!

 

그리고, Order는 0이나 음수를 써도 된다.

각각의 Order 간의 크기만 비교하기 때문이다.

 


글을 마치며

긴 글을 통해 AOP의 다양한 응용 방법에 대해 알아봤다.

직접 공부하며 작성한 글이라 많이 서투르지만, 그래도 정리하면서 많은 도움이 되었다.

처음엔 @Transactional의 원리를 이해하려고 시작한 공부였는데,

앞으로 활용할 가능성들에 흥미가 돋는다. 해보고 싶은게 점점 많아지는 요즘이다. 

 

만약 잘못된 내용이 있다면, 댓글 부탁드립니다

반응형

댓글