AOP(Aspect-Oriented Programming)는 핵심 로직과 부가 기능을 분리하여 애플리케이션 전체에 걸쳐 사용되는 부가 기능을 모듈화
하여 재사용할 수 있도록 지원하는 것이다.
Aspect-Oriented Programming를 해석하면 관점(관심) 지향 프로그래밍 이 되는데, 이는 프로젝트 구조를 바라보는 관점을 바꿔보자는 의미이다.
Java에서 객체지향프로그래밍인 OOP는 목적에 따라 클래스, 객체를 만드는 방식으로, 핵심 비즈니스 로직, 부가기능 로직 등 하나의 객체로 분리하는 방식인데, 이를 관점의 시선에서 바라보는 방식이 AOP다.
사실 말로 설명하면 이해가 잘 안되니 사진 하나를 보겠다.
위의 사진에서는 핵심 기능으로 이체, 대출, 이자계산 등이 있다. 해당 서비스 로직에서 우리가 공통적으로 필요한 것은 로깅, 보안, 트랜잭션들이 있다.
이처럼 핵심 기능을 도화주는 부가적인 기능인 부가 기능을 기존 핵심기능에서 분리하고 재사용하는 것을 AOP라고 생각하면 될것이다.
위의 로깅, 보안, 트랜잭션과 같이 공통된 부분을 잘라낸것을 AOP에서 크로스 컷팅(Cross-Cutting)이라고 한다.
AOP의 장점
Advice가 적용될 수 있는 모든 위치.
메서드 실행 시점
솔직히 말장난같다...🤣 그림을 보고 이해해보자
Spring AOP는 기본적으로 프록시 방식으로 동작한다. 프록시 패턴이란 어떤 객체를 사용하고자 할 때, 객체를 직접적으로 참조 하는 것이 아니라, 해당 객체를 대행(대리, proxy)하는 객체를 통해 대상객체에 접근하는 방식을 말한다.
프록시 객체 없이 Target 객체를 사용하고 있다고 생각해보자. Aspect 클래스에 정의된 부가 기능을 사용하기 위해서, 우리는 원하는 위치에서 직접 Aspect 클래스를 호출해야 한다. 이 경우 Target 클래스 안에 부가 기능을 호출하는 로직이 포함되기 때문에, AOP를 적용하지 않았을 때와 동일한 문제가 발생한다. 여러 곳에서 반복적으로 Aspect를 호출해야 하고, 그로 인해 유지보수성이 크게 떨어진다.
그래서 Spring에서는 Target 클래스 혹은 그의 상위 인터페이스를 상속하는 프록시 클래스를 생성하고, 프록시 클래스에서 부가 기능에 관련된 처리를 한다. 이렇게 하면 Target에서 Aspect을 알 필요 없이 순수한 비즈니스 로직에 집중할 수 있다.
아래에 예시를 들어 설명하겠다.
public interface TargetService{
void logic();
}
@Service
public class TargetServiceImpl implements TargetService{
@Override
public void logic() {
...
}}
logic() 메서드가 타겟일때,
@Service
public class TargetServiceProxy implements TargetService{
// 지금은 구현체를 직접 생성했지만, 외부에서 의존성을 주입 받도록 할 수 있다.
TargetService targetService = new TargetServiceImpl();
...
@Override
public void logic() {
// Target 호출 이전에 처리해야하는 부가 기능
// Target 호출
targetService.logic();
// Target 호출 이후에 처리해야하는 부가 기능
}
}
Proxy 에서 타겟 전후에 부가 기능을 처리하고 타겟을 호출한다.
@Service
public class UseService{
// 지금은 구현체를 직접 생성했지만, 외부에서 의존성을 주입 받도록 할 수 있다.
TargetService targetService = new TargetServiceProxy();
...
public void useLogic() {
// Target 호출하는 것처럼 부가 기능이 추가된 Proxy를 호출한다.
targetService.logic();
}
}
이경우 사용자 입장에서 타겟을 사용하는 것 처럼 Proxy를 사용할 수 있다.
메서드 실행시간 측정을 해볼것이다.
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ExeTimer {
}
위에 작성한 어노테이션 @ExeTimer가 붙은 메서드에 대해 AOP가 수행된다.
@Slf4j
@Aspect
@Component
public class ExecutionTimer {
// 조인포인트를 어노테이션으로 설정
@Pointcut("@annotation(com.aop.annotation.ExeTimer)")
private void timer(){};
// 메서드 실행 전,후로 시간을 공유해야 하기 때문
@Around("timer()")
public void AssumeExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
joinPoint.proceed(); // 조인포인트의 메서드 실행
stopWatch.stop();
long totalTimeMillis = stopWatch.getTotalTimeMillis();
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
String methodName = signature.getMethod().getName();
log.info("실행 메서드: {}, 실행시간 = {}ms", methodName, totalTimeMillis);
}
}
이제 @ExeTimer를 메서드에 걸어주기만 하면 된다!.
참고