1. AOP ( Aspect Oriented Programming )

AOP란 OOP와 상반되는 개념이 아니라 OOP를 OOP답게 프로그래밍 하기 위한 프로그래밍 기법입니다.

관점에 따라 프로그래밍을 한다고 해서 관점 지향 프로그래밍이라고 합니다.

AOP의 가장 기초가 되는 개념은 핵심 관심만 집중할 수 있도록, 필요하지만 중복 해서 작성해야 하는 핵심 이외의 코드들은 외부로 빼놓는 것입니다.



위의 그림과 같이 여러 객체에서 공통적으로 작성해야 하는 부분들을 횡단 관심이라고 하는데, 이 횡단 관심을 제거하여 핵심 관심( 비즈니스 로직 )만 집중할 수 있도록 하고자 하는 프로그래밍 기법이 AOP입니다.


AOP에서는 핵심 관심 모듈의 중간 중간에 필요한 횡단 관심 모듈을 직접 호출하지 않고 위빙(Weaving)이라 불리는 작업을 해서 횡단 관심 코드가 삽입되도록 합니다.

따라서 핵심 관심 모듈에서는 횡단 관심 모듈이 무엇인지조차 인식할 필요가 없습니다.





2. 용어

예제를 보기 전에, AOP에서 사용하는 용어를 먼저 알아보려고 합니다.

예제에서 코드로 나타나는 용어들이므로 살펴보시기를 권합니다.


위빙을 하기 위해서는 어디에, 언제, 어떤 코드를 삽입할 지가 중요합니다.

이를 Pointcut , Joinpoint , Advice라고 하는데, 각각의 용어들을 살펴보겠습니다.



1) Pointcut

pointcut은 어느 부분( Where )에 횡단 관심 모듈을 삽입 할 것인지 정의합니다.

자바 프로그래밍은 메서드의 호출로 실행되기 때문에 메서드에 횡단 관심 모듈을 삽입합니다.

[ 제어자, 반환타입, 패키지, 클래스, 이름, 파라미터, 예외 ]를 작성하여 pointcut을 정의 합니다.



2) Joinpoint

joinpoint는 언제( When ) 횡단 관심 모듈을 삽입할지를 정의합니다.

joinpoint의 시점은 아래와 같습니다.

1) 메서드 실행 전 ( before )

2) 메서드 실행 후 ( after )

3) 반환된 후 ( AfterReturning )

4) 예외가 던져지는 시점 ( AfterThrowing )

5) 메서드 실행 전, 후 ( around )


보시는 바와 같이 pointcut이 메서드이기 때문에, 메서드를 기준으로 joinpoint가 정의됩니다.

스프링에서는 위의 5가지 경우에 대해서 어노테이션을 제공합니다.



3) Advice

advice는 핵심 관심 모듈에 삽입 될 횡단 관심 모듈 자체( What )를 의미합니다.






3. 환경 설정

이제 예제를 살펴보면서 위의 용어들과 함께 기본적인 사용 방법을 알아보도록 하겠습니다.


pom.xml

<!-- spring aspect -->

<dependency>

        <groupId>org.springframework</groupId>

        <artifactId>spring-aspects</artifactId>

        <version>${org.springframework-version}</version>

</dependency>



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:tx="http://www.springframework.org/schema/tx"         xmlns:context="http://www.springframework.org/schema/context"

        xsi:schemaLocation="http://www.springframework.org/schema/beans

http://www.springframework.org/schema/beans/spring-beans.xsd

http://www.springframework.org/schema/tx    http://www.springframework.org/schema/tx/spring-tx.xsd

http://www.springframework.org/schema/aop    http://www.springframework.org/schema/aop/spring-aop.xsd

http://www.springframework.org/schema/context    http://www.springframework.org/schema/context/spring-context.xsd">


        <context:annotation-config />

        <context:component-scan base-package="com.victolee.aoptest">

               <context:include-filter type="annotation"

                       expression="org.springframework.stereotype.Repository" />

               <context:include-filter type="annotation"

                       expression="org.springframework.stereotype.Service" />

               <context:include-filter type="annotation"

                       expression="org.springframework.stereotype.Component" />

        </context:component-scan>



        <aop:aspectj-autoproxy />

</beans>

AOP에는 여러가지 방식이 있는데, <aop:aspectj-autoproxy /> 이 방식은 proxy를 두는 방식입니다.


proxy란 어떤 핵심 기능을 수행 하기 전과 후에 공통 기능을 수행한다고 할 때, aspect가 곧바로 핵심 기능에서 실행되는 것이 아니라, proxy(대행자)에서 공통 기능이 수행하도록 하는 것입니다.

즉 proxy는 핵심 기능을 하는 메서드에서 핵심 기능을 수행하고 다시 돌아와서 공통 기능을 수행하는 로직을 거치게 됩니다.


개발을 하면서 어디에 횡단 관심 모듈을 삽입할 지 알 수 없으므로 spring-servlet.xml 파일에도 <aop:aspectj-autoproxy /> 를 추가하는 것이 좋습니다.





4. MyAspect

이제 Pointcut( where ) , Joinpoint( when ), Advice( what )을 구현한 클래스를 작성해보겠습니다.

com.victolee.aoptest / MyAspect.java

@Aspect
@Component
public class MyAspect {
	// execution() : joinpoint에 함수를 실행한다.
	// 제어자 : public
	// 반환타입 : ProductVO
	// 패키지 : com.victolee.aoptest
	// 클래스 : ProductServcie
	// 메서드 : find
	// 예외 던지기 생략 가능
	@Before("execution(public ProductVO com.victolee.aoptest.ProductService.find(..) )") // joinpoint 지정
	public void beforeAdvice() {
		System.out.println("beforeAdvice() called");
	}
	
	
	// 접근제어자 생략 가능
	// 반환타입 : 모든 타입
	// 패키지 : com.victolee.aoptest
	// 클래스 : 패키지 내의 모든 클래스
	// 메서드 : find
	@After("execution(* com.victolee.aoptest.*.find(..) )")
	public void afterAdvice() {
		System.out.println("afterAdvice() called");
	}
	
	
	// 모든 패키지 내 ProductServcie 클래스의 모든 메서드
	@AfterReturning("execution(* *..ProductService.*(..))")
	public void afterReturningAdvice() {
		System.out.println("afterReturningAdvice() called");
	}
	
	
	// 모든 패키지의 모든 클래스의 모든 메서드
	// ProductService에서 예외를 던질 경우 그 예오를 받는다.
	// 예를들어, if(false) throw new RuntimeException("ProductService Exception 발생 ! ");
	@AfterThrowing(value="execution(* *..*.*(..))", throwing="ex")
	public void afterThrowingAdvice(Throwable ex) {
		System.out.println("afterThrowingAdvice() called: " + ex);
	}
	
	
	// pjp를 통해 "핵심 모듈의 메서드"의 실행이 가능
	// aoptest 패키지의 모든 클래스 모든 메서드 ( execution은 일반적으로 이렇게 쓰인다. )
	@Around("execution(* *..aoptest.*.*(..))")
	public Object aroundAdvice(ProceedingJoinPoint pjp) throws Throwable {
		// before advice
		System.out.println("aroundAdvice(): before");
		
		// proceed() 메서드 호출 => 실제 비즈니스
		// 비즈니스가 리턴하는 객체가 있을 수 있으므로 Obejct로 받아준다.
		Object result = pjp.proceed();
		
		// after advice
		System.out.println("aroundAdvice(): after");
		
		return result;
	}
}

관점(Aspect)을 정의하기 위한 클래스를 작성해보았습니다.

이 클래스를 스캔할 수 있도록 클래스 범위에 @Aspect 어노테이션을 추가했습니다.


@Before , @After , @AfterReturning , @AfterThrowing , @Around는 joinPoint입니다.

즉 언제 횡단 관심 모듈이 실행될 것인지를 어노테이션으로 작성해줍니다.

각 어노테이션의 의미는 도입부에서 말씀드린 용어 설명 부분을 참고해주세요


그리고 어노테이션안()에 pointcut을 작성합니다.

즉 어디에 횡단 관심 모듈이 삽입될 것인지를 명시해줘야 합니다.

경로명을 유연하게 작성할 수 있는 방법에 대해서는 주석을 참고해주세요.


마지막으로 메서드의 내부는 Adivce로서, 횡단 관심 모듈이 실행되어야 하는 코드가 작성되어 있습니다.



실제로 가장 많이 쓰이는 joinpoint는 @Around입니다.

@Around는 @Before와 @After가 합쳐진 것인데, 매개변수로 ProeedingJoinPoint 객체를 받습니다.

ProeedingJoinPoint 객체는 핵심 관심 모듈에 대한 정보를 갖고 있으며,

@Around 내부에서는 before와 after를 나누는 기준이 됩니다.





5. DAO 실행시간 측정 예제

이제 실제로 어떻게 활용할 수 있을지 DAO의 쿼리 수행시간을 측정해보도록 하겠습니다.

스프링에서는 StopWatch라는 클래스를 제공해주는데, 이 객체를 이용해서 실행 시간을 측정 해보겠습니다.

[ AOP 적용 전 ]

public UserVO get(UserVO vo){
	// 스프링에서 지원하는 StopWatch라는 클래스가 있음
	StopWatch sw = new StopWatch();
	sw.start();
	
	UserVO result = sqlSession.selectOne("user.getByEmailAndPwd", vo);
	
	sw.stop();
	Long time = sw.getTotalTimeMillis();
	
	return result;
}

모든 DAO 마다 위와 같이 start() , stop() 메서드를 호출하는 작업을 하는 것은 바람직하지 않으므로, 시간을 측정하는 코드는 횡단 관심 모듈로 분리하여 외부에서 자동적으로 처리할 수 있도록 할 것입니다.




com.victolee.aoptest/ MeasureExecutionTimeAspect.java

@Aspect
@Component
public class MeasureExecutionTimeAspect {
	
	// repository에 있는 모든 클래스의 메서드
	@Around("execution(* *..repository.*.*(..))")
	public Object aroundAdvice( ProceedingJoinPoint pjp) throws Throwable {
		// before advice
		StopWatch sw = new StopWatch();
		sw.start();
		
		Object result = pjp.proceed();
		
		// after advice
		sw.stop();
		Long total = sw.getTotalTimeMillis();
		
		// 어떤 클래스의 메서드인지 출력하는 정보는 pjp 객체에 있다.
		String className = pjp.getTarget().getClass().getName();
		String methodName = pjp.getSignature().getName();
		String taskName = className + "." + methodName;
		
		
		// 실행시간은 로그로 남기는 것이 좋지만, 여기서는 콘솔창에 찍도록 한다.
		System.out.println("[ExecutionTime] " + taskName + " , " + total + "(ms)");
		
		return result;
	}
}

joinpoint가 @Around 이며, repository 패키지에 있는 모든 클래스의 메서드에 대해서 위의 Advice가 실행됩니다.

ProceedingJoinPoint 객체에는 핵심 관심 모듈의 정보가 있다는 것을 언급하기 위해서 여러 정보들을 출력해보았습니다.


정리하면, 환경 설정을 통해 @Aspect 어노테이션을 스캔 할 수 있도록 해주고, 위와 같이 관점을 정의하면 repository 패키지에 있는 모든 클래스의 메서드에 대해서 횡단 관심 모듈이 실행될 것입니다.




이상으로 AOP가 무엇이고, 어떻게 활용할 수 있을지에 대해 알아보았습니다.

AOP 방식의 가장 큰 장점은 핵심 부분을 건드리지 않으면서 중복 코드를 제거할 수 있다는 것입니다.


댓글 펼치기 👇
  1. Favicon of https://mattmk.tistory.com Mattmk 2020.06.03 16:35 신고

    글을 읽으면서 이해가 너무 잘되었습니다.
    이렇게 좋은글 올려주셔서 정말 감사드립니다 :)