Spring 개념정리

[Spring] AOP(Aspect Oriented Programming)

개발자 문문 2025. 7. 1. 23:13

안녕하세요 개발자 문문입니다.

오늘은 AOP 공부를 해보겠습니다!

 

  • AOP(Aspect Oriented Programming)란?

-  AOP는 여러 객체에 공통으로 적용할 수 있는 기능을 분리해서 재사용성을 높여주는 프로그래밍 기법을 의미합니다.

-  AOP는 핵심 기능에 공통 기능을 삽입하는 것 입니다

- 스프링에서 제공하는 AOP 방식은 프록시 객체를 이용합니다.

 

  • 프록시(Proxy) : 대리자,  클라이언트가 사용하려고 하는 실제 대상인 것처럼 위장해서 클라이언트의 요청을 받아주는 역할을 합니다. 

   - 스프링 AOP는 프록시 객체를 자동으로 만들어 주기 때문에 직접 구현할 필요는 없습니다.

 

  • AOP 주요 용어
용어 의미
Advice 언제 공통 관심 기능을 핵심 로직에 적용할지를 정의합니다. 예를 들어 "메서드 호출 전(언제) 트랜잭션 시작(공통 기능)" 기능을 적용한다는 것을 정의 합니다.
Joinpoint Advice를 적용 가능한 지점을 의미합니다. 메서드 호출, 필드 값 변경 등이 이에 해당합니다. 스프링은 프록시를 이용해서 AOP를 구현하기 때문에 호출에 대한 Joinpoint만 지원합니다.
Pointcut Joinpoint의 부분 집합으로서 실제 Advice가 적용되는 Joinpoint를 나타냅니다. 스프링에서는 정규 표현식이나 AspectJ의 문법을 이용하여 Pointcut을 정의 할 수 있습니다.
Weaving Advice를 핵심 로직 코드에 적용하는 것을 weaving이라고 합니다.
Aspect 여러 객체에 공통으로 적용되는 기능을 Aspect라고 합니다. 트랜잭션이나 보안등이 있습니다.

 

  • Advice의 종류
종류 의미
Before Advice 대상 객체의 메서드 호출 전에 공통 기능을 실행합니다.
After Returning Advice 대상 객체의 메서드가 익셉션 없이 실행된 이후에 공통 기능을 실행합니다.
After Throwing Advice 대상 객체의 메서드를 실행하는 도중 익셉션이 발생한 경우에 공통 기능을 실행합니다.
After Advice 익셉션 발생 여부에 상관없이 대상 객체의 메서드 실행 후 공통 기능을 실행합니다.
Around Advice 대상 객체의 메서드 실행 전, 후 또는 익셉션 발생 시점에 공통 기능을 실행하는데 사용됩니다.

 

  • AOP 구현
@Aspect
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));
		}
		
	}
}

 

- @Pointcut : 공통 기능을 적용할 대상을 설정합니다. 여기서는 chap07 패키지와 그 하위 패키지에 위치한 타입의 public 메서드를 대상으로 지정했습니다.

 

- execution 명시자 표현식 : execution(수식어패턴? 리턴타입패턴 클래스이름패턴?메서드이름패턴(파라미터패턴)) → 수식어패턴은 생략가능

 

- @Around : publicTarget() 에서 지정한 대상( chap07, 하위 패키지)에 measure 메서드를 적용합니다. 

 

- ProceedingJoinPoint.proceed() : 실제 대상 객체의 메서드를 호출할 때 사용합니다. 이 메서드의 이전과 이후에 공통 기능을 위한 코드를 작성하면 됩니다.

 

- getTarget() : 대상 객체

 

- getSignature() : 메서드의 시그니처(메서드이름 + 파라미터)

 

  • 스프링 설정 클래스
@Configuration
@EnableAspectJAutoProxy
public class AppCtx {
	
	@Bean
	public ExeTimeAspect exeTimeAspect()
	{
		return new ExeTimeAspect();
	}

	@Bean
	public Calculator calculator()
	{
		return new RecCalculator();
	}
}

 

- @EnableAspectJAutoProxy : @Aspect 애노테이션을 붙인 클래스를 공통 기능을 적용하기 위해 사용합니다.

- RecCalculator : 인터페이스 Calculator의 구현 클래스

 

  • 실행 및 결과
public class MainAspect {

	public static void main(String[] args) {
		AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AppCtx.class);
		
		Calculator cal = ctx.getBean("calculator", Calculator.class);
		long fiveFact = cal.factorial(5);
		System.out.println("cal.factorial(5) = "+fiveFact);
		System.out.println(cal.getClass().getName());
		ctx.close();
	}
}

 

 

- 결과의 마지막 행을 보면 $Proxy17 라고 출력된게 보입니다.

- 이 타입은 스프링이 생성한 프록시 타입입니다. → 스프링 AOP를 사용하면 프록시 객체를 사용합니다. 

 

  • 프록시 생성 방식

- 위 예제의 main에서 Calculator가 아닌 그 구현 클래스 RecCaculator로 변경하면 어떻게 될까요?

- 변경 후 실행을 하면 아래와 같은 에러가 발생합니다.

Exception in thread "main" org.springframework.beans.factory.BeanNotOfRequiredTypeException: Bean named 'calculator' is expected to be of type 'chap07.RecCalculator' but was actually of type 'com.sun.proxy.$Proxy17'

- 이러한 에러가 발생한 이유는 스프링은 프록시 객체를 생성할 때 생성할 빈 객체가 인터페이스를 상속하면 인터페이스를 이용해서 프록시를 생성하기 때문입니다.

- 즉, 위에서 가져온 빈 "calculator"의 타입은 Calculator를 상속받은 프록시 타입이 됩니다.

- 만약에 인터페이스가 아닌 클래스로 프록시를 생성하고 싶다면 @EnableAspectJAutoProxy(proxyTargetClass = true) 속성을 사용하면 됩니다.

 

  • Advice 적용 순서

- @AspectJ 클래스를 하나 더 추가 해보겠습니다.

@Aspect
public class CacheAspect {

	private Map<Long, Object> cache = new HashMap<>();
	
	@Pointcut("execution(public * chap07..*(long))")
	public void cacheTarget() {
		
	}
	
	@Around("cacheTarget()")
	public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
		Long num = (Long) joinPoint.getArgs()[0];
		
		if(cache.containsKey(num)) {
			System.out.printf("CacheAspect: Cache에서 구함 [%d]\n", num);
			return cache.get(num);
		}
		
		Object result = joinPoint.proceed();
		cache.put(num, result);
		System.out.printf("CacheAspect : Cache에 추가 [%d]\n",num);
		return result;
	}
}

 

- 스프링 설정 클래스

@Configuration
@EnableAspectJAutoProxy
public class AppCtxWithCache {

	@Bean
	public CacheAspect cacheAspect()
	{
		return new CacheAspect();
	}
	
	@Bean
	public ExeTimeAspect exeTimeAspect()
	{
		return new ExeTimeAspect();
	}
	
	@Bean
	public Calculator calculator()
	{
		return new RecCalculator();
	}
	
}

 

- 실행 코드 및 결과

public class MainAspectWithCache {
	
	public static void main(String[] args)
	{
		AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AppCtxWithCache.class);
		
		Calculator cal = ctx.getBean("calculator", Calculator.class);
		cal.factorial(7);
		cal.factorial(7);
		cal.factorial(5);
		cal.factorial(5);
		ctx.close();
	}

}

 

RecCalculator.factorial([7])실행 시간 : 15500 ns

CacheAspect : Cache에 추가 [7] 

----------------------------------------------------------------------------------------(1)

CacheAspect: Cache에서 구함 [7]

----------------------------------------------------------------------------------------(2)

RecCalculator.factorial([5])실행 시간 : 12000 ns

CacheAspect : Cache에 추가 [5]

----------------------------------------------------------------------------------------(3)

CacheAspect: Cache에서 구함 [5]

 

---(1),(2),(3)은 제가 설명을 위해 추가한 구분선 입니다. 

 

- 첫번째 cal.factorial(7) 실행 시에는 CacheAspect와 ExeTimeAspect의 공통 기능 메서드가 둘 다 호출 됩니다.

- 하지만 두번째 cal.factorial(7) 실행 시에는 CacheAspect의 공통 기능 메서드만 호출 됩니다.

- 이유는 Advice 순서를 CacheAspect 프록시 > ExeTimeAspect 프록시 > 실제 대상 객체 순으로 적용했기 때문입니다.

- 이런 경우 순서는 다음과 같습니다.

  (1)

  1. CacheAspect의 execute 메서드 호출 > 첫번째 호출이므로 조건문에 부합하지 않음, jointPoint.proceed() 메서드 수행
  2. ExeTimeAspect의 measure 메서드 호출 > jointPoint.proceed() 메서드 수행
  3. 실제 객체의 factorial 메서드 호출
  4. ExeTimeAspect의 실행 시간 로직 수행
  5. CacheAspect의 cache 적재 로직 수행

 (2)

  1. CacheAspect의 execute 메서드 호출 > Map에 데이터가 존재하므로 cache.get(num) 리턴

 

  • @Order

- @Order를 사용하면 Aspect 적용 순서를 직접 결정할 수 있습니다.

- @Aspect 클래스에 @Order(1) ,@Order(2) 이런식으로 지정하면 작은 값 부터 큰 값 순서로 결정됩니다.

 

  • @Pointcut 재사용

- 만약 같은 PointCut을 여러 Advice가 함께 사용한다면 공통 PointCut을 재사용할 수 도 있습니다.

- 위의 예제에서 ExeTimeAspect의 publicTarget()을 public으로 변경하면 외부에서 아래와 같이 사용할 수 있습니다.

@Aspect
public class ExeTimeAspect {

	@Pointcut("execution(public * chap07..*(..))")
	public void publicTarget()
	{	
	}
...
@Aspect
public class CacheAspect {

	@Around("aspect.ExeTimeAspect.publicTarget()")
    public Object execute(ProceedingJoinPoint jointPoint) throws Throwable{
    ... }
    
    ...