코린이의 기록

[Spring] Spring 4 - 스프링 AOP(Aspect Oriented Programming) 구현 본문

Framework/Spring

[Spring] Spring 4 - 스프링 AOP(Aspect Oriented Programming) 구현

코린이예요 2018. 5. 3. 12:07
반응형

※ 이 포스팅은 "초보웹 개발자를 위한 스프링4 프로그래밍 입문" 책을 기반으로 작성하였습니다.

0. 용어 설명

Aspect : 공통 기능을 제공
Advice 언제/어디(=PointCut)에 Aspect를 적용할지 설정
Proxy : 핵심기능의 실행은 다른 객체에 위임하고 부가적인 기능을 제공하는 객체를 프록시라고 부른다.
JoinPoint : Advice를 적용 가능한 지점.
PointCut :  JoinPoint의 부분집합으로 실제 Advice가 적용되는 JoinPoint를 나타냄
Weaving : Advice를 핵심 로직코드에 적용하는 것


AOP는 Aspect Oriented Programming 의 약자로 여러 객체에 공통적으로 적용할 수 있는 기능을 구분함으로써 재 사용성을 높여주는 프로그래밍 기법이다. 쉽게 말하자면 핵심 기능에 공통 기능을 삽입하는 것이다. 즉, 핵심 기능의 코드를 수정하지 않으면서 공통 기능의 구현을 추가하는 개념이다. 

1. AOP 이해를 높이기 위한 예제

1.1 AOP를 사용하기 전
factorial의 기능 두가지를 구하고 각각의 실행시간을 구하는 클래스를 구현하는 예제이다.

Caclulator.java - factorial을 구하기 위한 인터페이스 정의
ImpeCalculator.java - Calculator 인터페이스 구현 1: for문을 위한 factorial 구하기
RecCalculator.java - Calculator 인터페이스 구현 2: 재귀 호출을 이용한 factorial 구하기

Calculator.java

package chap07; public interface Calculator { public long factorial(long num); }

ImpeCalculator.java

package chap07; public class ImpeCalculator implements Calculator { @Override public long factorial(long num){ long start = System.currentTimeMillis(); long result = 1; for(int i=1; i <= num; i++){ result*=i; } long end = System.currentTimeMillis(); System.out.printf("ImpeCalculator.factorial(%d) 실행시간 = %d\n", num, (end-start)); return result; } }

for문을 돌면서 result 값을 구한다. for문을 돌기 전과 후에 System.currentTimeMillis()메소드를 호출함으로써 실행 시간을 구할 수 있다. 

RecCalculator.java

package chap07; public class RecCalculator implements Calculator { @Override public long factorial(long num){ long start = System.currentTimeMillis(); try{ if(num ==0 ) return 1; else return num*factorial(num-1); }finally{ long end = System.currentTimeMillis(); System.out.printf("RecCalculator.factorial(%d) 실행시간 = %d\n", num, (end-start)); } } }

재귀 함수 호출 방식으로는 for문 방식과 다르게 재귀 할때마다 실행 시간이 계산된다. 예를들어 factorial(5);를 구하고자할 때, 총 5번의 factorial 메소드가 호출되기 때문에 5번의 실행시간이 계산된다. 

main.java

package main; import chap07.Calculator; import chap07.ImpeCalculator; import chap07.RecCalculator; public class MainProxy { public static void main(String[] args) { Calculator imCall = new ImpeCalculator(); System.out.println("result : " + imCall.factorial(20)); Calculator reCall = new RecCalculator(); System.out.println("result : " + reCall.factorial(20)); } }

결과 화면 (밀리초)

ImpeCalculator.factorial(20) 실행시간 = 0 result : 2432902008176640000 RecCalculator.factorial(0) 실행시간 = 0 RecCalculator.factorial(1) 실행시간 = 0 RecCalculator.factorial(2) 실행시간 = 1 RecCalculator.factorial(3) 실행시간 = 1 RecCalculator.factorial(4) 실행시간 = 2 RecCalculator.factorial(5) 실행시간 = 2 RecCalculator.factorial(6) 실행시간 = 2 RecCalculator.factorial(7) 실행시간 = 3 RecCalculator.factorial(8) 실행시간 = 3 RecCalculator.factorial(9) 실행시간 = 3 RecCalculator.factorial(10) 실행시간 = 4 RecCalculator.factorial(11) 실행시간 = 4 RecCalculator.factorial(12) 실행시간 = 5 RecCalculator.factorial(13) 실행시간 = 5 RecCalculator.factorial(14) 실행시간 = 6 RecCalculator.factorial(15) 실행시간 = 6 RecCalculator.factorial(16) 실행시간 = 6 RecCalculator.factorial(17) 실행시간 = 7 RecCalculator.factorial(18) 실행시간 = 8 RecCalculator.factorial(19) 실행시간 = 8 RecCalculator.factorial(20) 실행시간 = 9 result : 2432902008176640000

결과 화면 (나노초)

ImpeCalculator.factorial(20) 실행시간 (nano)= 705 result : 2432902008176640000 RecCalculator.factorial(0) 실행시간 (nano)= 0 RecCalculator.factorial(1) 실행시간 (nano)= 279978 RecCalculator.factorial(2) 실행시간 (nano)= 570181 RecCalculator.factorial(3) 실행시간 (nano)= 1068076 RecCalculator.factorial(4) 실행시간 (nano)= 1523305 RecCalculator.factorial(5) 실행시간 (nano)= 1815977 RecCalculator.factorial(6) 실행시간 (nano)= 2194688 RecCalculator.factorial(7) 실행시간 (nano)= 2656264 RecCalculator.factorial(8) 실행시간 (nano)= 2992661 RecCalculator.factorial(9) 실행시간 (nano)= 3435900 RecCalculator.factorial(10) 실행시간 (nano)= 3821663 RecCalculator.factorial(11) 실행시간 (nano)= 4327669 RecCalculator.factorial(12) 실행시간 (nano)= 4829090 RecCalculator.factorial(13) 실행시간 (nano)= 5185586 RecCalculator.factorial(14) 실행시간 (nano)= 5815713 RecCalculator.factorial(15) 실행시간 (nano)= 6408461 RecCalculator.factorial(16) 실행시간 (nano)= 6996274 RecCalculator.factorial(17) 실행시간 (nano)= 9554510 RecCalculator.factorial(18) 실행시간 (nano)= 9747744 RecCalculator.factorial(19) 실행시간 (nano)= 9939216 RecCalculator.factorial(20) 실행시간 (nano)= 10183227 result : 2432902008176640000

여기서 "우리는 실행 시간을 구하는 기능"과 같이 공통적인 기능을 각각의 factorial 인터페이스 구현 클래스에서 실행하였다. 이때, 밀리초 단위가 아닌 나노초 단위로 실행시간을 구하는 것으로 소스를 수정하려면 어떻게 해야할까? 
-> System.currentTimeMillis()에 해당하는 부분을 각각 System.nanoTime(); 으로 수정해주어야 한다. 
이러한 중복작업을 하지 않고 수정하는 방법이 AOP 방식이다. 
이러한 중복작업을 하지 않기위하여 프록시 객체를 사용할 것이다.

1.2 AOP를 사용한 후 
ImpeCalculator.java

package chap07; public class ImpeCalculator implements Calculator { @Override public long factorial(long num) { long result = 1; for (long i = 1; i <= num; i++) { result *= i; } return result; } }

RecCalculator.java

package chap07; public class RecCalculator implements Calculator { @Override public long factorial(long num) { if (num == 0) return 1; else return num * factorial(num - 1); } }

ImpeCalculator와 RecCalculator에서는 실행 시간을 구하는 부분이 삭제되었다. 

ExeTimeCalculator.java

package chap07; public class ExeTimeCalculator implements Calculator { private Calculator delegate; public ExeTimeCalculator(Calculator delegate) { this.delegate = delegate; } @Override public long factorial(long num) { long start = System.nanoTime(); long result = delegate.factorial(num); long end = System.nanoTime(); System.out.printf("%s.factorial(%d) 실행 시간 = %d\n", delegate.getClass().getSimpleName(), num, (end - start)); return result; } }

위와 같은 방식으로 구현하면, 밀리초에서 나노초로 계산하는 것으로 수정할 때 ExeTimeCalculator.java 클래스에서만 수정하면 된다. 

앞에서 언급한 프록시 객체가 무엇일까?
ExeTimeCalculator는 Calculator 인터페이스의 구현클래스이다. 여기서 특이점은 생성자를 통해서 Calculator 객체를 전달받아 delegate 필드에 할당하였다. 그리고 factorial 메소드에서 delegate 필드를 통해 factorial 메소드를 실행하고 있다. 이 delegate.factorial를 실행하기 전과 후에 시간을 구하여 시간 차이를 구하였다. ImpeCalculator, RecCalcurator와 ExeTimeCalculator 클래스의 차이점은 factorial 기능 자체를 직접 구현하지 않고 다른 객체에 factorial()의 실행을 위임한다는 것이다. (delegate.factorial()) 대신 계산 기능 이외의 부가적인 기능(실행 시간 측정)을 실행했다.
결론은 delegate와 같이 핵심 기능의 실행은 다른 객체에 위임하고 부가적인 기능을 제공하는 객체를 프록시라고 부른다.

여기서 공통적인 기능을 Aspect라고 한다.

main.java

package main; import chap07.ExeTimeCalculator; import chap07.ImpeCalculator; import chap07.RecCalculator; public class MainProxy { public static void main(String[] args) { ExeTimeCalculator ttCal1 = new ExeTimeCalculator(new ImpeCalculator()); System.out.println("result : " + ttCal1.factorial(20)); ExeTimeCalculator ttCal2 = new ExeTimeCalculator(new RecCalculator()); System.out.println("result : " + ttCal2.factorial(20)); } }

결과 화면 (밀리초)

ImpeCalculator.factorial(20) 실행 시간 = 0 result : 2432902008176640000 RecCalculator.factorial(20) 실행 시간 = 0 result : 2432902008176640000

결과 화면 (나노초)

ImpeCalculator.factorial(20) 실행 시간(nano) = 3879 result : 2432902008176640000 RecCalculator.factorial(20) 실행 시간(nano) = 4937 result : 2432902008176640000

참고로 0.용어설명에서 언급한 용어를 위 예제에 대입하자면, 
Aspect는 실행 시간을 구하는 기능
PointCut은 핵심 로직에서 메소드를 호출하기 전과 후 지점이 된다.

2. Spring AOP 구현하기

AOP를 구현하기 위해서는 pom.xml에 아래와 같은 dependency를 추가해야한다.

<dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.8.2</version> </dependency>

스프링 AOP는 프록시 객체를 자동으로 만들어준다. 앞에 예시에서 ExeTimeCalculator 클래스에서 상속받은 프록시 클래스를 직접 구현할 필요가 없다. 

Spring AOP 구현을 위해서는 아래 절차를 따른다.
1. 공통 기능 (Aspect) 구현하기
2. 1에서 구현한 Aspect를 어디에 적용(PointCut)할지 설정한다. (=Advice)
-> 2번 Advice 설정은 XML이나 자바 설정을 통해서 설정한다. 

Advice 설정 방법에는 두가지가 있다.
1. XML 스키마 기반
2. @Aspect 어노테이션

2.1. Spring AOP 구현하기 : XML 스키마 기반

2.1.1. 공통 기능 (Aspect) 구현하기
ExeTimeAspect.java

package aspect; import java.util.Arrays; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.Signature; public class ExeTimeAspect { public Object measure(ProceedingJoinPoint joinPoint) throws Throwable { long start = System.nanoTime(); try { Object result = joinPoint.proceed(); return result; } finally { long finish = System.nanoTime(); Signature sig = joinPoint.getSignature(); System.out.printf("%s.%s(%s) 실행 시간 : %d ns\n", joinPoint.getTarget().getClass().getSimpleName(), sig.getName(), Arrays.toString(joinPoint.getArgs()), (finish - start)); } } }

Aspect (공통 기능)을 ExeTimeAspect.java에서 구현하고 있다. aspectj의 ProceedinJoinPoint와 Signature를 유심히보자.
- ProceedingJoinPoint : 핵심 기능(factorial 계산)을 하는 객체의 메소드를 호출할 때 사용하는 파라미터이다. 
- proceed() : 실제 객체 메소드(factorial)을 호출할때에는 proceed()메소드를 사용하면 된다.
- long start = System.nanoTime: & long finish = System.nanoTime(); : 공통 기능(Aspect)을 위한 시간 구하기의 지점이다. 즉 Advice를 지정한 부분이 그러하다.
참고로 Aspect 기능이 메소드 실행 전/후에 사용되기 때문에 Advice의 종류 중에서 Around Advice에 해당한다. 

Advice의 종류
Before Advice : 대상 객체의 메소드 호출 전에 공통 기능을 실행한다.
After Returning Advice : 대상 객체의 메소드가 익셉션 없이 실행된 이후에 공통 기능을 실행한다.
After Throwing Advice : 대상 객체의 메서드를 실행하는 도중 익셉션이 발생한 경우에 공통 기능을 실행한다.
After  Advice : 대상 객체의 메소드를 실행하는 도중에 익셉션이 발생했는지의 여부에 상관 없이 메소드 실행 후 공통 기능을 실행한다. 
Around Advice : 대상 객체의 메소드 실행 전, 후 또는 익셉션 발생 시점에 공통 기능을 실행하는데 사용된다. (주로 사용)

여기서 Signature sig = joinPoint.getSignature()와 joinPoint.getTarget(), joinPoint.getArgs()는 나중에 언급하도록 하겠다. 각각은 대상 객체의 클래스 이름과 메소드 이름을 출력하기 위해사용된다.

2.1.2. Aspect를 어디에 적용(PointCut)할지 설정
aopPojo.xml

<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <!-- 공통 기능을 제공할 클래스를 빈으로 등록 --> <bean id="exeTimeAspect" class="aspect.ExeTimeAspect" /> <!-- Aspect 설정: Advice를 어떤 Pointcut에 적용할 지 설정 --> <aop:config> <aop:aspect id="measureAspect" ref="exeTimeAspect"> <aop:pointcut id="publicMethod" expression="execution(public * chap07..*(..))" /> <aop:around pointcut-ref="publicMethod" method="measure" /> </aop:aspect> </aop:config> <bean id="impeCal" class="chap07.ImpeCalculator"> </bean> <bean id="recCal" class="chap07.RecCalculator"> </bean> </beans>

- aop:aspect tag : 공통 기능으로 ref 속성에 exeTimeAspect를 설정해주었다.
- aop:pointcut tag: 공통 기능을 설정할 부분의 범위를 지정해주었다. expression 속성으로 지정해주는데 execution 명시자 표현식으로 나타내 주었다. pointcut의 범위는 chap07패키지 및 그 하위 패키지에 있는 모든 public 메서드가 된다. 파라미터 또한 0개 이상이다.

Execution 명시자 표현식
execution(수식어패턴? 리턴타입패턴 클래스이름패턴?메서드이름패턴(파라미터패턴)
수식어 패턴 : 생략 가능함. public protected등이 온다. 스프링 AOP의 경우 public 메서드에만 사용 가능하기 때문에 public 이외의 값은 의미가 없다.
리턴타입 패턴 : 리턴 타입 명시
클래스이름패턴, 메서드이름패턴 : 클래스이름 및 메서드 이름을 패턴으로 명시한다.
파라미터 패턴: 매칭될 파라미터에 대해서 명시한다.
각 패턴은 '*'을 통해서 모든 값을 표현할 수 있다. 또한 '..'을 이용하여 0개 이상이라는 의미를 표현할 수 있다.

- aop:around tag: around advice로 설정한다. pointcut-ref 속성으로 위 pointcut 범위에서 measure이라는 메소드를 이용하여 공통 기능으로 사용한다는 의미이다. Around advice 이외에도 다른 tag가 있으니 아래를 참고한다.

Advice 종류 tag 참고 

위 설정을 정리해보면 pointCut을 나타내는 publicMethod는 impeCal 빈 객체와 recCal 빈 객체의 public 메서드인 factorial()을 의미한다. 따라서 impeCal 빈 객체의 factorial을 실행하면 Aspect tag로 지정한 exeTimeAspect 빈 객체의 measure가 공통 기능으로 실행된다. 

main.java

package main; import org.springframework.context.support.GenericXmlApplicationContext; import chap07.Calculator; public class MainXmlPojo { public static void main(String[] args) { GenericXmlApplicationContext ctx = new GenericXmlApplicationContext("classpath:aopPojo.xml"); Calculator impeCal = ctx.getBean("impeCal", Calculator.class); long fiveFact1 = impeCal.factorial(5); System.out.println("impeCal.factorial(5) = " + fiveFact1); Calculator recCal = ctx.getBean("recCal", Calculator.class); long fiveFact2 = recCal.factorial(5); System.out.println("recCal.factorial(5) = " + fiveFact2); } }

2.2. Spring AOP 구현하기 : @Aspect 애노테이션 이용

xml 설정돠 @Aspect 애노테이션 이용 방법은 거의 유사하다 다만 @Aspect 애노테이션을 사용할 경우에는 해당 애노테이션을 설정할 클래스에 공통 기능 및 pointCut을 설정해주어야 한다. 

2.2.1. 스프링 설정이 xml일 경우

ExeTimeAspect2.java

package aspect; import java.util.Arrays; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.Signature; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; @Aspect public class ExeTimeAspect2 { @Pointcut("execution(public * chap07..*(..))") private void publicTarget() { } @Around("publicTarget()") public Object measure(ProceedingJoinPoint joinPoint) throws Throwable { long start = System.nanoTime(); try { Object result = joinPoint.proceed(); return result; } finally { long finish = System.nanoTime(); Signature sig = joinPoint.getSignature(); System.out.printf("%s.%s(%s) 실행 시간 : %d ns\n", joinPoint.getTarget().getClass().getSimpleName(), sig.getName(), Arrays.toString(joinPoint.getArgs()), (finish - start)); } } }

xml 설정했을때의 ExeTimeAspect 클래스와 차이점이 있다면 
1. 클래스 앞에 @Aspect 애노테이션을 설정해 주었다.
2. publicTarget이라는 메소드에 @Pointcut 애노테이션과 범위를 지정해주었다.
3. measure 메소드앞에 @Around 애노테이션을 설정하여 해당 메소드가 Around Advice임을 나타내고 또한 publicTarget 메소드를 명시함으로써 해당메소드가 Advice로 사용됨을 나타낸다. 

aopAspect.xml

<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <aop:aspectj-autoproxy /> <bean id="exeTimeAspect" class="aspect.ExeTimeAspect2" /> <bean id="impeCal" class="chap07.ImpeCalculator"> </bean> <bean id="recCal" class="chap07.RecCalculator"> </bean> </beans>

@Aspect 애노테이션을 사용할 경우에는 xml 스프링 설정에서 <aop:aspectj-autoproxy/> 태그를 추가해 주어야 한다. 이 태그가 의미하는 것은 이 태그를 사용할 시 @Aspect 애노테이션이 적용된 bean 객체를 Advice로 사용한다는 의미이다. xml 파일에서 보면 exeTimeAspect라는 빈 객체를 Advice로 사용하여 공통 기능을 지정한 빈 객체에 적용시킨다.

main.java

package main; import org.springframework.context.support.GenericXmlApplicationContext; import chap07.Calculator; public class MainXmlAspect { public static void main(String[] args) { GenericXmlApplicationContext ctx = new GenericXmlApplicationContext("classpath:aopAspect.xml"); Calculator impeCal = ctx.getBean("impeCal", Calculator.class); long fiveFact = impeCal.factorial(5); System.out.println("impeCal.factorial(5) = " + fiveFact); } }


2.2.2. 스프링 설정이 java일 경우

aopAspect.xml 대신에 javaConfig.java를 통해서 @Aspect 애노테이션을 적용 및 설정한다.
javaConfig.java

package config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.EnableAspectJAutoProxy; import aspect.ExeTimeAspect2; import chap07.Calculator; import chap07.RecCalculator; @Configuration @EnableAspectJAutoProxy public class JavaConfig { @Bean public ExeTimeAspect2 exeTimeAspect() { return new ExeTimeAspect2(); } @Bean public Calculator recCal() { return new RecCalculator(); } }

xml 설정파일에서는 <aop:aspectj-autoproxy> 태그를 사용함으로써 @Aspect 애노테이션이 적용된 빈 객체를 Advice로 사용할 수 있도록 해주었다. 마찬가지로 java config로 설정할 경우에는 config파일에 @EnableAspectJAutoProxy 애노테이션을 설정해주어 @Aspect 애노테이션이 적용될 수 있도록 한다.

main.java

package main; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import chap07.Calculator; import config.JavaConfig; public class MainJavaAspect { public static void main(String[] args) { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(JavaConfig.class); Calculator recCal = ctx.getBean("recCal", Calculator.class); long fiveFact = recCal.factorial(5); System.out.println("recCal.factorial(5) = " + fiveFact); } }


반응형
Comments