AOP(Aspect Oriented Programming)
AOP는 여러 객체에 공통으로 적용할 수 있는 기능을 분리해서 재사용성을 높여주는 프로그래밍 기법이다.
핵심 기능과 공통 기능의 구현을 분리함으로써 핵심 기능을 구현한 코드의 수정 없이 공통 기능을 적용할 수 있게 만들어 준다.
아래 예제와 함께 이해해보도록 하자.
1. 프로젝트 준비
메이븐 프로젝트의 pom.xml 파일에는 다음과 같이 aspectjweaver 의존을 추가한다.
이 모듈은 스프링이 AOP를 구현할 때 사용하는 모듈이다.
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.13</version>
</dependency>
</dependencies>
스프링 프레임워크의 AOP 기능은 spring-aop 모듈이 제공하는데 spring-context 모듈을 의존 대상에 추가하면 spring-aop 모듈도 함께 의존 대상에 포함된다.
따라서 spring-aop 모듈에 대한 의존을 따로 추가하지 않아도 된다.
aspectjweaver 모듈은 AOP를 설정하는데 필요한 애너테이션을 제공하기 때문에 이 의존을 추가해야 한다.
아래 코드는 계승을 구하기 위한 인터페이스를 정의한다.
public interface Calculator {
public long factorial(long num);
}
Calculator 인터페이스를 구현한 첫 번째 클래스는 다음과 같이 for 문을 이용해서 계승값을 구했다.
public class ImpeCalculator implements Calculator {
@Override
public long factorial(long num) {
long result = 1;
for (long i = 1; i <= num; i++) {
result *= i;
}
return result;
}
}
Calculator 인터페이스를 구현한 두 번째 클래스는 재귀호출을 이용했다.
public class RecCalculator implements Calculator {
@Override
public long factorial(long num) {
if (num == 0)
return 1;
else
return num * factorial(num - 1);
}
}
2. 프록시와 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 *= i;
}
long end = System.currentTimeMillis();
System.out.println("ImpeCalculator.factorial(%d) 실행 시간= %d\n", num, (end-start));
return result;
}
}
RecCalculator 클래스는 factorial() 메서드를 재귀 호출로 구현해서 실행 시간이 총 3번이나 출력된다.
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.println("ImpeCalculator.factorial(%d) 실행 시간= %d\n", num, (end-start));
}
}
}
메서드를 호출할 때마다 실행 전 후 값을 구하자면 중복되는 코드가 많아지는 문제가 발생한다.
이처럼 기존 코드를 수정하지 않고 코드 중복도 피할 수 있는 방법이 프록시 객체이다.
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 필드에 할당하고 factorial() 메서드에서 delegate.factorial() 메서드를 실행한다.
그리고 delegate.facotrial()의 코드를 실행하기 전후에 현재 시간을 구해 차이를 출력한다.
위의 흐름도를 보면 ExeTimeCalculator 클래스의 factorial() 메서드는 결과적으로 ImpeCalculator의 factorial() 메서드의 실행 시간을 구해서 콘솔에 출력하게 된다.
아래 코드에서 각각 ImpeCalculator 객체와 RecCalculator 객체를 이용해서 ExeTimeCalculator 를 생성한다.
따라서 ttCal1.facotorial(20)은 ImpeCalculator 객체의 facotorial(20) 실행 시간을 출력하고, ttCal2.facotorial(20)은 RecCalculator 객체의 facotorial(20) 실행 시간을 출력한다.
import chap07.ImpeCalculator;
import chap07.RecCalculator;
import chap07.ExeTimeCalculator;
public class MainProxy {
public static void main(String[] args) {
ExeTimeCalculator ttCal1 = new ExeTimeCalculator(new ImpeCalculator());
System.out.println(ttCal1.factorial(20));
ExeTimeCalculator ttCal2 = new ExeTimeCalculator(new RecCalculator());
System.out.println(ttCal2.factorial(20));
}
}
위 결과 다음을 알 수 있다.
1. 기존 코드를 변경하지 않고 실행 시간을 출력할 수 있다. ImpeCalculator 클래스나 RecCalculator 클래스의 코드 변경 없이 이 클래스의 factorial() 메서드 실행 시간을 출력할 수 있게 되었다.
2. 실행 시간을 구하는 코드의 중복을 제거했다. 나노초 대신에 밀리초를 사용해서 실행 시간을 구하고 싶다면 ExeTimeCalculator 클래스만 변경하면 된다.
이렇게 핵심 기능의 실행은 다른 객체에 위임하고 부가적인 기능을 제공하는 객체를 프록시(proxy)라고 부른다.
프록시의 특징은 핵심 기능은 구현하지 않는다는 점이다.
ImpeCalculator 클래스나 RecCalculator 클래스는 팩토리얼 연산이라는 핵심 기능을 구현하고 있다.
반면에 ExeTimeCalculator 클래스는 팩토리얼 연산 자체를 구현하고 있지 않다.
프록시는 핵심 기능을 구현하지 않는 대신 여러 객체에 공통으로 적용할 수 있는 기능(실행 시간 측정 기능)을 구현한다.
AOP의 기본 개념은 핵심 기능에 공통 기능을 삽입하는 것이고, 핵심 기능의 코드를 수정하지 않으면서 공통 기능의 구현을 추가하는 것이다.
- 컴파일 시점에 코드에 공통 기능을 삽입하는 방법
- 클래스 로딩 시점에 바이트 코드에 공통 기능을 삽입하는 방법
- 런타임에 프록시 객체를 생성해서 공통 기능을 삽입하는 방법
스프링이 제공하는 AOP 방식은 프록시를 이용한 세 번째 방식이다.
3. 스프링 AOP 구현
스프링 AOP를 이용해서 공통 기능을 구현하고 적용하는 방법은 다음과 같은 절차를 따르면 된다.
- Aspect로 사용할 클래스에 @Aspect 애노테이션을 붙인다.
- @Pointcut 애노테이션으로 공통 기능을 적용할 Pointcut을 정의한다.
- 공통 기능을 구현한 메서드에 @Autowired 애노테이션을 적용한다.
3.1 @Aspect, @Pointcut, @Around를 이용한 AOP 구현
공통 기능을 제공하는 Aspect 구현 클래스를 만들고 자바 설정을 이용해서 Aspect를 어디에 적용할지 설정하면 된다.
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;
import org.springframework.core.annotation.Order;
@Aspect
// @Order(1)
public class ExeTimeAspect {
@Pointcut("execution(public * chap07..*(..))")
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.printf("%s.%s(%s) 실행 시간 : %d ns\n",
joinPoint.getTarget().getClass().getSimpleName(),
sig.getName(), Arrays.toString(joinPoint.getArgs()),
(finish - start));
}
}
}
@Aspect 애노테이션을 적용한 클래스는 Advice와 Pointcut을 함께 제공한다.
@Pointcut은 공통 기능을 적용할 대상을 설정한다.
@Around 애노테이션은 Around Advice를 설정한다.
@Around 애노테이션의 값이 "publicTarget()"인데 이는 publicTarget() 메서드에 정의한 Pointcut에 공통 기능을 적용한다는 것을 의미한다.
publicTarget() 메서드는 chap07 패키지와 그 하위 패키지에 위치한 public 메서드에 @Around가 붙은 measure() 메서드를 적용한다.
measure() 메서드의 ProceedingJoinPoint 타입 파라미터는 프록시 대상 객체의 메서드를 호출할 때 사용한다.
proceed() 메서드를 사용해서 실제 대상 객체의 메서드를 호출한다.
이 메서드를 호출하면 대상 객체의 메서드가 실행되므로 이 코드 이전과 이후에 공통 기능을 위한 코드를 위치시키면 된다.
'WEB > SPRING' 카테고리의 다른 글
Springboot properties 파일 분리 (local, prod, ...) (0) | 2023.03.17 |
---|---|
Springboot에서 java-email 활용하기 (0) | 2022.11.21 |
[Spring] 빈(Bean) 🟢 (0) | 2022.06.03 |
[Spring] 컴포넌트 스캔 (0) | 2022.06.03 |
[Spring] @Configuration 설정 클래스의 @Bean 설정과 싱글톤 (0) | 2022.06.03 |
댓글