구리

TIL_210618_어노테이션 기반 AOP 본문

SPRING FRAMEWORK

TIL_210618_어노테이션 기반 AOP

guriguriguri 2021. 6. 18. 14:55

목차

 

 

AOP 설정

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

스프링 설정 파일에 <aop:aspectj-autoproxy> 엘리먼트 선언

해당 엘리먼트를 선언함으로써 스프링 컨테이너는 AOP 관련 어노테이션들을 인식하고 용도에 맞게 처리해줍니다.

 

2. 포인트컷 설정

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(){}
}

 

3. 어드바이스 설정

어드바이스 메소드가 언제 동작할지 결정하여 관련된 어노테이션을 메소드 위에 설정하면 됩니다.

 

예시

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(){}
	
	@Pointcut("execution(* com.springbook.biz..*Impl.get*(..))")
	public void getPointcut(){}
	
	@Before("allPointcut()")
	public void printLog(){
		System.out.println("[공통 로그] 비즈니스 로직 수행 전 동작");
	}
}

위 설정은 allPointcut() 참조 메소드로 지정한 비즈니스 메소드가 호출될 때, 어드바이스 메소드인 printLog() 메소드가 Before 형태로 동작하도록 설정한 것입니다.

 

4. 애스팩트 설정

@Aspect가 설정된 애스팩트 객체에는 반드시 포인트컷과 어드바이스를 결합하는 설정이 있어야 합니다.

 

예시

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("allPointcut()") // 어드바이스
	public void printLog(){
		System.out.println("[공통 로그] 비즈니스 로직 수행 전 동작");
	}
}

@Aspect 어노테이션으로 인해 스프링 컨테이너는 LogAdvice 객체를 애스팩트 객체로 인식하고 @Pointcut, @Before 어노테이션에 의해 위빙이 처리됩니다.

위 소스는 allPointcut() 메소드로 지정한 포인트컷 메소드가 호출될 때, printLog()라는 어드바이스 메소드가 실행되도록 설정한 것입니다. 그리고 @Before가 설정되었으므로 printLog() 메소드는 사전 처리 형태로 동작합니다.

 

 

어드바이스 동작 시점

 

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

 

AfterReturning 어드바이스

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

getUser() 메소드 후 @AfterRetuning 어노테이션으로 인해 afterLog() 메소드가 실행된 결과로 @AfterRetuning 에 pointcut 속성을 추가하여 포인트컷을 참조한 이유는 비즈니스 메소드 수행 결과를 받아내기 위해 returning 속성을 이용해 바인드 변수를 지정했기 때문입니다.

 

AfterThrowing 어드바이스

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 + "() 메소드 수행 중 예외 발생!");
		
		if(exceptObj instanceof IllegalArgumentException){
			System.out.println("부적합한 값이 입력되었습니다");
		}else if(exceptObj instanceof NumberFormatException){
			System.out.println("숫자 형식의 값이 아닙니다");
		}else if(exceptObj instanceof Exception){
			System.out.println("문제가 발생했습니다");
		}
	}
}
package com.springbook.biz.board.impl;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

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

@Service("boardSerivce")
public class BoardServiceImpl implements BoardService {
	@Autowired
	private BoardDAOSpring boardDAO;
	
	public BoardServiceImpl() {
	}

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

	public List<BoardVO> getBoardList(BoardVO vo) {
		return boardDAO.getBoardList(vo);
	}
}

위와 같이 BoardServiceImpl 클래스의 insertBoard() 에 임의로 예외가 발생하게 설정해놓은 후 xml 파일을 실행하면 위 사진처럼 출력됩니다. @AfterThrowing 도 마찬가지로 throwing 속성을 이용한 바인딩 변수를 사용해야 하기에 pointcut 속성을 사용하여 포인트컷을 지정하였습니다.

 

 

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("[사후 처리] 비즈니스 로직 수행 후 무조건 동작 ");
	}
}

예외가 발생해도 finallyLog() 메소드가 실행되는 것을 볼 수 있습니다. 그리고 finallyLog() 메소드에 바인드 변수가 없으므로 @After 어노테이션은 포인트컷 메소드만  참조하면 됩니다.

 

 

 

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();
		
		stopWatch.stop();
		
		System.out.println(method + "() 메소드 수행에 걸린 시간 : " + stopWatch.getTotalTimeMillis() + "(ms)초");

		return obj;
	}
}

UserServiceClient 클래스에서 실행하게 되면 다음과 같은 결과를 얻게 되는데 클라이언트가 호출한 비즈니스 메소드 실행 전 후로 arondLog() 메소드가 실행된 것을 알 수 있습니다.

참고로 Around 어드바이스 메소드에서만 ProceedingJoinPoint 객체를 매개변수로 받아야 하는 것이 핵심입니다.

(proceed() 메소드를 이용해 클라이언트가 호출한 비즈니스 메소드를 실행할 수 있기 때문)

 

 

외부 Pointcut 참조하기

어노테이션으로 설정시 어드바이스 클래스마다 포인트컷 설정이 포함되기에, 중복되는 포인트컷이 반복 선언되는 문제가 생깁니다.

이를 해결하기 위해 모든 포인트컷을 특정 클래스에 설정 후 어드바이스 클래스에서 어노테이션 참조값을 변경합니다.

 

- 모든 포인트컷을 선언하는 PointcutCommon 클래스 

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 어드바이스 클래스 어노테이션 변경

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

원래는 각 어드바이스 클래스에 포인트컷 메소드를 일일이 생성하였다면 이제는 한 클래스에 모든 포인트컷을 선언하고 각 어드바이스 클래스의 메소드에는 @어노테이션에 (포인트컷 클래스명.포인트컷 메소드명) 이와 같은 방식으로 명시하면 됩니다.

바인드 변수가 있을 때도 포인트컷 클래스의 메소드를 참조하는 것은 같습니다.

 

AfterReturning 어드바이스 코드 예시

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.board.UserVO;

@Service
@Aspect
public class 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());
	}
}