AOP가 무엇인가?
Aspect Oriented Programming (관점 지향 프로그래밍)
- 횡단 관심사의 분리를 허용해 모듈성을 증가시키는 것이 목적인 프로그래밍 패러다임
여러 객체에 공통으로 적용할 수 있는 기능을 분리해, 반복 작업을 줄이고 핵심 기능 개발에만 집중할 수 있습니다.
즉, 비즈니스 로직과 공통 로직을 분리해서 구현하는 것 입니다.
AOP 예시
- 나는 학부생으로써, Factorial을 구현하는 과제를 부여 받았다.
- Factorial을 구현할 때 반복문 방식, 재귀 방식 2가지로 구현해야한다.
- 과제를 제출하기 직전, 과제에 새로운 내용을 추가해야했다. 바로
실행시간도 함께 출력하도록 하는 것이다.
이런 위와같은 예시에서 일반적인 코드 형식을 보자
//계산기를 구현하기 위한 Calculator 인터페이스
public interface Caculator{
public long getFacotrial(int n);
}
//반복문으로 구현하는 팩토리얼
public class IterCalculator implements Caculator{
public long getFactorial(int n){
int result = 1;
for(int i = 1 ; i < n ; ++i) result *= i;
return result;
}
}
//재귀 형식으로 구현하는 팩토리얼
public class RecurCalculator implements Caculator{
public long getFactorial(int n){
if(n == 0 ) return 1;
return n * getFactorial(n-1);
}
}
//메소드를 실행시키는 방법
public static void main(String[] args){
IterCalculator iter = new IterCalculator();
RecurCalculator recur = new RecurCalculator();
//반복으로 얻는 팩토리얼 계신시간 구하기
long iterStart = System.currentTimeMills();
iter.getFactorial(50);
long iterEnd = System.currentTimeMills();
System.out.printf("반복 getFactorial(50) 소요시간 : %d \n", (iterEnd - iterStart) );
//재귀 방식으로 얻은 팩토리얼 계산시간 구하기
long recurStart = System.currentTimeMills();
recur.getFactorial(50);
long recurEnd = System.currentTimeMills();
System.out.printf("재귀 getFactorial(50) 소요시간 : %d \n", (recurEnd - recurStart) );
}
대략적으로 위와 같은 형식으로 과제를 제출할 수 있습니다.
만약 계산 시간을 구하는 방법에 대해 지속적인 변경이 있다면 매번 코드를 변경하실건가요? 제 생각에 그것은 매우 비효율적인 작업이라 느껴집니다.
동일한 내용의 코드가 중복되서 나타나기 때문
그렴 여기서 AOP의 개념에 따라 비즈니스 로직과 공통 로직을 분리해 표현하고 관리해보겠습니다.
- 비즈니스 로직(핵심기능) : getFactorai(n);
- 공통 로직(부가 기능) : 메소드의 시간 순서를 계산하는 코드
- 각의 비즈니스 로직에 공통된 로직이 추가되기 때문에
횡단 관심사
라고 언급할 수 있음
- 각의 비즈니스 로직에 공통된 로직이 추가되기 때문에
그렇다면 두 로직을 분리해 구현한다면 다음과 같이 구현할 수 있습니다.
//계산기 인터페이스
public interface Caculator{
long getFacotrial(int n);
}
//공통 로직(시간계산 처리)
public class ExecutionTime implements Caculator{
private Caculator instance;
//Excution을 생성할 때 재귀방식, 반복방식 계산기 객체를 주입한다.
public ExecutionTime(Calculator instance){
this.instance = instance;
}
@Override
public long getFacotrial(int n) {
long commonLogicStart = System.currentTimeMillis();
long result = getFactorial(n);
long commonLogicEnd = System.currentTimeMillis();
System.out.println(this.getClass().getSimpleName() + "의 소요시간 : " + (commonLogicEnd - commonLogicStart) + "ms");
return result;
}
}
//==============================
//메소드 실행영역
public static void main(String[] args){
Calculator proxyIter = new ExecutionTime(new IterCalculator());
proxyIter.getFactorial(50);
Calculator proxyRecur = new ExecutionTime(new RecurCalculator());
proxyRecur.getFactorial(50);
}
다음과 같이 작성하면 ExcutionTime 클래스의 getFactorial만 수정하면 시간을 계산하는 요구사항을 지속적으로 변경할 수 있습니다.
- 비즈니스 로직의 코드를 수정하지 않고
- 공통 기능의 구현을 추가, 변경 하는 것
Proxy?
AOP를 구현하기 전 Proxy에 대해 가볍게 언급하고 넘어가겠습니다.
- 자신의 클라이언트가 사용하려고 하는 것을 실제 대상인 것 처럼 위장해서 클라이언트의 요청을 받아주는 것
클라이언트
-> 프록시
-> 타깃
사용 목적에 따라 2가지로 나눌 수 있는데
- 클라이언트가 타깃에 접근하는 방법을 제어하기 위해서
-> 프록시 패턴 - 타깃에 부가적인 기능을 부여해주기 위해서
입니다.
-> 데코레이터 패턴
이제 본격적으로 AOP에 대해 얘기해보고자 합니다.
AOP 용어 정리
1. Target Object
부가 기능 부여 대상
2. Aspect
AOP 기본 모듈. Aspect 자체 만으로 핵심 기능을 담고 있지 않지만, 애플리케이션을 구성하는 중요하는 한 가지 요소. 부가될 기능을 정의한 Advice와 Advice를 어디에 적용할지 결정하는 Pointcut을 함께 가진다.
3. Advice
타깃에게 제공할 부가 기능을 담은 모듈. 타깃이 필요 없는 순수한 부가 기능이다. Aspect를 언제 수행할지 정의하고 있다.
- 어떤 부가 기능?
- Before : 비즈니스 메서드 실행 전
- AfterReturning : 비즈니스 메서드 성공적인 반환
- AfterThrowing : 비즈니스 메서드 예외 발생시
- After : 비즈니스 메서드 후 무조건 실행
- Around : 메서드 호출 가로채서, 비즈니스 메서드 실행 전후에 특정 로직 실행
4. Join Point
프로그램 실행 내부에서 Advice가 적용될 수 있는 위치
5. Pointcut
Advice에 적용할 JoinPoint를 선별하는 작업 또는 그 기능을 정의한 모듈
AOP 구현 방법
- 컴파일 시점에 코드를 공통 기능에 삽입
- AOP프레임 워크인 AspectJ 컴파일러로 유연하게 적용
- 의존성을 주입해야하는 단점
- 클래스 로딩 시점에 바이트 코드에 기능 삽입
- AOP프레임 워크인 AspectJ 클래스 로더 조작기로 적용 유연하게
- 의존성을 주입해야하는 단점
- 런타임 시점에 프록시 객체를 생성해 공통 기능 삽입
=> Spring에서는 런타임 시점에 프록시 객체 생성 방식 적용
- 프록시는 @Override 개념으로 동작하기 때문에 AOP 메서드 실행 시점에만 적용가능
- 스프링 컨테이너가 관리할 수 있는 Bean에만 적용할 수 있음
- AsepctJ를 직접 사용하는 것이 아닌, 문법을 차용하고 프록시 방식의 AOP 적용
사용방법
- 적용할 클래스에
@Component
어노테이션 적용해 빈으로 등록 - 적용할 클래스에
@Aspect
어노테이션을 적용해 AOP로 등록 - Pointcut을 지정해준다.
- Advice 정의
- Target 호출
@Component public class IterCalculator implements Caculator{ public long getFactorial(int n){ int result = 1; for(int i = 1 ; i < n ; ++i) result *= i; return result; } } //재귀 형식으로 구현하는 팩토리얼 @Component public class RecurCalculator implements Caculator{ public long getFactorial(int n){ if(n == 0 ) return 1; return n * getFactorial(n-1); } }
@Component
@Aspect
public class ExecutionTimeAspect{
//메서드 이름이 get으로 시작하는 경우 AOP를 수행한다.
@Pointcut("execution(* get*(..))")
private void publicTarget(){
}
//Advice를 정의하고
@Around("publicTarget()")
public long getFacotrial(int n) {
long commonLogicStart = System.currentTimeMillis();
try{
//Target을 호출하게 됩니다.
Object result = joinPoint.proceed();
return result
}finally{
long commonLogicEnd = System.currentTimeMillis();
Signature sig = jointPoint.getSignatue();
System.out.println(jointPoint.getTarget().getClass().getSimpleName() + "의 소요시간 : " + (commonLogicEnd - commonLogicStart) + "ms");
}
}