Recent Posts
Recent Comments
Link
06-30 12:53
Today
Total
관리 메뉴

삶 가운데 남긴 기록 AACII.TISTORY.COM

Spring AOP 본문

DEV&OPS/Java

Spring AOP

ALEPH.GEM 2023. 1. 16. 22:29

 

관점 지향(Aspect Oriented Programming)

  • 로깅
  • 보안/인증
  • 트랜잭션
  • 리소스 풀링
  • 에러 검사 / 처리
  • 정책
  • 멀티 스레드 안전 관리
  • 데이터 영속처리

위 항목들은 소프트웨어 개발 시 발생하는 공통적인 문제들입니다.

핵심 관심(core concern)은 업무 로직을 말합니다.

횡단 관심(cross-cutting concern)은 위에서 언급한 로깅이나 보안등 공통 시스템 로직을 말합니다. 

이처럼 업무 기능과 시스템 기능간에 결합성을 제거하도록 분리하여 작성하는 방법을 관점 지향이라고 합니다.

 

 

관점 지향 용어

  • advice : 관점이 언제, 무엇을 하는지를 정의
  • joinpoint : 관점이 실행될 수 있는 위치들
  • pointcut : joinpoint 중에서 advice 하는 위치(어디서)
  • aspect : advice + pointcut 즉, 무엇을 언제 어디서 하는 지를 정의
  • weaving : proxied object를 생성하여 aspect를 대상 객체에 적용
  • introduction : 기존 클래스에 새로운 메서드나 애트리뷰트를 추가하는 것

 

 

Advice

  • before : 메서드 호출 전 advice기능이 발생
  • after : 메서드 실행 완료 후 결과와 관계없이 advice 기능이 발생
  • after-returning : 메서드가 성공적으로 완료 후 advice 기능이 발생
  • after-throwing : 메서드가 예외를 발생 후 advice 기능이 발생
  • arount : 메서드가 호출되기 전과 후에 advice 기능이 발생

 

 

Spring AOP

pom.xml에 아래 2가지 모듈을 추가합니다.

		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-aop</artifactId>
			<version>${spring-framework.version}</version>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-aspects</artifactId>
			<version>${spring-framework.version}</version>
		</dependency>

 

 

로깅 기능 구현 예제

스프링 프로젝트의 기본 패키지에 aspect  패키지를 생성 후 aspect 클래스(여기서는 LoggingAspect)를 정의합니다.

@Component
public class LoggingAspect {
    private Logger log = LoggerFactory.getLogger(getClass());
	
    //advice 메서드
    //before advice
	public void logBefore(JoinPoint joinpoint) {
		String message = buildJoinpoint(joinpoint);
		message += "메서드 실행 전";
		log.info(message);
	}
    
    //...
}

advice는 joinpoint 타입이 인수로 전달됩니다.

아래 예제처럼 Joinpoint 인터페이스의 메서드들로 조인 포인트의 정보들을 구할 수 있습니다.

 

private String buildJoinpoint(JoinPoint joinpoint) {
    //joinpoint의 클래스 네임
    String className = joinpoint.getTarget().getClass().getName();
    
    //메서드의 시그니처 네임
    String methodName = joinpoint.getSignature().getName();
    
    String message = className + " 의 " + methodName + "( ";
    
    //joinpoint의 메서드 매개 변수 목록
    Object [] args = joinpoint.getArgs();
    
    for(int i = 0; i < args.length; ++i){
        Object arg  = args[i];
        message += arg.getClass().getTypeName();
        if(i != args.length - 1 )
            message += ", ";
    }
    
    message += " ) ";
    
    return message;
}

around advice의 경우는 ProceddingJoinPoint 타입의 매개 변수를 받는데  메서드 실행 전과 실행 후에 실행되기 때문에 joinpoint를 진행(proceed) 시킬 필요가 있습니다. 

	// around 어드바이스 
	public void logAround(ProceedingJoinPoint joinpoint) throws Throwable {
    	//시작
		String message = buildJoinpoint(joinpoint);
		message += "메서드 실행 시작";
		log.info(message);
		
        //메서드 호출 진행
        joinpoint.proceed();		 
		
        //종료
        message = buildJoinpoint(joinpoint);
		message += "메서드 실행 종료";

		log.info(message);
	}

 

 

aspect 설정

위에서 구현한 LoggingAspect 클래스를 Spring bean으로 설정합니다.

<?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"
	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.1.xsd
		http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.1.xsd">
	<context:component-scan base-package="컴포넌트베이스패키지경로(예:com.aacii.aspect)"/>
	 	<bean id="loggingAspect" class="클래스경로(예:com.aacii.aspect.LoggingAspect)"/>
 	<aop:config>
 		<aop:aspect ref="loggingAspect">
 			<aop:pointcut expression="execution(* 컴포넌트패키지경로.*.*.get*(..)) || execution(* 컴포넌트패키지경로.*.*.find*(..))" id="getLogging"/>
 			<aop:pointcut expression="execution(* 컴포넌트패키지경로.*.*.save*(..))" id="saveLogging"/>
 			<aop:before method="logBefore" pointcut-ref="getLogging"/>
 			<aop:around method="logAround" pointcut-ref="saveLogging"/>
 		</aop:aspect>
 	</aop:config>
</beans>

<aop:config> :  최상위 AOP element 

<aop:aspect> : 관점 설정

<aop:pointcut> : point 컷 설정

<aop:before> : before advice 설정

<aop:around> : around advice 설정

after, after-returning, after-throwing은 생략했습니다.

 

 

pointcut 설정

pointcut은 aop를 구현할 때 advice가 실행되는 위치를 지정합니다.

pointcut을 표현하는 expression은 아래와 같습니다.

execution(반환타입 메서드가속한패키지.*.*.메서드이름*(인수))
<!-- 예제 -->
execution(* com.aacii.*.*.get*(..))

execution() 지명자 외에도 within(), bean(), target(), this(), args() 들이 있지만 여기서는 생략합니다.

 

 

정리해봅시다.

예를들어 업무 로직이 exampleService.getAll() 이라면,

aspect(관점)은 xml의 <aop:before pointcut-ref="getLogging" method="logBefore" /> 등으로 설정하고,

시스템로직은 advice로 별도 클래스(LoggingAspect)를 만들어서 메서드(logBefore();)로 동작을 정의합니다.

 

 

@AspectJ

@AspectJ 어노테이션을 이용해서 XML 설정 없이 AOP를 구현할 수 있습니다.

위에서 언급한 용어대로 @Aspect, @Pointcut, @Before, @After, @Around 등을 이용해서 설정 할 수 있습니다.

aspect 클래스를 spring bean으로 설정하기 위해 @Componet 을 추가합니다.

@Aspect
@Component
public class LoggingAspect {
	
    // getLogging 포인트컷
	@Pointcut("execution(* com.aacii.ex.*.*.get*(..))" + " || execution(* com.aacii.ex.*.*.find*(..))")
	public void getLogging() {}
	
    // before 어드바이스
	@Before("getLogging()")
	public void logBefore(JoinPoint joinpoint) {
		String message = buildJoinpoint(joinpoint);
		message += "실행 시작";
		log.info(message);
	}
    //생략...
}

@Around 어노테이션은 괄호안에 포인트컷 표현식을 문자열로 설정합니다.

XML 때와 마찬가지로 ProceedingJoinPoint를 이용해야 하고, proceed()메서드를 호출해야 메서드를 실행시킵니다.

	// around 어드바이스 
	@Around("execution(* com.aacii.ex.*.*.save*(..))")
	public void logAround(ProceedingJoinPoint joinpoint) throws Throwable {
		long start = System.currentTimeMillis();
		String message = buildJoinpoint(joinpoint);
		message += "메서드 실행 시작";
		log.info(message);
        
		joinpoint.proceed();		// 메서드 호출 
        
		message = buildJoinpoint(joinpoint);
		message += "메서드 실행 종료";
		long end = System.currentTimeMillis();
        
		long duration = end - start;
		log.info("실행 시간 : " + duration + " 밀리초");
	}

이렇게 구현된 LoggingAspect 클래스를 관점 클래스로 만들기 위해 Spring context 를 지정한  xml 파일에 프록시 bean을 설정해서 @AspectJ 어노테이션이 지정된 bean을 프록시 어드바이스로 변환해야 합니다.

아래처럼 <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"
	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.1.xsd
		http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.1.xsd">
	<context:component-scan base-package="com.ensoa.order"/>	
	<aop:aspectj-autoproxy/>
</beans>

 

 

Advice에 매개 변수 전달

업무 로직을 구현한 메서드를 호출할 때 매개변수를 advice에서 가로채기 위해 매개 변수를 전달할 필요가 있을 수 있습니다.

아래 처럼 pointcut을 지정하는 execution() 표현식에서 매개변수를 지정해주고 사용합니다.

	// 매개변수 포인트 컷
	@Pointcut("execution(* com.aacii.ex.service.*.update*(com.aacii.ex.domain.Product))" + " && args(product)")
	public void updateLogging(Product product) {}    
    
	// 매개변수가 필요한 advice
	@Before("updateLogging(customer)")
	public void logBeforeUpdate(JoinPoint joinpoint, Product product) {
		String message = buildJoinpoint(joinpoint);
		message += "메서드 실행 시작";
		log.info(message);
	}

 

 

 

 

 

 

 

 

 

728x90