AOP (Aspect-Oriented Programming, 관점 지향 프로그래밍)
OOP를 보완하는 수단으로, 흩어진 관심사(Cross-cutting Concerns)를 모듈화할 수 있는 프로그래밍 기법이다.
여러 객체에 공통으로 적용할 수 있는 기능을 분리해서 개발자의 반복 작업을 줄이고 핵심 기능 개발에만 집중할 수 있도록 한다.
Calculator 객체 구현 예시
팩토리얼 연산을 수행하는 Calculator 인터페이스가 있고, 이를 반복문 형태로 구현한 BasicCalculator가 있다고 해보자.
package aop;
public interface Calculator {
long factorial(long num);
}
package aop;
public class BasicCalculator implements Calculator {
@Override
public long factorial(final long num) {
long result = 1;
for (long i = 1; i <= num; i++) {
result *= i;
}
return result;
}
}
요청 1 ) factorial 실행시간을 구하고 싶어요
BasicCalculator 클래스의 factorial 함수 안에 아래와 같이 실행시간을 구하는 소스를 추가해야한다.
package aop;
public class BasicCalculator implements Calculator {
@Override
public long factorial(final long num) {
long start = System.currentTimeMillis();
try {
long result = 1;
for (long i = 1; i <= num; i++) {
result *= i;
}
return result;
} finally {
long end = System.currentTimeMillis();
System.out.printf("BasicCalculator factorial(%d) 실행시간 -> %d \n",
num, (end-start));
}
}
}
요청 2 ) 반복문 말고 재귀로도 구현하고 싶어요
package aop;
public class RecursiveCalculator implements Calculator {
@Override
public long factorial(final long num) {
if (num == 0) {
return 1;
}
return num * factorial(num - 1);
}
}
factorial 연산을 재귀적으로 구현한 RecursiveCalculator를 만들고, 실행시간을 측정하는 부분은 중복제거를 위해 호출하는 쪽에서 수행하도록 한다.
여기서 만약 실행시간을 밀리초가 아닌, 나노초로 출력하고 싶다는 추가 요청이 들어오게 된다면, 호출하는 쪽으로 옮긴 코드도 수정해야한다.
이렇게 계속되는 요청으로 기존코드의 수정과 코드 중복이 계속해서 발생할 수 있다.
이를 해결하기 위해 프록시를 사용한다 !
프록시란?
자신이 클라이언트가 사용하려고 하는 실제 대상인 것처럼 위장해서 클라이언트의 요청을 받아주는 것이다.
사용 목적은 크게 아래 두가지이다.
- 프록시 패턴 : 클라이언트가 타깃에 접근하는 방법을 제어
- 데코레이터 패턴 : 타깃에 부가적인 기능을 부여
ExecutionTimeCalculator 프록시 객체 구현
1. Calculator 인터페이스를 구현
2. 내부에 Calculator 인터페이스를 필드로 가짐
3. 외부에서 구현체의 의존성을 주입 받음
4. 이전에 핵심연산을 수행했던 factorial 메서드에서는
부가기능인 실행시간을 측정하고, 핵심연산인 factorial 연산은 외부에서 주입받은 객체에게 위임
package aop;
public class ExecutionTimeCalculator implements Calculator {
private Calculator delegate;
public ExecutionTimeCalculator(final Calculator delegate) {
this.delegate = delegate;
}
@Override
public long factorial(final 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;
}
}
따라서 아래처럼 호출하게 된다.
Calculator proxyCalculator1 = new ExecutionTimeCalculator(new BasicCalculator());
System.out.println(proxyCalculator1.factorial(20));
Calculator proxyCalculator2 = new ExecutionTimeCalculator(new RecursiveCalculator());
System.out.println(proxyCalculator2.factorial(20));
위와 같이 프록시를 사용하여 구현하니 기존 코드의 변경 없이 실행 시간을 출력할 수 있고, 실행 시간을 구하는 코드의 중복이 제거된다.
이 예제를 통해 AOP 개념을 다시 정리해보자.
각각의 구현체들은 핵심 기능을 가지고, 공통적인 부가 기능을 가지게 될 경우 중복이 발생한다.
이 때 핵심 기능과 부가 기능의 관점을 분리하여, 부가 기능의 공통된 부분을 추출하는 것이 AOP 이다.
AOP 주요 개념
개념만 봐서는 이해하기 어려우니, 지금까지 살펴본 계산기와 실행시간 측정 예시를 통해 알아보자
@Component
public interface Calculator {
long factorial(long num);
long factPlus(long num1, long num2);
long minus(long num1, long num2);
}
계산기에 factorial 메서드 이외에 factPlus, minus 메서드가 추가되었다고 해보자.
Target
- Aspect가 적용되는 곳
- 부가 기능을 부여할 대상
Advice
- 타깃에게 제공할 부가 기능을 담은 모듈
- Aspect가 무엇을 언제 할지 정의
- 실행시간 측정 기능을 말한다
JoinPoint
- 프로그램 실행 내부에서 Advice가 Target에 적용될 수 있는 지점
ex) 메서드 진입 시, 필드에서 값을 꺼낼 때, 생성자 호출 시 등등 - factorial, factPlus, minus와 같이 부가기능을 적용할 수 있는 지점들을 말한다
Pointcut
- JoinPoint를 선별하는 작업 또는 그 기능을 정의한 모듈
- 예를 들어 메서드 이름이 fact일 때 부가기능을 적용하겠다는 정의이다
Aspect
- AOP의 기본 모듈
- 부가될 기능을 정의한 Advice와 이 Advice를 어디에 적용할 지 결정하는 Pointcut을 함께 가짐
AOP 구현 방법
컴파일 시점의 코드에 공통 기능 삽입
클래스 로딩 시점의 바이트 코드에 공통 기능 삽입
런타임 시점에 프록시 객체를 생성하여 공통 기능 삽입
런타임 시점은 이미 자바언어가 실행된 이후이므로 자바 언어가 제공하는 기능인 프록시를 생성하여 공통 기능을 삽입한다.
이 방식이 스프링에서 AOP를 구현하는 방식이다.
https://jainkku.tistory.com/99
reference
- https://dailyheumsi.tistory.com/202
- https://www.youtube.com/watch?v=hjDSKhyYK14&t=27s