AOP는 관점 지향 프로그래밍이다. 관점을 지향한다는 것이 대체 무슨 뜻인지 잘 와닿지 않는다.
여기서 관점이란 어떤 기능을 구현할 때, 그 기능을 핵심 기능과 부가 기능으로 구분해 각각을 하나의 관점으로 보는 것을 의미한다.
이것 역시 잘 와닿지 않을 수 있다. 예시를 들어 더 자세히 알아보겠다.
public interface Calculator{
public long factorial(long num);
}
다음과 같이 팩토리얼을 계산하는 Calculator
인터페이스가 있다.
public class BasicCalculator implements Calculator {
@Override
public long factorial(long num) {
long result = 0;
for(long i = 1;i<num;i++){
result *= i;
}
return result;
}
}
Caculator
인터페이스를 구현받아, For-loop로 팩토리얼을 계산하는 BasicCalculator
이때 만약, BasicCalculator에서 구현한 factorial()의 실행 시간을 구하는 요구사항이 추가되면 다음과 같이 구현할 수 있다.
public class BasicCalculator implements Calculator {
@Override
public long factorial(long num) {
long start = System.currentTimeMills();
try {
long result = 0;
for(long i = 1;i<num;i++){
result *= i;
}
return result;
} finally {
long end = System.currentTimeMills();
System.out.printf("BasicCalculator 실행 시간 = %d\n", (end - start));
}
}
}
하지만 BasicCalculator
이외에 다른 100개 이상의 Calculator
구현체에 실행 시간을 구하는 요구사항들이 추가된다면, 코드의 유지보수는 끔찍하게 나빠질 것이다.
심지어 실행 시간을 구하는 로직은 비즈니스 로직이 아닌, 부가 기능일 뿐인데 그것에 시간을 쏟는 것은 최악의 경우다.
이를 해결할 수 있는 방법이 프록시 패턴(Proxy Pattern)이다. 네트워크에서 사용하는 Proxy server에서의 그 Proxy와 동일한 개념이다.
최종적으로는 나누어 구현한 부가기능과 핵심기능을 하나로 동작하는 Proxy 객체를 생성하는 것이다. 이에 대한 내용은 앞으로 나올 예정이니깐 이 정도만 알고 있으면 된다.
그럼 프록시 패턴을 적용하여 코드를 변경해 보겠다.
public class ExecutionTimeCalculator implements Calculator {
private Calculator delegate;
public ExecutionTimeCalculator(final Calculator delegate) {
this.delegate = delegate;
}
@Override
public long factorial(long num) {
long start = System.currentTimeMills();
long result = delegate.factorinal(num);
long end = System.currentTimeMills();
System.out.printf("%s 실행 시간 = %d\n", delegate.getClass().getSimpleName(), (end - start));
return result;
}
}
만약 아래와 같은 재귀를 이용한 RecurCalculator
가 추가되어도 RecurCalculator
클래스를 변경하지 않아도 기능을 구현할 수 있다.
public class RecurCalculator implements Calculator {
@Override
public long factorial(long num) {
if(num == 0)
return 1;
else
return num*factorial(num-1);
}
}
바로 이렇게 말이다.
Calculator proxyCalculator1 = new ExecutionTimeCalculator(new BasicCalculator());
System.out.println(proxyCalculator1.factorial(20));
Calculator proxyCalculator2 = new ExecutionTimeCalculator(new BasicCalculator());
System.out.println(proxyCalculator2.factorial(20));
네트워크 프록시 서버
AOP 프록시 패턴
그림과 같이 ExecutionTimeCalculator는 네트워크의 프록시와 같은 역할을 하게 되는 것이다.
이로 인해, 기존 코드를 변경하지 않고 실행 시간을 출력할 수 있고, 실행 시간을 구하는 코드 중복도 제거된다.
정리하자면, 개발자는 여러 객체에서 공통으로 적용할 수 있는 부가 기능을 분리하여 반복 작업을 줄이고, 핵심 기능 개발에만 집중할 수 있다.
AOP의 기본 모듈. Advice(부가 기능의 내용) + Pointcut(부가 기능을 어디에 둘지), 즉 부가 기능을 구현한 클래스에 Advice를 적용하도록 지원할 수 있는 것을 Aspect
부가 기능을 부여할 대상 객체
부가 기능의 순수한 내용을 의미. Before,After,Around의 실행 위치 지정
Advice(부가 기능이)이 적용될 수 있는 지점
Advice(부가 기능)를 어느 JointPoint에 둘지 선별하는 모듈
📌 AOP 3가지 방법
1. 컴파일 시점에 코드에 공통 기능 삽입 ⬅ AspectJ
2. 클래스 로딩 시점에 바이트 코드에 공통 기능 삽입 ⬅ AspectJ
3. 런타임 시점에 프록시 객체를 생성하여 공통 기능 삽입 ✔Spring에서는 3번을 사용
우선 Spring AOP는 Bean으로 등록된 객체에 대해서만 프록시 객체를 생성할 수 있다.
동작시점 | 설명 |
---|---|
Before | 메소드 실행 전 동작 |
After | 메소드 실행 후 동작 |
After-returning | 메소드가 Exception없이 실행되면 실행 후 동작 |
After-throwing | Exception발생하면, 발생한 후 동작 |
Around | 메소드 호출 이전, 이후, 예외발생 등 모든 시점에서 동작 |
TestCode
실행 결과를 보면, BasicCalculator 뒤에 "$$ 블라블라!@!@!"가 붙어있다.
이는 사실, BasicCulator.factorial()
을 실행한 것이 아닌 BasicCalulator
의 프록시 객체인 BasicCalculator$$!@#!#
가 실행된 것이다.
ExecutionTimeAspectTest
와 BasicCalculator
사이에 프록시 객체가 있다.
프록시 객체는 BasicCalulator를 감싸고 있다고 생각하면 된다.