Spring

Spring Framework - AOP 코드 예제

728x90

참조한 강의 : Spring & Hibernate For Beginners (www.udemy.com/course/spring-hibernate-tutorial/)

 

 

앞선 포스팅에서 AOP 에 대해서 간략하게 알아봤으니 이번에는 몇가지 예제를 살펴보면서

Spring AOP 를 구체적으로 어떻게 사용하는지 알아본다

 

 

- @Before

Advice 타입에는 아래와 같은 종류가 존재한다.

 

- @Before

- @After

- @AfterReturning

- @AfterThrowing

- @Around

 

이 중에서 먼저 @Before 에 대해 알아본다.

 

@Before 는 Aspect 를 부여할 타겟으로 삼은 메소드가 실행되기 이전에 먼저 실행되는 Aspect 를 정의할때 사용된다.

 

(아래의 예제들은, 아주 간단하게 AOP 에 대해서 개념만 잡고 가는 정도로만 이해할것이기 때문에, 코드를 매우 단순하게 작성할것이다.)

 

예를들어, 계정을 만드는 코드를 작성해본다고 가정해보자

Spring AOP 를 이용해서 작성한다고 할때, 아주 단순하게 코드를 짤때, 이에 대한 도식화된 그림은 아래와 같다.

출처 : www.udemy.com/course/spring-hibernate-tutorial/

자바 프로그램을 실행하기 위한 Main 함수 부분이 있는 Main App 과

Spring AOP 를 사용하는 경우 Proxy Pattern 을 쓰기 때문에, 생성된 Proxy 객체

그리고 타겟 객체로 삼은 AccountDAO 가 있다.

그리고 타겟 객체인 AccountDAO 를 실행하기전에, 이 예제에서는 로그 정보를 남기는 logging aspect 를 만들것이다.

(여기서는 실제 DAO 와는 다르게 DAO 가 단순히 그냥 출력만 하도록 해놨다. AOP 가 뭔지 단순히 개념을 잡기 위해서 이다) 

 

 

- AccountDAO.java

@Component
public class AccountDAO {
 
    public void addAccount() {
        System.out.println(getClass() + ": DOING MY DB WORK : ADDING AN ACCOUNT");
    }
}
cs

 

- MyDemoLoggingAspect.java

@Aspect
@Component
public class MyDemoLoggingAspect {
    // this is where we add all of our related advices for logging.
    
    @Before("execution(public void addAccount())")
    public void beforeAddAccountAdvice() {
        System.out.println("\n ======>>> Executing @Before advice on addAccount()");
    }
}
 
cs

 

- MainDemoApp.java

public class MainDemoApp {
 
    public static void main(String[] args) {
        
        // read spring config java class
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DemoConfig.class);
        
        // get the bean from spring container
        AccountDAO accountDAO = context.getBean("accountDAO", AccountDAO.class);
 
        accountDAO.addAccount();
        
        // close the context
        context.close();
    }
 
}
cs

 

위 코드를 실행해보면, 타겟 메소드인 addAccount() 를 실행하기전에, Advice 가 실행되는것을 볼 수 있다.

 

 

 

* Pointcut Expression

앞선 AOP Overview 포스팅에서 AOP 관련 용어들을 학습한적이 있었다.

그 중에서 pointcut 이란것은 타겟 메소드의 위치를 의미하는 용어이다.

 

Spring AOP 를 통해서 pointcut 을 표현할때 쓰는것이 pointcut expression language 라는 것을 사용해서 표현한다.

1
@Before("execution(public void addAccount())")
cs

 

 

Advice Type 을 나타내는 어노테이션에 매개변수 값으로 들어가는게 바로 pointcut expression language 이다.

pointcut expression language 에서 제공하는 타입이 여러개가 있는데, 그 타입 중 하나가 execution 이다.

(다른 타입들에 대한 자세한 정보는 아래 참조)

www.baeldung.com/spring-aop-pointcut-tutorial

 

Introduction to Pointcut Expressions in Spring | Baeldung

A quick and practical intro to Spring AOP and Pointcut Expressions.

www.baeldung.com

 

execution 으로 사용할때는, 항상 먼저 execution 으로 시작해야하며,

 

그 뒤에 들어가는 내용들은 다음의 패턴을 가진다

execution(modifiers-pattern? return-type-pattern declaring-type-pattern? method-name-pattern(param-pattern) throws-pattern?)
cs

 

- modifiers-pattern

: 자바의 제어자(modifier) 를 말하는 부분으로 Spring AOP 가 제공하는 modifiers-pattern 은 public 과 * 밖에 없다

(* 는 타입 무관이라는 의미)

그리고 뒤에 붙은 ? 는 이 modifiers-pattern 값이 옵션 값이라는 의미이다. (즉 필수값이 아니므로 생략 가능)

 

- return-type-pattern

: 말 그대로 리턴 타입을 정의하는 부분이다.

(void, String, boolean 등..)

 

- declaring-type-pattern

: 타겟 클래스를 지정하는 부분이다. 

이 패턴값을 넣을 때 주의할점은 반드시 패키지 이름까지 같이 넣어서 정확한 클래스의 위치를 서술해줘야 한다는점이다.

 

- method-name-pattern (param-pattern)

: 타겟 메소드를 지정하는 부분이다.

뒤의 param-pattern 은 타겟 메소드의 매개변수 패턴을 정의하는 부분이다.

매개변수의 경우 아래와 같은 세가지 타입이 존재한다. 

 

첫째 : ()

-> 매개변수가 필요없는 경우

 

둘째 : (*)

-> 타입 상관없이 하나의 매개변수가 있을때

 

셋째 : (..)

-> 타입 상관없이 0개 또는 그이상의 매개변수들이 있을때

 

- throws-pattern

: 예외 처리 타입을 지정하는 부분이다.

 

 

* 다수의 Pointcut Expression 을 조합해서 사용할때

: 하나의 Advice 에 다수의 pointcut expression 을 조합해서 쓰고 싶을때는 어떻게 해야될까?

예를들어, 특정 패키지 내에서 getter 와 setter 를 제외한 모든 메소드에 advice 를 적용하고 싶다고 하면 pointcut expression 을 어떻게 작성해야될까?

 

-> 이에 대한 해답은 바로 논리 연산자 이다.

 

pointcut expression 을 조합할때 사용되는 논리연산자는 예를들면 아래 같은 것들이 있다

 

@Before(expression1() && expression2())

-> expression 1 과 expression 2 를 동시에 만족하는 경우

 

@Before(expression3() || expression4())

-> expression 3 와 expression 4 중 하나라도 만족하는 경우

 

@Before(expression5() && !expression6())

-> expression 5 는 만족하나 expression 6 는 만족하지 않는 경우 

 

그래서 이 논리 연산자를 이용해서 getter 와 setter 를 제외하고 나머지 메소드들에게 advice 를 적용하고 싶으면 이런식으로 쓰면 될것이다.

@Pointcut("execution(* package.*.get*(..))")
private void getter() {}
 
@Pointcut("execution(* package.*.set*(..))")
private void setter() {}
 
@Pointcut("somePointcut() && !(getter() || setter())")
private void combinationOfPointcuts() {}
 
@Before("combinationOfPointcuts()")
public void something() {}
cs

 

 

 

* Ordering Aspects

: 만약 하나의 타겟 메소드에게 다수의 aspect 가 적용되는 경우라면, 이들을 적용시키는 순서는 어떻게 정의할까?

순서를 정해놓지 않고 그냥 advice 를 여러개 적용시키면, Spring AOP 는 이 advices 들을 랜덤한 순서로 적용시킨다.

 

그래서 일정한 순서를 갖고 적용시키고자 한다면, 

@Order 라는 어노테이션을 적용해야한다.

 

예를들어, 이런식의 코드를 작성하면 된다

 

- Order(1)

@Aspect
@Component
@Order(1)
public class MyAPIAnalyticsAspect {
    
    @Before("...생략")
    public void performApiAnalytics() {
        System.out.println("...생략");
    }
}
cs

 

- Order(2)

@Aspect
@Component
@Order(2)
public class MyCloudLogAsyncAspect {
    
    @Before("...생략")
    public void logToCloudAsync() {
        System.out.println("...생략");
    }
}
cs

 

-> @Order 어노테이션은 안에들어가는 숫자가 작을 수록 우선 순위가 크다

그리고 꼭 숫자를 연속적으로 써야되는것도 아니다.

1, 2, 3 .... 이런식이 아니라

-100, -33, 200, 9999 이런식으로 써도 된다.

 

@Order 안에 들어가는 숫자의 범위는 Integer.MIN_VALUE ~ Integer.MAX_VALUE 이다.

 

(만약 @Order 안에 들어가는 숫자가 같은게 여러개 있다면, Spring AOP 가 순서를 정하지 못해서 그냥 랜덤하게 실행된다)

 

 

 

* JoinPoint

: JoinPoint 는 타겟 메소드의 매개변수를 읽는데 사용된다.

타겟 메소드의 멤버변수에 대한 로그를 남기기 위해 사용된다.

 

아래는 join point 에 대한 코드 예제이다

 

- Account.java

public class Account {
    
    private String name;
    private String level;
    
    /*
        getter, setter 생략..
    */
}
cs

 

- AccountDAO.java

@Component
public class AccountDAO {
    
    private String name;
    private String serviceCode;
    
    public void addAccount(Account account, boolean isVIP) {
        System.out.println(getClass() + ": DOING MY DB WORK : ADDING AN ACCOUNT");
    }
    
    /*
        getter, setter 생략..
    */
}
cs

 

 

- PointcutExpressions.java

@Aspect
public class PointcutExpressions {
    
    @Pointcut("execution(* com.luv2code.aopdemo.dao.*.*(..))")
    public void forDAOPackage() {}
        
    @Pointcut("execution(* com.luv2code.aopdemo.dao.*.get*(..))")
    public void forGetter() {}
        
    @Pointcut("execution(* com.luv2code.aopdemo.dao.*.set*(..))")
    public void forSetter() {}
        
    @Pointcut("forDAOPackage() && !(forGetter() || forSetter())")
    public void forDAOPackageNoGetterSetter() {}
}
cs

 

 

- MyDemoLoggingAspect.java

@Aspect
@Component
@Order(1)
public class MyDemoLoggingAspect {
    
    @Before("com.luv2code.aopdemo.aspect.PointcutExpressions.forDAOPackageNoGetterSetter()")
    public void beforeAddAccountAdvice(JoinPoint joinPoint) {
        System.out.println(" ======>>> Executing @Before advice on addAccount()");
        
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        
        System.out.println(methodSignature);
        
        Object[] args = joinPoint.getArgs();
        
        for (Object tempArg : args) {
            System.out.println("arg : " + tempArg);
            
            if (tempArg instanceof Account) {
                Account tempAccount = (Account) tempArg;
                System.out.println("temp account name : " + tempAccount.getName());
                System.out.println("temp account level : " + tempAccount.getLevel());
            }
        }
    }
}
cs

 

 

- Main.java

public class MainDemoApp {
 
    public static void main(String[] args) {
        
        // read spring config java class
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DemoConfig.class);
        
        // get the bean from spring container
        AccountDAO accountDAO = context.getBean("accountDAO", AccountDAO.class);
                
        Account account = new Account();
        account.setLevel("Bronze");
        account.setName("Madhu");
        
        // call the business method
        accountDAO.addAccount(account, true);
 
        context.close();
    }
}
cs

 

- 결과 화면

 

-> 매개변수값들을 읽어오는것을 볼 수 있다.

 

 

- @AfterReturning

: @AfterReturning 은 타겟 메소드가 성공적으로 실행이 완료되었을때만 수행되는 advice 이다.

타겟 메소드가 실행 도중에 오류가 나거나 정상적으로 끝나지 않으면 이 타입의 advice 는 실행되지 않는다.

 

그림으로 나타내면 아래와 같다

출처 : www.udemy.com/course/spring-hibernate-tutorial/

아래는 코드 예제이다

(JoinPoint 에서 보여줬던 예제하고 코드 구조가 거의 똑같다)

 

 

- MyDemoLoggingAspect.java

@Aspect
@Component
@Order(1)
public class MyDemoLoggingAspect {
    
    @AfterReturning(
            pointcut="execution(* com.luv2code.aopdemo.dao.AccountDAO.findAccounts(..))",
            returning="result")
    public void afterReturningFindAccountsAdvice(JoinPoint joinPoint, List<Account> result) {
        
        // print out which method we are advising on
        String method = joinPoint.getSignature().toShortString();
        System.out.println("\n =========>> Executing @AfterReturning on method : " + method + "\n");
        
        // print out the results of method call
        System.out.println("\n =========>> results are : " + result + "\n");
        
        // convert the account names to uppercase
        convertAccountNamesToUpperCase(result);
        
        System.out.println("\n =========>> after converting, results are : " + result + "\n");
    }
    
    private void convertAccountNamesToUpperCase(List<Account> result) {
        
        // loop through accounts
        
        for (Account tempAccount : result) {
            // get uppercase version of name
            String upperName = tempAccount.getName().toUpperCase();
            
            // update the name on the account 
            tempAccount.setName(upperName);
        }
    }
 
}
cs

-> 이 코드에서 하나 주목해야될점은 @Before 와는 다르게 하나의 매개변수가 더 추가 되었다는것이다.

returning 이라는게 추가되었다는것. 이거는 타겟 메소드가 보내는 결과값을 의미한다.

그리고 advice 를 정의할때, 매개변수에 반드시 일치하는 이름을 써야만 한다

returning = "result" 와 매개변수 List<Account> result 가 일치하는것을 볼 수 있다.

 

 

 

- AccountDAO.java

@Component
public class AccountDAO {
    
    private String name;
    private String serviceCode;
    
    public List<Account> findAccounts() {
        List<Account> accounts = new ArrayList<>();
        
        Account account1 = new Account("John""Sliver");
        Account account2 = new Account("Madhu""Gold");
        Account account3 = new Account("Luca""Bronze");
        
        accounts.add(account1);
        accounts.add(account2);
        accounts.add(account3);
        
        return accounts;
    }
 
    /*
        생략...
    */
}
cs

(생략된 부분은 join point 코드 부분)

 

 

- Main.java

public class AfterReturningDemoApp {
 
    public static void main(String[] args) {
        
        // read spring config java class
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DemoConfig.class);
        
        // get the bean from spring container
        AccountDAO accountDAO = context.getBean("accountDAO", AccountDAO.class);
        
        List<Account> accounts = accountDAO.findAccounts();
        
        System.out.println("Main Program : AfterReturningDemoApp");
        
        System.out.println("\n\n" + accounts + "\n\n");
                
        // close the context
        context.close();
    }
}
cs

 

- 출력 결과

타겟 메소드를 성공적으로 수행해서, @AfterReturning 에 정의한대로, 결과값들을 대문자로 치환하는것을 볼 수 있다,

 

 

 

- @AfterThrowing

: @AfterThrowing 은 타겟메소드가 수행 도중에 오류가 발생한 경우 사용하는 advice 이다.

그림으로 나타내면 아래와 같다

 

출처 : www.udemy.com/course/spring-hibernate-tutorial/

 

아래는 코드 예제이다.

위의 @AfterReturning 과 똑같은데 findAccouts 부분에 boolean 값을 넣어서 의도적으로 RuntimeException 오류가 나도록 해놨다.

 

 

- AccountDAO.java

@Component
public class AccountDAO {
    
    private String name;
    private String serviceCode;
    
    public List<Account> findAccounts(boolean tripWire) {
        
        if (tripWire) {
            throw new RuntimeException("Error !");
        }
        
        List<Account> accounts = new ArrayList<>();
        
        Account account1 = new Account("John""Sliver");
        Account account2 = new Account("Madhu""Gold");
        Account account3 = new Account("Luca""Bronze");
        
        accounts.add(account1);
        accounts.add(account2);
        accounts.add(account3);
        
        return accounts;
    }
}
cs

 

 

- MyDemoLoggingAspect.java

@Aspect
@Component
@Order(1)
public class MyDemoLoggingAspect {
    
    @AfterThrowing(
            pointcut="execution(* com.luv2code.aopdemo.dao.AccountDAO.findAccounts(..))",
            throwing="exc")
    public void afterThrowingFindAccountAdvice(JoinPoint joinPoint, Throwable exc) {
        String method = joinPoint.getSignature().toShortString();
        System.out.println(" =======>>> Executing @AfterThrowing on method : " + method);
        
        System.out.println(" =======>>> Executing exception : " + exc);
    }
}
cs

 

@AfterThrowing 에서는 returning 이 아니라, throwing 을 쓰는것을 볼 수 있다.

그리고 타입도 Throwable 이다.

 

 

- Main.java

public class AfterThrowingDemoApp {
 
    public static void main(String[] args) {
        
        // read spring config java class
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DemoConfig.class);
        
        // get the bean from spring container
        AccountDAO accountDAO = context.getBean("accountDAO", AccountDAO.class);
        
        List<Account> accounts = null;
        
        try {
            boolean tripWire = true;
            accounts = accountDAO.findAccounts(tripWire);
        } catch (Exception e) {
            System.out.println("\n\n ========>> Main Program caught exception" + e);
        }
        
        System.out.println("Main Program : AfterThrowingDemoApp");
        
        System.out.println("\n\n" + accounts + "\n\n");
                
        // close the context
        context.close();
    }
}
cs

 

- 실행 결과

 

 

 

 

- @After

: @After 는 타겟 메소드가 실행에 성공하든 실패하든 성공 여부와는 관계 없이 실행되는 advice 이다.

 

그림으로 나타내면 아래와 같다

 

출처 : www.udemy.com/course/spring-hibernate-tutorial/

 

@AfterThrowing 에서 작성한 코드 예제를 그대로 쓰되,

아래처럼 @After 만 따로 정의해준다

 

- MyDemoLoggingAspect.java

@Aspect
@Component
@Order(1)
public class MyDemoLoggingAspect {
    
    @After("execution(* com.luv2code.aopdemo.dao.AccountDAO.findAccounts(..))")
    public void afterFinallyFindAccountsAdvice(JoinPoint joinPoint) {
        String method = joinPoint.getSignature().toShortString();
        System.out.println(" =======>>> Executing @After on method : " + method);
    }
}
cs

 

 

 

- @Around

: @Around 는 타겟 메소드 전후로 실행되는 advice 이다.

그림으로 나타내면 아래와 같다.

 

출처 : www.udemy.com/course/spring-hibernate-tutorial/

 

@Around 를 사용하면, 예외 처리를 메인에게 넘겨 줄수도 있고, 안줄수도 있다.

 

아래는 @Around 에 대한 코드 예제이다.

 

- MyDemoLoggingAspect.java

@Aspect
@Component
@Order(1)
public class MyDemoLoggingAspect {
    
    private Logger myLogger = Logger.getLogger(getClass().getName());
    
    @Around("execution(* com.luv2code.aopdemo.service.*.getFortune(..))")
    public Object aroundGetFortune(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
 
        String method = proceedingJoinPoint.getSignature().toShortString();
        myLogger.info("\n ======>>> executing @Around on method : " + method + "\n");
        
        long begin = System.currentTimeMillis();
        
        Object result = null;
        
        try {
            result = proceedingJoinPoint.proceed(); // 타겟 메소드 수행
        } catch (Throwable e) {
            myLogger.warning(e.getMessage());
            
            result = "Exception was handled by @Around advice.";
            
            // throw e;
        }
        
        long end = System.currentTimeMillis();
        
        long duration = end - begin; // 타겟 메소드를 수행하는데 걸리는 시간 체크
        myLogger.info("\n Duration : " + duration / 1000.0 + " seconds ");
        
        return result; // 타겟 메소드 수행 결과 리턴
    }
}
cs

 

 

- TrafficService.java

@Component
public class TrafficFortuneService {
    
    public String getFortune() {
        
        try {
            TimeUnit.SECONDS.sleep(5); // sleep 을 걸어둔 이유는 @Around advice 의 실행과정을 더 편하게 보기 위해서이다.
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        return "Expect heavy traffic this morning";
    }
 
    public String getFortune(boolean tripWire) {
        
        if (tripWire) 
            throw new RuntimeException("Major Accident! Highway is closed ");
        
        return getFortune();
    }
}
cs

 

 

- Main.java

public class AroundHandleExceptionDemoApp {
    
    private static Logger myLogger = Logger.getLogger(AroundHandleExceptionDemoApp.class.getName());
 
    public static void main(String[] args) {
        
        // read spring config java class
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DemoConfig.class);
        
        // get the bean from spring container
        TrafficFortuneService trafficFortuneService = context.getBean("trafficFortuneService", TrafficFortuneService.class);
        
        myLogger.info("\n ========== >>> Main Program : Around Demo App ");
 
        myLogger.info("Calling getFortune()");
        
        boolean tripWire = true;
        String data = trafficFortuneService.getFortune(tripWire);
        
        myLogger.info("\n =======> My Fortune is : " +  data + "\n");
        
        myLogger.info("Finished");
        
        // close the context
        context.close();
    }
 
}
cs

 

- 출력 결과

 

위에 MyDemoLoggingAspect.java 에서 try catch 부분에 result = "Exception was ~" 부분을 지우고 

throw e; 를 대신쓰면 main 으로 예외처리가 넘어간다

 

아래는 메인으로 넘겼을때 화면이다.

 

* 참고

https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#aop-ataspectj-advice-ordering

같은 @Aspect 클래스안에 있으면서 동시에 같은 join point 를 가르키는 advice method 들이 존재한다면,
@After advice 가 @AfterReturning 이나 @AfterThrowing 뒤에 수행된다.

 

원래는 순서가 역으로 되어 있었는데, 스프링 5.2.7 이후 부터 순서가 개정되었다.

 

 

 

 

여기까지 Advice 타입과 간단한 예제 몇가지를 알아봤다

 

다음은 Spring Security 에 대해 알아본다.

 

 

- Reference)

1. AOP Pointcut Tutorial  : www.baeldung.com/spring-aop-pointcut-tutorial

2. Advice Ordering : docs.spring.io/spring-framework/docs/current/reference/html/core.html#aop-ataspectj-advice-ordering

 

 

 

 

728x90

'Spring' 카테고리의 다른 글

Spring Security - Overview  (0) 2021.03.04
Maven 이란  (0) 2021.02.25
Spring Framework - AOP Overview  (0) 2021.02.17
Spring Framework - Layered Architecture  (0) 2021.02.16
Spring Framework - 연관 관계 매핑  (0) 2021.02.13