안녕하세요 개발자 문문입니다.
오늘은 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)
- CacheAspect의 execute 메서드 호출 > 첫번째 호출이므로 조건문에 부합하지 않음, jointPoint.proceed() 메서드 수행
- ExeTimeAspect의 measure 메서드 호출 > jointPoint.proceed() 메서드 수행
- 실제 객체의 factorial 메서드 호출
- ExeTimeAspect의 실행 시간 로직 수행
- CacheAspect의 cache 적재 로직 수행
(2)
- 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{
... }
...
'Spring 개념정리' 카테고리의 다른 글
[Spring] MVC : 요청 매핑, 커맨드 객체, 리다이렉트, 모델 (2) | 2025.07.06 |
---|---|
[Spring] 스프링 MVC 프레임워크 동작 방식 (0) | 2025.07.02 |
[Spring] 빈 라이프사이클과 범위 (2) | 2025.06.30 |
[Spring] 컴포넌트 스캔 (1) | 2025.06.30 |
[Spring] 스프링 DI(Dependency Injection) (1) | 2025.06.24 |