본문 바로가기
Java . Spring . Web . SQL

[AOP] 어노테이션 기반 AOP

by heidish 2020. 10. 19.
반응형

 

 

 

 

 

 

스프링 IoC와 같이 스프링 AOP도 어노테이션 설정을 지원한다.

 

 

AOP를 어노테이션으로 설정하려면 가장 먼저 스프링 설정 파일에

<aop:aspectj-autoproxy> 엘리먼트를 선언해야 한다.

 

 

 

( applicationContext.xml )

<?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:tx="http://www.springframework.org/schema/tx"
	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
		http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.2.xsd">

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

 

 

<aop:aspectj-autoproxy> 엘리먼트를 위와 같이 선언해주면,

스프링 컨테이너는 AOP 관련 어노테이션들을 자동으로 인식하고 용도에 맞게 처리해준다.

 

 

 


 

 

AOP 관련 어노테이션들은 어드바이스 클래스 내부에 설정해주면 된다.

 

 

이 때, 어드바이스 클래스에 선언된 어노테이션들을 스프링 컨테이너가 처리하게 해주려면

어드바이스 객체가 생성된 상태여야 한다 !

 

따라서 어드바이스 클래스는 반드시 스프링설정파일에서 <bean> 태그로 등록하거나,

@Service 어노테이션을 사용해서 컴포넌트가 검색될 수 있게 해줘야한다.

 

 

 

 

( 나는 @Service 어노테이션으로 객체 생성을 해줄꺼임..! )

 

 


 

 

1.  Pointcut (포인트컷) 설정

 

 

기존에는 포인트컷 설정을 xml 파일에서 <aop:pointcut> 태그를 사용했는데,

 

xml 설정 파일에서 말고 어노테이션 설정으로 포인트컷을 설정해줄 수 있다!

 

 

 

어노테이션 설정으로 포인트컷을 설정할 땐, @Pointcut 어노테이션을 사용한다.

 

하나의 어드바이스 클래스 내에 여러개의 Pointcut을 선언할 수 있는데,

이런 경우 여러개의 포인트컷을 식별하기 위한 식별자가 필요하며 이 때 참조 메소드를 사용한다.

 

 

 

참조메소드란 메소드 코드 내부가 비어있는, 구현 로직이 없는 메소드인데

구현 로직이 없기 때문에 어떠한 기능을 처리하는걸 목적으로 하는게 아니라 단순히 포인트컷 식별용으로만 사용된다.

 

 

 

( LogAdvice.java )

package com.heidi.biz.board.common;

import org.aspectj.lang.annotation.Pointcut;

public class LogAdvice {

	public LogAdvice() {
	}
	
	@Pointcut("execution(* com.heidi.biz..*Impl.*(..))")
	public void allPointCut() {}
	
	@Pointcut("execution(* com.heidi.biz..*Impl.get*(..))")
	public void getPointCut() {}
	
	public void printLog() {
		System.out.println("[공통 로그] 비즈니스 로직 수행 전 동작");
	}

}

위의 LogAdvice 라는 하나의 어드바이스 클래스에 있는 allPointcut(), getPointcut() 메소드를

각각 @Pointcut 어노테이션을 이용해서 포인트컷 설정을 해준거다.

 

현재 여러개의 포인트컷이 있기 때문에 구별하기 위해서 식별자를 붙여주었고,

앞서 말한 것 처럼 식별자로는 참조메소드를 사용했다.

( 내부가 텅 빈 메소드들.. )

 

 

 

 


 

 

2.  Advice (어드바이스) 설정

 

동작 시점을 설정해주는 어드바이스 열시 어노테이션으로 설정이 가능하고,

 

어드바이스 메소드가 결합될 포인트컷을 반드시 참조해야 한다 !

 

 

포인트컷을 참조할땐 어드바이스 어노테이션 뒤에 ( ) 내에 포인트컷 참조 메소드를 지정하면 된다.

 

 

어드바이스 동작 시점과 관련된 어노테이션은 아래와 같다.

 

@Before

@AfterReturning

@AfterThrowing

@After

@Around

 

 

 

( LogAdvice.java )

package com.heidi.biz.board.common;

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

public class LogAdvice {

	public LogAdvice() {
	}
	
	@Pointcut("execution(* com.heidi.biz..*Impl.*(..))")
	public void allPointCut() {}
	
	@Pointcut("execution(* com.heidi.biz..*Impl.get*(..))")
	public void getPointCut() {}
	
	@Before("allPointcut()")
	public void printLog() {
		System.out.println("[공통 로그] 비즈니스 로직 수행 전 동작");
	}

}

 

 


 

 

3.  Aspect (애스팩트) 설정

 

 

@Aspect 어노테이션을 이용해서 설정하며,

 

@Aspect 가 설정된 객체에는 반드시 포인트컷과 어드바이스를 결합해주는 설정이 필요하다 !

 

 

 

package com.heidi.biz.board.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 {

	public LogAdvice() {
	}
	
	@Pointcut("execution(* com.heidi.biz..*Impl.*(..))")
	public void allPointCut() {}
	
	@Pointcut("execution(* com.heidi.biz..*Impl.get*(..))")
	public void getPointCut() {}
	
	@Before("allPointcut()")
	public void printLog() {
		System.out.println("[공통 로그] 비즈니스 로직 수행 전 동작");
	}

}

 

 

 

 


 

 

지금부터는 이전에 XML 기반으로 설정했던 각 어드바이스들을 어노테이션으로 변경하자...

 

 

 


 

1.  Before 어드바이스

 

 

( BeforeAdvice.java )

package com.heidi.biz.board.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
public class BeforeAdvice {

	public BeforeAdvice() {
	}
	
	@Pointcut("execution(* com.heidi.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());
	}
}

 

 

( applicationContext.xml )

<?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:aop="http://www.springframework.org/schema/aop"
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:p="http://www.springframework.org/schema/p"
	xmlns:tx="http://www.springframework.org/schema/tx"
	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
		http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.2.xsd">

	<context:component-scan base-package="com.heidi.biz"></context:component-scan>

	<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
	
</beans>

 

 

( UserServiceClient.java )

package com.heidi.biz.user;

import org.springframework.context.support.AbstractApplicationContext;
import org.springframework.context.support.GenericXmlApplicationContext;

public class UserServiceClient {

	public static void main(String[] args) {
		// 1. Spring 컨테이너를 구동한다.
		AbstractApplicationContext container = new GenericXmlApplicationContext("applicationContext.xml");
				
		// 2. Spring 컨테이너로부터 userService 객체를 Lookup 한다.
		UserService userService = (UserService) container.getBean("userService");
		
		// 3. 로그인 기능 테스트
		UserVO vo = new UserVO();
		vo.setId("Admin");
		vo.setPassword("admin");
		
		UserVO user = userService.getUser(vo);
		if (user != null) {
			System.out.println(user.getName() + "님 환영합니다.");
		} else {
			System.out.println("로그인 실패");
		}
		
		// 4. Spring 컨테이너를 종료한다.
		container.close();
				
	}
}

 

 

실행 결과

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@7dc5e7b4: startup date [Mon Oct 19 17:19:56 KST 2020]; 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=Admin, password=admin, name=null, role=null]
==> JDBC로 getUser() 기능 처리
administrator님 환영합니다.
INFO : org.springframework.context.support.GenericXmlApplicationContext - Closing org.springframework.context.support.GenericXmlApplicationContext@7dc5e7b4: startup date [Mon Oct 19 17:19:56 KST 2020]; root of context hierarchy

 

 


 

2.  After Returning 어드바이스

 

 

 

( AfterReturningAdvice.java )

package com.heidi.biz.board.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.heidi.biz.user.UserVO;

@Service
@Aspect
public class AfterReturningAdvice {

	public AfterReturningAdvice() {
	}
	
	@Pointcut("execution(* com.heidi.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());
	}
	
}

 

 

( applicationContext.xml )

동일

 

 

( UserServiceClient.java )

동일

 

 

실행 결과

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@7dc5e7b4: startup date [Mon Oct 19 17:27:05 KST 2020]; root of context hierarchy
INFO : org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor - JSR-330 'javax.inject.Inject' annotation found and supported for autowiring
==> JDBC로 getUser() 기능 처리
administrator 로그인(Admin)
[사후 처리] getUser() 메소드 리턴값 : UserVO [id=Admin, password=admin, name=administrator, role=Admin]
administrator님 환영합니다.
INFO : org.springframework.context.support.GenericXmlApplicationContext - Closing org.springframework.context.support.GenericXmlApplicationContext@7dc5e7b4: startup date [Mon Oct 19 17:27:05 KST 2020]; root of context hierarchy

 

 


 

3.  After Throwing 어드바이스

 

 

 

( AfterThrowingAdvice.java )

package com.heidi.biz.board.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 {

	public AfterThrowingAdvice() {
	}
	
	@Pointcut("execution(* com.heidi.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 +"() 메소드 수행 중 예외 발생! ");

		if(exceptObj instanceof IllegalArgumentException){
			System.out.println("부적합한 값이 입력되었습니다.");
		}else if(exceptObj instanceof NumberFormatException){
			System.out.println("숫자 형식의 값이 아닙니다.");
		}else if (exceptObj instanceof Exception) {
			System.out.println("문제가 발생했습니다.");
		}
	}
}

 

 

( BoardServiceImpl.java )

insertBoar() 메소드 내부 if문 살려놓기

	@Override
	public void insertBoard(BoardVO vo) {
		if (vo.getSeq() == 0) {
			throw new IllegalArgumentException("0번 글은 등록할 수 없습니다.");
		}
		this.boardDAO.insertBoard(vo);
	}

 

 

( BoardServiceClient.java )

package com.hedidi.biz.board;

import java.util.List;

import org.springframework.context.support.AbstractApplicationContext;
import org.springframework.context.support.GenericXmlApplicationContext;

import com.heidi.biz.board.BoardService;
import com.heidi.biz.board.BoardVO;

public class BoardServiceClient {

	public static void main(String[] args) {
		// 1. Spring 컨테이너를 구동한다.
		AbstractApplicationContext container = new GenericXmlApplicationContext("applicationContext.xml");
		
		// 2. Spring 컨테이너로부터 boardService 객체를 Lookup 한다.
		BoardService boardService = (BoardService) container.getBean("boardService");
		
		// 3. 글 등록 기능 테스트
		BoardVO vo = new BoardVO();
		vo.setTitle("임시 제목");
		vo.setWriter("heidi");
		vo.setContent("임시 내용...");
		boardService.insertBoard(vo);
	
		// 4. 글 목록 검색 기능 테스트
		List<BoardVO> boardList = boardService.getBoardList(vo);
		for (BoardVO board : boardList) {
			System.out.println("==> " + board.toString());
		}
		
		// 5. Spring 컨테이너 종료
		container.close();
		
	}
}

 

 

실행 결과

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@7dc5e7b4: startup date [Mon Oct 19 17:40:57 KST 2020]; root of context hierarchy
INFO : org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor - JSR-330 'javax.inject.Inject' annotation found and supported for autowiring
[사후 처리] insertBoard() 메소드 수행 중 예외 발생! 
부적합한 값이 입력되었습니다.
Exception in thread "main" java.lang.IllegalArgumentException: 0번 글은 등록할 수 없습니다.

 

 

 


 

 

4.  After 어드바이스

 

 

 

( AfterAdvice.java )

package com.heidi.biz.board.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 {

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

 

 

( BoardServiceClient.java )

 

 

실행 결과

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@7dc5e7b4: startup date [Mon Oct 19 17:44:31 KST 2020]; root of context hierarchy
INFO : org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor - JSR-330 'javax.inject.Inject' annotation found and supported for autowiring
[사후 처리] insertBoard() 메소드 수행 중 예외 발생! 
부적합한 값이 입력되었습니다.
[사후 처리] 비즈니스 로직 수행 후 무조건 동작
Exception in thread "main" java.lang.IllegalArgumentException: 0번 글은 등록할 수 없습니다.

 

 

 


 

5.  Around 어드바이스

 

 

 

( AroundAdvice.java )

package com.heidi.biz.board.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 {

	public AroundAdvice() {
	}

	@Pointcut("execution(* com.heidi.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();
		
		System.out.println("[BEFORE] : ...");
		
		Object obj = pjp.proceed();
		
		System.out.println("[AFTER] : ...");

		stopWatch.stop();
		
		System.out.println(method + "() 메소드 수행에 걸린 시간 : "+ stopWatch.getTotalTimeMillis()+"(ms)초");
		return obj;
	}
	
}

 

 

( BoardServiceImpl.java )

insertBoard() 내부 if문 주석처리하기

 

 

( UserServiceClient.java )

동일

 

 

 

실행 결과

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@7dc5e7b4: startup date [Mon Oct 19 17:49:27 KST 2020]; root of context hierarchy
INFO : org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor - JSR-330 'javax.inject.Inject' annotation found and supported for autowiring
[BEFORE] : ...
==> JDBC로 getUser() 기능 처리
[AFTER] : ...
getUser() 메소드 수행에 걸린 시간 : 738(ms)초
[사후 처리] 비즈니스 로직 수행 후 무조건 동작
administrator님 환영합니다.
INFO : org.springframework.context.support.GenericXmlApplicationContext - Closing org.springframework.context.support.GenericXmlApplicationContext@7dc5e7b4: startup date [Mon Oct 19 17:49:27 KST 2020]; root of context hierarchy

 

 

 

 

 



 

 

"  외부 Pointcut 참조  "

 

 

이 전까지 XML 설정을 통해 포인트컷을 설정해줄때는 여러개의 포인트컷을 등록할 수 있었는데,

어노테이션 기반으로 포인트컷을 성정하게 되면서는 각 어드바이스 클래스마다 포인트컷 설정을 해줘야했고,

비슷하거나 동일한 이름의 포인트컷이 반복설정되는 문제가 있었다.

 

스프링에서는 이런 문제점을 피하기 위해서,

포인트컷을 모두 모아서 외부에 독립된 클래스에 따로 설정해준다 !

 

 

 

( PointCutCommon.java )

package com.heidi.biz.board.common;

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

@Service
@Aspect
public class PointCutCommon {

	public PointCutCommon() {
	}

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

 

 

이렇게 외부 클래스에 포인트컷을 정의해놓은 뒤,

해당 포인트컷들을 사용하려면 어드바이스 클래스 내부 코드를 수정해야한다.

 

 

 

( BeforeAdvice.java )

package com.heidi.biz.board.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
public class BeforeAdvice {

	public BeforeAdvice() {
	}
	
	@Pointcut("execution(* com.heidi.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());
	}
}

위와 같이 기존의 코드에서 @Pointcut, @Before 어노테이션 설정을 해준 부분에서,

 

package com.heidi.biz.board.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
public class BeforeAdvice {

	public 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());
	}
}

이렇게 바꿔줘야한다 !

 

 

 

또한 바인드변수가 존재하는 경우에는 아래와 같다.

( AfterReturningAdvice.java )

package com.heidi.biz.board.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.heidi.biz.user.UserVO;

@Service
@Aspect
public class AfterReturningAdvice {

	public AfterReturningAdvice() {
	}
	
	@AfterReturning(pointcut="PointCutCommon.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());
	}
	
}

 

 

 

 

 

반응형

댓글