참조한 강의 : 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 를 이용해서 작성한다고 할때, 아주 단순하게 코드를 짤때, 이에 대한 도식화된 그림은 아래와 같다.
자바 프로그램을 실행하기 위한 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
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 는 실행되지 않는다.
그림으로 나타내면 아래와 같다
아래는 코드 예제이다
(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 이다.
그림으로 나타내면 아래와 같다
아래는 코드 예제이다.
위의 @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 이다.
그림으로 나타내면 아래와 같다
@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 이다.
그림으로 나타내면 아래와 같다.
@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 으로 예외처리가 넘어간다
아래는 메인으로 넘겼을때 화면이다.
* 참고
같은 @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
'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 |