트랜잭션의 처리 방식을 이해하려면 AOP(Aspect Oriented Programming)를 이해해야 합니다. 제일 어려운 부분이니 자세히 짚어보겠습니다.
이전 요약글
준비
먼저 AOP 프로젝트를 준비하기 위해 pom.xml의 Dependencies에 아래코드를 추가해 줍니다.
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.13</version>
</dependency>
스프링 프레임워크의 AOP 기능은 spring-aop 모듈이 제공하는데 spring-context 모듈을 의존 대상에 추가하면 spring-aop 모듈도 함께 의존 대상에 포함되기 때문에, aspectjweaver 모듈은 AOP를 설정하는데 필요한 애노테이션을 제공하므로 해당 의존성을 추가해야 합니다.
다음은 양의 정수 n의 계승 값을 구하기 위한 코드를 이어서 작성해 보겠습니다.
public interface Calculator {
public long factorial(long num);
}
계승을 구하기 위한 Calculator 인터페이스를 정의합니다.
public class ImpeCalculator implements Calculator {
@Override
public long factorial(long num) {
long result = 1;
for (long i = 1; i <= num; i++) {
result *= 1;
}
return result;
}
}
Calcuator를 구현한 ImpeCalculator는 for문으로 이용해 계승 값을 구합니다.
public class RecCalculator implements Calculator {
@Override
public long factorial(long num) {
if (num == 0) {
return 1;
} else
return num * factorial(num - 1);
}
}
Calcuator 인터페이스를 구현한 RecCalculator는 재귀호출을 이용해서 계승을 구합니다.
프록시와 AOP
앞에서 구현한 계승 구현 클래스의 실행 시간을 출력하려면, 쉬운 방법으로는 메서드의 시작과 끝에서 시간을 구하고 두 시간의 차이를 출력하면 될 것 같은데요.
public class ImpeCalculator implements Calculator {
@Override
public long factorial(long num) {
long start = System.currentTimeMillis();
long result = 1;
for (long i = 1; i <= num; i++) {
result *= 1;
}
long end = System.currentTimeMillis();
System.out.printf("ImpeCalculator.factorial(%d) 실행시간 = %d\n", num , end-start);
return result;
}
}
위와 같이 start - end 클래스를 이용해 시간을 구하면 됩니다.
하지만 재귀 호출로 구현한 RegCalculator 클래스는 어떨까요?
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("ImpeCalculator.factorial(%d) 실행시간 = %d\n", num, end - start);
}
}
}
약간 복잡해집니다. 위 클래스의 factorial() 메서드는 재귀 호출로 구현해서 factorial() 메서드의 시작과 끝에 시간을 구해서 차이를 출력하는 코드를 넣으면 메시지가 여러 번 출력되는 문제가 있습니다.
예를 들면, factorial(2)를 실행하면 재귀호출로 인해, factorial(1), factorial(0)가 실행되고 메서드가 실행될 때마다, 시간 메시지가 3번 출력됩니다.
RecCalculator 클래스를 고려하면 실행 시간을 출력하기 위해 기존 코드를 변경하는 것보다는 차라리 다음 코드처럼 메서드 실행 전후에 값을 구하는 게 나을지도 모릅니다.
public class Main {
public static void main(String[] args) {
ImpeCalculator impeCal = new ImpeCalculator();
long start1 = System.currentTimeMillis();
long fourCactorial1 = impeCal.factorial(4);
long end1 = System.currentTimeMillis();
System.out.printf("ImpeCalculator.factorial(%d) 실행시간 = %d\n", 4, end1 - start1);
RecCalculator recCal = new RecCalculator();
long start2 = System.currentTimeMillis();
long fourCactorial12= impeCal.factorial(4);
long end2 = System.currentTimeMillis();
System.out.printf("ImpeCalculator.factorial(%d) 실행시간 = %d\n", 4, end2 - start2);
}
}
그렇지만, 만약 실행 시간을 밀리초 단위가 아니라 나노초 단위로 구해야 하면 어떻게 해야 할까요? 위 코드에서 시작, 종료 시간과 시간을 출력하는 출력문 로직을 모두 변경할 필요가 있어 보입니다.
그렇다면 여기서 드는 생각은 바로 기존 코드를 수정하지 않고 코드 중복도 피할 수 있는 방법은 없을까요?
이때, 등장하는 것이 바로 프록시 객체입니다.
프록시 객체의 등장
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는 Calculator 인터페이스를 구현하고 있습니다. 생성자를 통해 다른 Calculator 객체를 전달받아 delegate 필드에 할당하고 delegate.factorial() 메서드를 실행합니다. 마찬가지로 해당 메서드를 실행 전 후에 현재 시간을 구해 시간차이를 출력하고 있습니다.
ExeTimeCalculator를 사용하면 ImpeCalculaotr의 실행시간을 측정할 수 있습니다.
public class Main {
public static void main(String[] args) {
ImpeCalculator impeCal = new ImpeCalculator();
ExeTimeCalculator calculator = new ExeTimeCalculator(impeCal);
long result = calculator.factorial(4);
}
}
실행흐름은 다음과 같습니다
1. calculator.factorial(4)가 호출됩니다.
2. 해당 메서드는 ExeTimeCalculator 생성자로 주입받은 ImpeCalculator 클래스의 factorial(4) 메서드를 실행합니다.
3. ImpeCalculator 출력문이 출력되고, ExeTimeCalculaotr 출력문이 출력됩니다.
ImpeCalculator.factorial(4) 실행시간 = 0
ImpeCalculator.factorial(4) 실행시간 = 12455042
더 명확하게 확인하기 위해 MainProxy를 통해 알아보도록 하겠습니다.
각 주입되는 ImpeCalculator와 RecCalculator의 출력문 코드는 제거하였습니다.
public class MainProxy {
public static void main(String[] args) {
// 1. ImpeCalculator 주입
ExeTimeCalculator ttCal1 = new ExeTimeCalculator(new ImpeCalculator());
System.out.println(ttCal1.factorial(20));
// 2. RecCalculator 주입
ExeTimeCalculator ttCal2 = new ExeTimeCalculator(new RecCalculator());
System.out.println(ttCal2.factorial(20));
}
}
ImpeCalculator.factorial(20) 실행시간 = 1625
1
RecCalculator.factorial(20) 실행시간 = 4084
2432902008176640000
저의 컴퓨터에서는 이와 같은 결과가 나왔네요..
위의 결과에서 알 수 있는 것은
1. 기존 코드를 변경하지 않고 실행 시간을 출력할 수 있습니다. ImpeCalculator 나 RecCalculator의 코드 변경 없이 이 두 클래스의 factorial() 메서드 실행 시간을 출력할 수 있게 되었습니다.
2. 실행 시간을 구하는 코드의 중복을 제거했습니다. 나노초 대신 밀리초를 사용해서 실행 시간을 구하고 싶다면 ExeTimeCalculator 만 변경 해주면 됩니다.
이것이 가능한 이유는 ExeTimeCalculaotr를 아래와 같이 구현했기 때문입니다.
1. factorial() 기능 자체를 직접 구현하기보다는 다른 객체에 factorial() 실행을 위임합니다.
2. 계산 기능 외에 다른 부가적인 기능을 실행합니다. 여기서 부가적인 기능은 실행 시간 측정입니다.;
이처럼, 핵심 기능의 실행은 다른 객체에 위임하고 부가적인 기능을 제공하는 객체를 프록시(proxy)라고 부릅니다.
여기서는 ExeTimeCalculator가 프록시 객체이고, ImpeCalculator, RecCalculator 가 프록시의 대상 객체가 됩니다.
엄밀히 말하자면 지금 작성한 코드는 프록시라기 보다는 데코레이터(decorator) 객체에 가깝습니다.
프록시 : 접근제어에 관점 초점
데코레이터 : 기능 추가와 확장에 초점
예제코드를 볼 때는 기존 기능에 시간 측정 기능을 추가하고 있기 때문에 데코레이터에 가깝지만 스프링의 레퍼런스 문서에서 AOP를 설명할 때 프록시란 용어를 사용하고 있어서 프록시를 사용했습니다.
프록시의 특징
프록시는 핵심 기능을 구현하지 않습니다.
프록시는 핵심기능을 구현하지 않는 대신 여러 객체에 공통으로 적용할 수 있는 기능을 구현합니다.
정리하자면 ImpeCalculator와 RecCalculator는 팩토리얼을 구한다는 핵심 기능 구현에 집중하고, 프록시인 ExeTimeCalculator는 실행 시간 측정이라는 공통 기능 구현에 집중합니다.
이렇게 공통 기능 구현과 핵심 기능 구현을 분리하는 것이 바로 AOP의 핵심원리입니다.
'Spring' 카테고리의 다른 글
[Spring] Spring AOP 구현해보기 (@Aspect, @Pointcut, @Around) (1) | 2024.02.02 |
---|---|
[Spring] Spring AOP (0) | 2024.01.31 |
[Spring] 빈 객체의 생성과 관리범위 (0) | 2024.01.29 |
[Spring] 빈 라이프 사이클 - 빈 객체의 초기화와 소멸 : 커스텀 메소드 (0) | 2024.01.29 |
[Spring] 빈 라이프 사이클 : Bean 초기화, 소멸 (인터페이스) (1) | 2024.01.25 |