Spring/4. 스프링 AOP

Day 69 : 어노테이션 기반 AOP

pancakemaker 2022. 1. 21. 17:02

1. 어노테이션 사용을 위한 스프링 설정 파일

: <aop:aspectj-autoproxy /> 추가

 

- 어노테이션 사용하지 않을 때의 스프링 설정 파일

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:aop="http://www.springframework.org/schema/aop"
	xmlns:p="http://www.springframework.org/schema/p"
	xsi:schemaLocation="http://www.springframework.org/schema/beans 
		http://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/context 
		http://www.springframework.org/schema/context/spring-context-4.2.xsd
		http://www.springframework.org/schema/aop 
		http://www.springframework.org/schema/aop/spring-aop-4.2.xsd">

	<context:component-scan base-package="com.springbook.biz">
	</context:component-scan>
	
	<bean id="around" class="com.springbook.biz.common.AroundAdvice" />
	
	<aop:config>
		<aop:pointcut id="allPointcut"
					expression="execution(* com.springbook.biz..*Impl.*(..))"/>		
						
		<aop:aspect ref="around">
			<aop:around pointcut-ref="allPointcut" method="aroundLog"/>
		</aop:aspect>
				
	</aop:config>	
</beans>

 

- 어노테이션을 사용할 때의 스프링 설정 파일

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:aop="http://www.springframework.org/schema/aop"
	xmlns:p="http://www.springframework.org/schema/p"
	xsi:schemaLocation="http://www.springframework.org/schema/beans 
		http://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/context 
		http://www.springframework.org/schema/context/spring-context-4.2.xsd
		http://www.springframework.org/schema/aop 
		http://www.springframework.org/schema/aop/spring-aop-4.2.xsd">

	<context:component-scan base-package="com.springbook.biz">
	</context:component-scan>
	
	<aop:aspectj-autoproxy></aop:aspectj-autoproxy>	
	
</beans>

2. 어드바이스 클래스 : @Service (또는 스프링 설정 파일에 <bean> 등록)

3. 포인트컷 설정 : @Pointcut

 

- 어드바이스 클래스

package com.springbook.biz.common;

import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Service;

@Service
public class LogAdvice {
	@Pointcut("execution(* com.springbook.biz..*Impl.*(..))")
	public void allPointcut() {

	}
	
	@Pointcut("execution(* com.springbook.biz..*Impl.get*(..))")
	public void getPointcut() {

	}
}

4. 어드바이스 설정 (포인트컷 참조 설정) : @Before, @AfterReturning, @AfterThrowing, @After, @Around

 

- 어드바이스 클래스

package com.springbook.biz.common;

import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Service;

@Service
public class LogAdvice {
	@Pointcut("execution(* com.springbook.biz..*Impl.*(..))")
	public void allPointcut() {

	}
	
	@Before("allPoingcut()")	//"allPoingcut" 포인트컷 참조
	public void printLog() {
		System.out.println("[공통 로그] 비즈니스 로직 수행 전 동작");
	}
}

5. 애스팩트 설정 : @Aspect (= Poingcut + Advice)

 

- 어드바이스 클래스

package com.springbook.biz.common;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Service;

@Service
@Aspect		//Aspect = Pointcut + Advice
public class LogAdvice {			//포인트컷
	@Pointcut("execution(* com.springbook.biz..*Impl.*(..))")
	public void allPointcut() {

	}
	
	@Before("allPoingcut()")		//어드바이스 ("allPoingcut" 포인트컷 참조)
	public void printLog() {
		System.out.println("[공통 로그] 비즈니스 로직 수행 전 동작");
	}
}

6. 어드바이스 동작 시점

① Before 어드바이스 : @Before

- 어드바이스 클래스

package com.springbook.biz.common;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Service;

@Service	//서비스 설정(객체 생성)
@Aspect		//애스팩트 설정 = Pointcut + Advise
public class BeforeAdvice {
	@Pointcut("execution(* com.springbook.biz..*Impl.*(..))")	//포인트컷 설정
	public void allPointcut() {
		
	}
	
	@Before("allPointcut()")	//어드바이스 설정
	public void beforeLog(JoinPoint jp) {
		String method = jp.getSignature().getName();	//클라이언트가 호출한 메소드의 이름 추출
		Object[] args = jp.getArgs();					//메소드 호출 시 넘겨준 인자 목록(사용한 값들)을 배열로 추출
		
		System.out.println("[사전 처리] " + method + "() 메소드 ARGS 정보: " + args[0].toString());
	}
}

 

- 클라이언트 클래스 실

INFO : org.springframework.beans.factory.xml.XmlBeanDefinitionReader - Loading XML bean definitions from class path resource [applicationContext.xml]
INFO : org.springframework.context.support.GenericXmlApplicationContext - Refreshing org.springframework.context.support.GenericXmlApplicationContext@442d9b6e: startup date [Fri Jan 21 16:30:34 KST 2022]; root of context hierarchy
INFO : org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor - JSR-330 'javax.inject.Inject' annotation found and supported for autowiring
[사전 처리] getUser() 메소드 ARGS 정보: UserVO [ id = test, password = test123, name = null, role = null]
===> JDBC로 getUser() 기능 처리
관리자님 환영합니다.
INFO : org.springframework.context.support.GenericXmlApplicationContext - Closing org.springframework.context.support.GenericXmlApplicationContext@442d9b6e: startup date [Fri Jan 21 16:30:34 KST 2022]; root of context hierarchy

 

② After Returning 어드바이스 : @AfterReturning에서 pointcut 속성과 returning 속성 사용

- 어드바이스 클래스

package com.springbook.biz.common;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Service;

import com.springbook.biz.user.UserVO;

@Service
@Aspect
public class AfterReturningAdvice {
	@Pointcut("execution(* com.springbook.biz..*Impl.get*(..))")
	public void getPointcut() {
		
	}
	
	@AfterReturning(pointcut = "getPointcut()", returning = "returnObj")
	public void afterLog(JoinPoint jp, Object returnObj) {
		String method = jp.getSignature().getName();
		
		if(returnObj instanceof UserVO) {
			UserVO user = (UserVO) returnObj;
			if(user.getRole().equals("Admin")) {
				System.out.println(user.getName() + " 로그인(Admin)");
			}
		}	
		
		System.out.println("[사후 처리] " + method + "() 메소드 리턴값 : " + returnObj.toString());
	}
}

 

- 클라이언트 클래스 실행

INFO : org.springframework.beans.factory.xml.XmlBeanDefinitionReader - Loading XML bean definitions from class path resource [applicationContext.xml]
INFO : org.springframework.context.support.GenericXmlApplicationContext - Refreshing org.springframework.context.support.GenericXmlApplicationContext@442d9b6e: startup date [Fri Jan 21 16:34:18 KST 2022]; root of context hierarchy
INFO : org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor - JSR-330 'javax.inject.Inject' annotation found and supported for autowiring
===> JDBC로 getUser() 기능 처리
관리자 로그인(Admin)
[사후 처리] getUser() 메소드 리턴값 : UserVO [ id = test, password = test123, name = 관리자, role = Admin]
관리자님 환영합니다.
INFO : org.springframework.context.support.GenericXmlApplicationContext - Closing org.springframework.context.support.GenericXmlApplicationContext@442d9b6e: startup date [Fri Jan 21 16:34:18 KST 2022]; root of context hierarchy

 

③ After Throwing 어드바이스 : @AfterThrowing에서 pointcut 속성과 throwing 속성 사용

- 어드바이스 클래스

package com.springbook.biz.common;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Service;

@Service
@Aspect
public class AfterThrowingAdvice {
	@Pointcut("execution(* com.springbook.biz..*Impl.*(..))")
	public void allPointcut() {
		
	}
	
	@AfterThrowing(pointcut = "allPointcut()", throwing = "exceptObj")
	public void exceptionLog(JoinPoint jp, Exception exceptObj) {
		String method = jp.getSignature().getName();
				
		System.out.println("[예외 처리] " + method + "() 메소드 수행 중 발생된 예외 메시지 : " + exceptObj.getMessage());
		
		//발생하는 예외의 종류에 따라 메시지 지정 가능
		if(exceptObj instanceof IllegalArgumentException) {
			System.out.println("부적합한 값이 입력되었습니다.");
		} else if(exceptObj instanceof NumberFormatException) {
			System.out.println("숫자 형식의 값이 아닙니다.");
		} else if(exceptObj instanceof Exception) {
			System.out.println("문제가 발생했습니다.");
		}
	}
}

 

- 클라이언트 클래스 실행

INFO : org.springframework.beans.factory.xml.XmlBeanDefinitionReader - Loading XML bean definitions from class path resource [applicationContext.xml]
INFO : org.springframework.context.support.GenericXmlApplicationContext - Refreshing org.springframework.context.support.GenericXmlApplicationContext@442d9b6e: startup date [Fri Jan 21 16:39:49 KST 2022]; root of context hierarchy
INFO : org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor - JSR-330 'javax.inject.Inject' annotation found and supported for autowiring
[예외 처리] insertBoard() 메소드 수행 중 발생된 예외 메시지 : 0번 글은 등록할 수 없습니다.
부적합한 값이 입력되었습니다.
Exception in thread "main" java.lang.IllegalArgumentException: 0번 글은 등록할 수 없습니다.

 

④ After 어드바이스 : @After (예외 발생과 상관 없이 메소드 처리 후 무조건 동작)

- 어드바이스 클래스

package com.springbook.biz.common;

import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Service;

@Service
@Aspect
public class AfterAdvice {
	@Pointcut("execution(* com.springbook.biz..*Impl.*(..))")
	public void allPointcut() {
		
	}
	
	@After("allPointcut()")
	public void finallyLog() {
		System.out.println("[사후 처리] 비즈니스 로직 수행 후 무조건 동작");
	}
}

 

- 클라이언트 클래스 실행 (After와 AfterThrowing 동시 동작)

INFO : org.springframework.beans.factory.xml.XmlBeanDefinitionReader - Loading XML bean definitions from class path resource [applicationContext.xml]
INFO : org.springframework.context.support.GenericXmlApplicationContext - Refreshing org.springframework.context.support.GenericXmlApplicationContext@442d9b6e: startup date [Fri Jan 21 16:42:37 KST 2022]; root of context hierarchy
INFO : org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor - JSR-330 'javax.inject.Inject' annotation found and supported for autowiring
[예외 처리] insertBoard() 메소드 수행 중 발생된 예외 메시지 : 0번 글은 등록할 수 없습니다.
부적합한 값이 입력되었습니다.
Exception in thread "main" [사후 처리] 비즈니스 로직 수행 후 무조건 동작
java.lang.IllegalArgumentException: 0번 글은 등록할 수 없습니다.

 

⑤ Around 어드바이스 : @Around

- 어드바이스 클래스

package com.springbook.biz.common;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Service;
import org.springframework.util.StopWatch;

@Service
@Aspect
public class AroundAdvice {
	@Pointcut("execution(* com.springbook.biz..*Impl.*(..))")
	public void allPointcut() {
		
	}
	
	@Around("allPointcut()")
	public Object aroundLog(ProceedingJoinPoint pjp) throws Throwable {
		String method = pjp.getSignature().getName();
		
		StopWatch stopWatch = new StopWatch();
		stopWatch.start();
		
		Object obj = pjp.proceed();	//비즈니스 메소드 호출하는 메소드 .proceed()
		
		stopWatch.stop();
		System.out.println(method + "() 메소드 수행에 걸린 시간 : " + stopWatch.getTotalTimeMillis() + "(ms)초");
		return obj;
	}
}

 

- 클라이언트 클래스 실행

INFO : org.springframework.beans.factory.xml.XmlBeanDefinitionReader - Loading XML bean definitions from class path resource [applicationContext.xml]
INFO : org.springframework.context.support.GenericXmlApplicationContext - Refreshing org.springframework.context.support.GenericXmlApplicationContext@442d9b6e: startup date [Fri Jan 21 16:46:35 KST 2022]; root of context hierarchy
INFO : org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor - JSR-330 'javax.inject.Inject' annotation found and supported for autowiring
===> JDBC로 getUser() 기능 처리
getUser() 메소드 수행에 걸린 시간 : 132(ms)초
관리자님 환영합니다.
INFO : org.springframework.context.support.GenericXmlApplicationContext - Closing org.springframework.context.support.GenericXmlApplicationContext@442d9b6e: startup date [Fri Jan 21 16:46:35 KST 2022]; root of context hierarchy

7. 외부 포인트컷 클래스 참조하기

: 어노테이션 설정으로 변경하고부터 어드바이스 클래스마다 포인트컷 설정이 포함되면서, 비슷하거나 같은 포인트컷이 반복 선언되는 문제 발생 → 포인트컷을 외부에 독립된 클래스에 별도로 설정

 

- 사용할 모든 포인트컷을 등록해두는 별도의 클래스

package com.springbook.biz.common;

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

@Aspect
public class PointcutCommon {
	@Pointcut("execution(* com.springbook.biz..*Impl.*(..))")
	public void allPointcut() {
		
	}
	
	@Pointcut("execution(* com.springbook.biz..*Impl.get*(..))")
	public void getPointcut() {
		
	}
}

 

- 외부의 포인트컷 클래스를 이용하는 어드바이스 클래스 : (@Before("PointcutCommon.allPointcut()")으로 이용)

package com.springbook.biz.common;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Service;

@Service	//서비스 설정(객체 생성)
@Aspect		//애스팩트 설정 = Pointcut + Advise
public class BeforeAdvice {
	@Before("PointcutCommon.allPointcut()")	//어드바이스 설정
	public void beforeLog(JoinPoint jp) {
		String method = jp.getSignature().getName();	//클라이언트가 호출한 메소드의 이름 추출
		Object[] args = jp.getArgs();					//메소드 호출 시 넘겨준 인자 목록(사용한 값들)을 배열로 추출
		
		System.out.println("[사전 처리] " + method + "() 메소드 ARGS 정보: " + args[0].toString());
	}
}

 

- 클라이언트 클래스 실행

INFO : org.springframework.beans.factory.xml.XmlBeanDefinitionReader - Loading XML bean definitions from class path resource [applicationContext.xml]
INFO : org.springframework.context.support.GenericXmlApplicationContext - Refreshing org.springframework.context.support.GenericXmlApplicationContext@442d9b6e: startup date [Fri Jan 21 16:57:01 KST 2022]; root of context hierarchy
INFO : org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor - JSR-330 'javax.inject.Inject' annotation found and supported for autowiring
[사전 처리] getUser() 메소드 ARGS 정보: UserVO [ id = test, password = test123, name = null, role = null]
===> JDBC로 getUser() 기능 처리
관리자님 환영합니다.
INFO : org.springframework.context.support.GenericXmlApplicationContext - Closing org.springframework.context.support.GenericXmlApplicationContext@442d9b6e: startup date [Fri Jan 21 16:57:01 KST 2022]; root of context hierarchy