AOP란
AOP란 Aspect Oriented Programming의 약자로, 여러 객체에 공통으로 적용할 수 있는 기능을 구분함으로써재사용성을 높이는 프로그래밍 기법이다. AOP는 핵심기능과 공통기능의 구현을 분리함으로써 핵심 기능을구현한 코드의 수정 없이 공통 기능을 적용 할 수 있게 해준다.
먼저 스프링에 적용하기전에 아래와 같이 실습을 해 봅시다.
완성된 파일 모습.
Calculator.java
package com.aop.step1;
public interface Calculator {
public long factorial(long num);
}
구현체1. ForCalculator.java (for문)
package com.aop.step1;
public class ForCalculator implements Calculator{
@Override
public long factorial(long num) {
long result=1;
for(int i=1; i<=num ; i++) {
result *=i;
}
return result;
}
}
구현체2. RecCalculator.java (재귀함수)
package com.aop.step1;
public class RecCalculator implements Calculator{
@Override
public long factorial(long num) {
return num==0 ? 1 : num*factorial(num-1);
}
}
위에서 구현한 두 개의 구현 클래스의 실행 시간을 출력하고 싶다고 해보자.
Main.java
package com.aop.step1;
public class Main {
public static void main(String[] args) {
ForCalculator forCal= new ForCalculator();
long start1= System.currentTimeMillis();
long forFactorial= forCal.factorial(4);
long end1= System.currentTimeMillis();
System.out.printf("ForCalculator.factorial(4) 실행시간 = %d\n", (end1-start1));
RecCalculator recCal= new RecCalculator();
long start2= System.currentTimeMillis();
long recFactorial= recCal.factorial(4);
long end2= System.currentTimeMillis();
System.out.printf("RecCalculator.factorial(4) 실행시간 = %d\n", (end2-start2));
}
}
이러면 ForCalculator, RecCalculator도 잘 작동할 것이다.
그런데 currentTimeMills()는 너무 단위가 커서 0으로 나온다.
그래서 좀 더 정확한 나노초 단위로 구하기 위해
currentTimeMills()대신에 nanoTime으로 바꾸기로 했다.
이 때 For,Rec 측정하는 부분 2군데를 바꿔야한다.
만약 factorial() 메소드를 실행하는 부분이 10개가 있다면 10군데를 바꿔야 한다.
어떻게 하면 시간 측정 코드를 반복적으로 사용하지 않을 수 있을까?
factorial()메소드에서 factorial구하는 식 전,후로 시간을 출력하면 될거 같다.
ForCalculator.java
package com.aop.step1;
public class ForCalculator 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("ForCalculator.factorial(%d) 실행시간 = %d\n", num , end-start);
return result;
}
}
근데 RecCalculator의 경우에는 약간 이상하다.
출력하는 메시지를 쓰면 재귀함수가 호출될때마다 콘솔에 실행시간이 출력되어버린다.
어떻게 하면 RecCalculator에서도 시간 측정을 할 수 있을까?
기존코드를 수정하지않으면서, 코드 중복을 하지 않는 방법은 없을까???
이 때 필요한 것이 프록시이다.
먼저 다음의 클래스를 만들자.
ExeTimeCalculator.java
package com.aop.step1;
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;
}
}
여기서 delegate는 '대리자' 라는 뜻이다.
이 ExeTimeCalculator는 생성자를 통해 다른 Calculator 객체를 전달받아
delegate필드에 할당한 뒤 해당 factorial메소드를 실행한다.
즉 생성자로 전달받는게 ForCalculator객체면 for문으로 factorial,
RecCalculator객체면 재귀함수로 factorial을 실행하게된다.
main메소드에서는 다음과 같이 실행한다.
ForCalculator forCal= new ForCalculator();
ExeTimeCalculator excal=new ExeTimeCalculator(forCal);
long result=excal.factorial(4);
RecCalculator recCal= new RecCalculator();
ExeTimeCalculator excal2=new ExeTimeCalculator(recCal);
long result2=excal2.factorial(4);
위 코드는 다음과 같은 순서로 실행된다.
이렇게 프록시 객체를 적용하면
- 기존코드를 변경하지 않고 실행 시간을 출력 할 수 있다.
ForCalculator나 RecCalculator클래스의 코드를 변경하지않고도 이 두 클래스의 factorial()메소드의
실행시간을 출력 할 수 있게 되었다. - 실행 시간을 구하는 코드 중복을 제거했다.
ForCalculator나 RecCalcaultor에 작성하지않고 ExeTiemCalcualtor클래스에만 작성하면 된다.
실행시간에 대해 코드 변경이 있어도 ExeTimeCalcualtor만 변경하면 된다.
와 같은 장점이 있다. 이것이 가능한 이유는 ExeTimeCalcualtor클래스를 다음과 같이 구현했기 때문이다.
- factorial() 기능 자체를 직접 구현하기보다는, 다른 객체에 factorial()의 실행을 위임한다.
- 계산 기능 외에 시간을 측정하는 기능을 실행한다. 여기서 계산기능은 주기능, 시간 측정기능은 부기능이다.
이렇게 핵심 기능의 실행은 다른 객체에 위임하고
부가적인 기능을 제공하는 객체를 프록시라고 부르고,
실제 핵심 기능을 실행하는 객체를 대상 객체라고 부른다.
프록시의 특징은 핵심 기능은 구현하지 않는다는 점이다.
ForCalculator와 RecCalculator는 실제 팩토리얼을 구하는 핵심기능(주기능)에 집중하고있고,
ExeTimeCalcualtor는 실행 시간 측정이라는 공통 기능 구현(부기능)에 집중하고 있다.
이렇게 공통 기능 구현과 핵심 기능 구현을 분리하는 것이 AOP의 핵심이다.
이 때 부가기능이 핵심기능에 횡단에 걸쳐서 존재하기 때문에 횡단관심(cross-cutting concern)이라고 한다.
일반적인 웹 사이트 기능에 대한 부가기능(횡단관심사 들)과 핵심기능(비즈니스로직) 관계
SPRING AOP
AOP의 기본 개념은 핵심 기능(factorial)에 공통 기능(시간측정)을 삽입하는 것이다.
핵심 기능에 공통기능을 삽입하는 방법에는 다음의 세가지 방식이있다.
- 컴파일 시점에 코드에 공통 기능 추가
- 클래스 로딩 시점에 바이트 코드에 공통 기능을 추가
- 런타임에 프록시 객체를 생성해서 공통 기능을 추가
이 중 스프링이 제공하는 AOP 방식은 프록시를 이용한 세 번째 방식이다.
프록시를 이용하는 방법은 앞의 실습 처럼 중간에 프록시 객체를 생성한다.
그리고 다음 그림처럼 실제 객체의 기능을 사용하기 전 · 후에 공통 기능을 호출한다.
스프링 AOP는 프록시 객체를 자동으로 만들어 줍니다.
따라서 앞의 실습에서처럼 ExeTimeCalculator 클래스처럼
상위 타입의 인터페이스를 상속 받은 프록시 클래스를 직접 구현할 필요가 없다.
단지 공통 기능을 구현한 클래스만 알맞게 구현하면 된다.
(공통 기능 클래스 만드는데 Calculator를 상속 받을 필요가 없다.
그냥 클래스만 덩그러니 만들면 된다.)
AOP 주요 용어
1.AOP 주요 용어
용어 | 설명 |
Joinpoint | Advice를 적용가능한 지점을 의미합니다. 메서드 호출, 필드 값, 변경 등이 이에 해당한다. Spring에서는 프록시를 이용해서 AOP를 구현하기 때문에 메서드 호출에 대한 JoinPoint만 지원한다. 즉, Spring에서는 JoinPoint 신경 안써도 된다. 무조건 메서드 호출이기 때문이다. |
Pointcut | Joinpoint의 부분 집합으로, Advice가 적용되는 Joinpoint를 나타냅니다. spring에서는 메서드에만 공통 기능이 적용되는데, 모든 메소드에 적용하는 것이 아닌 개발자가 원하는 특정 메서드에만 공통기능을 적용할 것입니다. 이 때 특정 메서드를 지정해주는 역할이 Pointcut입니다. |
Aspect | 여러 객체에 공통으로 적용되는 기능을 말합니다. (공통 기능, 쉽게 말해 클래스, .java파일) |
Advice | Aspect(공통기능)를 언제 핵심 코드에 적용할 지를 정의합니다. |
Weaving | Advice를 핵심 코드에 적용하는 것을 말합니다. |
2. ADVICE 종류
용어 | 설명 |
Before Advice | 대상 객체의 메서드 호출 전에 공통 기능을 실행합니다. |
After Returning Advice | 대상 객체의 메서드가 예외 없이 실행된 이후에 공통기능을 실행합니다. |
After Throwing Advice | 대상 객체의 메서드를 실행하는 도중 예외가 발생한 경우 공통기능을 실행합니다. |
After Advice | 대상 객체의 메서드 실행 후 공통 기능을 실행합니다. |
Around Advice | 대상 객체의 메서드 실행 전 / 후, 예외 발생 시점에 공통 기능을 실행합니다. |
이 중 Around Advice는 다양한 시점에 원하는 기능을 삽입 할 수 있어 많이 사용된다.
실습도 Around Advice만 적용한다.
스프링 AOP 구현
시작하기전에 AOP관련 dependecy를 pom.xml에 추가해줍시다.
(spring-context 라이브러리도 필요하지만 mvc 프로젝트를 만들면 기본적으로 pom.xml에 있다.)
AOP 구현체중 JAVA는 aspectj를 사용한다.
<!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.7</version>
</dependency>
스프링에서 AOP를 이용해 공통기능 구현 후 적용하는 방법은 간단하다.
1. 공통 기능을 제공하는 Aspect를 구현
2. Aspect를 어디(Pointcut)에 적용할지 설정한다.
즉,개발자는 Aspect 구현 클래스를 만들고,
XML이나 자바 설정을 이용해서 Aspect를 어디에 적용할 지 설정하면 된다.
우리는 xml 만 실습한다.
XML 기반 (AOP 관련 빈 등록을 통해..)
공통기능을 제공하는 클래스, Aspect를 만들어 봅시다.
ExeTimeAspect.java
package com.aop.step2;
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();
MethodSignature sig= (MethodSignature) joinPoint.getSignature();
System.out.println("실행 클래스 : "+joinPoint.getTarget().getClass().getSimpleName());
System.out.println("실행 메소드 : "+ sig.getName());
System.out.println("그 메소드의 파라미터 : "+ Arrays.toString( sig.getParameterNames()));
System.out.println("그 메소드의 파라미터 타입 : "+ Arrays.toString( sig.getParameterTypes()));
System.out.println("실행 시간 : "+ (finish-start)) ;
}
}
}
step2의 ForCalculator와 RecCalculator를 만든다.
public class ForCalculator {
public long factorial(long num) {
long result=1;
for(int i=1; i<=num ; i++) {
result *=i;
}
return result;
}
}
public class RecCalculator {
public long factorial(long num) {
return num==0 ? 1 : num*factorial(num-1);
}
}
그리고 다음과 같이 src/main/resources에 aop폴더를 만들고 step2.xml을 만들어줍시다.
물론 이클립스에서 만들 때 new other spring bean configuration file로 만듭니다.
step2.xml (소스코드 복사해도 되지만, namespace에서 aop체크해주자)
<?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"
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-4.3.xsd">
<bean id="exeTimeAspect" class="com.aop.step2.ExeTimeAspect"></bean> <!-- 공통기능을 수행할 빈 생성 -->
<aop:config proxy-target-class="true">
<!-- 기본값은 'false'인데 aop 적용 될 클래스(for,recCal)가 특정 인터페이스(Calculator)를 상속 받은 경우
프록시 객체 생성 방식 때문에 에러가 난다.
Bean named 'forCal' is expected to be of type 'com.aop.step2.ForCalculator' but was actually of type 'com.sun.proxy.$Proxy2'
이 때 값을 'true'로 해준다면 프록시 객체 생성 방식이 변경 되면서 에러가 안 나게 된다.
-->
<aop:aspect id="timeMeasureAspect" ref="exeTimeAspect"> <!-- 공통기능 클래스 , aspect 설정 -->
<aop:pointcut expression="execution(public * com.aop.step2..*(..))" id="publicMethod"/>
<!-- 공통기능을 어디(어떤메소드)에다 지정할 지 설정 -->
<!-- expression 사용법은 외울필요없다. 그 때 그 때 검색하자. -->
<aop:around pointcut-ref="publicMethod" method="measure"/>
<!-- method : aspect 클래스인 exeTimeAspect 에서 어떤 메소드를 공통메소드로 할 건지.. -->
<!-- 즉, around(전후)로 publicMethod에서 지정한 com.aop.step2에 있는 모든 public 메소드들을 실행할 때
exeTimeAspect클래스에 있는 measure메소드가 실행된다.
-->
</aop:aspect>
</aop:config>
<bean id="forCal" class="com.aop.step2.ForCalculator"></bean>
<bean id="recCal" class="com.aop.step2.RecCalculator"></bean>
</beans>
그리고 main 메소드를 다음과 같이 작성하자.
Main.java
package com.aop.step2;
import org.springframework.context.support.AbstractApplicationContext;
import org.springframework.context.support.GenericXmlApplicationContext;
public class Main {
public static void main(String[] args) {
AbstractApplicationContext ctx= new GenericXmlApplicationContext("aop/step2.xml");
ForCalculator forCal= ctx.getBean("forCal", ForCalculator.class);
RecCalculator recCal= ctx.getBean("recCal", RecCalculator.class);
long factorailFor= forCal.factorial(5);
System.out.println("결과 값 : " + factorailFor);
System.out.println();
System.out.println();
long factorailRec= recCal.factorial(5);
System.out.println("결과 값 : " + factorailRec);
}
}
그리고 main메소드를 실행해보면 다음과 같은 결과가 나온다.
XML, @기반 AOP 적용
aop도 di의 component-scan과 비슷하게 특정 패키지를 스캔한 후 @을 분석해 aop를 적용하게 할 수 있다.
src/main/resources에 다음과 같이 step3.xml을 만든다.
step3.xml (step2와 마찬가지로 namespace에서 aop 체크) (빈들의 클래스가 step3다.)
<?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"
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-4.3.xsd">
<bean id="exeTimeAspect" class="com.aop.step3.ExeTimeAspect"></bean> <!-- 공통기능을 수행할 빈 생성 -->
<aop:aspectj-autoproxy proxy-target-class="true"></aop:aspectj-autoproxy>
<bean id="forCal" class="com.aop.step3.ForCalculator"></bean>
<bean id="recCal" class="com.aop.step3.RecCalculator"></bean>
</beans>
그리고 com.aop.step2 패키지를 복사하자.
이 중 ExeTimeAspect에 다음과 같이 @을 추가하자.
package com.aop.step3;
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 ExeTimeAspect {
@Pointcut("execution(public * com.aop.step3..*(..))")
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.println("실행 클래스 : " + joinPoint.getTarget().getClass().getSimpleName());
System.out.println("실행 메소드 : " + sig.getName());
System.out.println("그 메소드의 파라미터 : " + Arrays.toString(joinPoint.getArgs()));
System.out.println("실행 시간 : " + (finish - start));
}
}
}
그리고 Main메소드에서 context생성 코드에서 step3.xml을 읽도록 바꾸고 실행하자.
new GenericXmlApplicationContext("aop/step3.xml");
결과는 똑같이 step2랑 똑같이 나오는 걸 확인할 수 있다.
이렇게 step3에서는 @ 를 이용해 AOP를 적용해 보았다.
참고사항
1.ProceedingJoinPoint의 메서드
public Object measure(ProceedingJoinPoint joinPoint) throws Throwable {
ExeTimeAspect에서 사용한 measure 메서드의
ProceedingJoinPoint 객체 joinPoint를 이용해서
실행클래스, 실행 메소드, 파라미터 등을 구했었다.
ProceedingJoinPoin의 주요메소드는 다음과 같다,
- Signature getSignature() - 호출되는 메서드에 대한 정보를 제공하는 Signature를 리턴
이 Signature를 이용해 메서드 이름 등을 구할 수 있다. - Object getTarget() - 대상 객체(핵심 객체)를 구한다. (ExeTimeAspect가 프록시객체이다)
- Object getArgs() - 파라미터 목록을 구한다
2. 프록시 객체 생성 방식
만약 ForCalculator가 Calculator를 implement 했다면
<bean class="aop.step2.ForCalculator"></bean>
로 생성한 빈 객체의 실제 타입은 프록시타입이다
따라서 다음과 같은 코드에서 실제 타입이 다르기 때문에 에러가 날 수 있다
ForCalculator forCal= ctx.getBean("forCal", ForCalculator.class); //에러
Calculator forCal= ctx.getBean("forCal", ForCalculator.class); //에러X
3. AOP 적용 순서
ForCalculator의 factorial 실행할 때 적용되는 공통관심사가 2개라고 가정해보자.
(원래는 시간측정인 measure하나뿐)
이 때 이 공통관심사(메소드겠지?) 2개의 실행순서가 중요한 경우
개발자가 실행순서를 지정해줘야 한다. 간단하다.
<aop:aspect id="timeMeasureAspect" ref="exeTimeAspect" order="1" > <!-- 공통기능 클래스 , aspect 설정 -->
위 태그에서 order속성을 지정해주면 된다. order속성 값이 적은 것일 수록 먼저 실행된다.
@을 이용할 경우
@Aspect
@Order(1)
public class ExeTimeAspect {
처럼 지정해주면 된다.
4. Pointcut의 exectuion명시자 표현식
Pointcut은 공통기능이 어떤 메소드가 실행될 때 실행될지 지정해주는 것이라고 했습니다.
이를 execution 명시자를 이용해서 지정하는데 규칙은 다음과 같습니다.
execution(접근명시자? 리턴타입 클래스이름패턴?메서드이름패턴(파라미터패턴)
접근명시자의 경우 spring에서는 public만 사용해서 별 의미가 없다.
다음의 예시를 보자.
PointCut 표현식 예시 - execution
"execution(* aspects.trace.demo.*.*(. . ))" |
execution | execution 표현식 |
* | Any return type |
aspects.trace.demo | package |
.* | class |
.* | method |
(. . ) | Any type and number of arguments |
표현식 | 설명 |
execution(public void get*(..)) | public void인 모든 get메소드 파라미터는 모든 종류를 다 허용합니다. |
execution(* com.codelab.ex.*.*()) | com.codelab.ex 패키지에 파라미터가 없는 모든 메소드 |
execution(* com.codelab.ex..*.*()) | com.codelab.ex 패키지 및 com.codelab.ex 하위 패키지에 파라미터가 없는 모든 메소드 |
execution(* com.codelab.ex.Worker.*()) | com.codelab.ex.Worker클래스안에 있는 파라미터가 없는 모든 메소드 |
execution(* hello(..)) | hello라는 이름을 가진 메서드를 선정합니다. 파라미터는 모든 종류를 다 허용합니다. |
execution(* hello()) | 파라미터 패턴이 ()로 되어 있으므로 hello 메서드 중에서 파라미터가 없는 것만 선택합니다. |
execution(* com.codelab.service.UserServiceImpl.*(..)) | com.codelab.service.UserServiceImpl 클래스를 직접 지정하여 이 클래스가 가진 모든 메서드를 선택합니다. |
execution(* com.codelab.service.UserServiceImpl.*.*(..)) | com.codelab.service 패키지의 모든 클래스에 적용됩니다. 하지만 서브패키지의 클래스는 포함되지 않습니다. |
execution(* com.codelab.service..*.*(..)) | com.codelab.service 패키지의 모든 클래스에 적용됩니다. '..' 를 사용해서 서브패키지의 모든 클래스까지 포함합니다. |
execution(* *..Target.*(..)) | 패키지에 상관없이 Target이라는 이름의 모든 클래스에 적용됩니다. 다른 패키지에 같은 이름의 클래스가 있으면 모두 적용이 되므로 유의해야 합니다. |