[스프링] Spring AOP
AOP : Aspect Oriented Programming
- 관점 지향 : 어떤 로직을 기준으로 핵심 관점과 부가 관점으로 나누어 모듈화 하는 방법
-
공통 관심 사항을 구현한 코드를 핵심 코드 안에 삽입하는 방식으로 구현
- 공통 관심사항 : 공통 기능(전체에 적용)
- 핵심 관심사항 : 핵심 비즈니스로직
Spring AOP 작동원리
- 프록시 패턴을 사용하여 구현
- 런타임 방식 사용 (링크-프록시), (링크-디자인패턴)
- 런타임시 클래스를 빈으로 만들 때 공통 기능이 있는 프록시 빈으로 감싸서 만든다
- 스프링 AOP는 런타임 방식이기 때문에 JointPoint가 메서드 실행지점으로만 제한된다
- 스프링 빈에만 AOP를 적용할 수 있다
빈 후처리기(BeanPostProcessor)
- 스프링은 일반적으로
@ComponentScan
으로 실제 객체를 빈으로 등록한다 - 하지만, 프록시 빈으로 감싸서 생성하기 위해서는 빈을 생성할 때 후처리를 해야한다
- 스프링부트는 자동설정으로 빈 후처리기인
AnnotationAwareAspectJAutoProxyCreator
를 빈으로 자동 등록하고 사용한다 -
AutoProxyCreator
: 자동 프록시 생성기- 스프링 빈으로 등록된
Advisor
를 자동으로 찾아서 프록시를 생성한다 - 프록시 생성시
PointCut
을 체크해 프록시로 만들지 말지 결정 @Aspect
를 Advisor로 변환하는 역할도 함- 변환된 어드바이저는
AspectJAdvisorBuilder
에 저장된다
- 스프링 빈으로 등록된
- 스프링은 빈 객체를 생성한다
- 생성된 빈 객체를
스프링 컨테이너
에 등록하기 전에빈 후처리기
에 전달한다 - 모든
Advisor
빈을 조회해 해당 빈이 적용 대상인지 확인한다
3.2@Aspect
을 통해 변환된 어드바이저도 조회해 확인한다 - 적용 대상이면 프록시를 생성하여
스프링 컨테이너
에 등록하고 아니라면 원본 객체를 등록한다
AOP 용어
Aspect
- 여러 곳에서 쓰이는 공통 기능을 모듈화 한 것
-
Aspect = Advice + PointCut (가장 큰개념)
- cf) Advisor
- 1개의 Advice + 1개의 PointCut으로 구성
- Spring AOP에서만 사용하는 용어
Target
- Aspect를 적용하는 곳
- PointCut에 의해 결정
Advice
- 실질적으로 공통 기능이 있는 구현체
- 공통 기능이 동작할 시점도 갖고 있음
동작시점 | 설명 |
---|---|
Before | 공통기능 실행 전에 동작 |
After | 공통기능 실행 후에 동작 |
After-returning | 공통기능이 정상적으로 실행된 후에 동작 |
After-throwing | 예외가 발생한 후에 동작(finally 구문) |
Around | 모든 시점에 동작 |
-
Before
: proceed() 실행 전 -
After-xx
: proceed() 실행 후 -
Around
- Before + After, After-returning, After-throwing
@Slf4j
@Aspect
public class AspectV6 {
@Around("execution(public * board..*(..))")
public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
try {
//@Before
log.info("[around][트랜잭션 시작] {}", joinPoint.getSignature());
Object result = joinPoint.proceed();
//@AfterReturning
log.info("[around][트랜잭션 커밋] {}", joinPoint.getSignature());
return result;
} catch (Exception e) {
//@AfterThrowing
log.info("[around][트랜잭션 롤백] {}", joinPoint.getSignature());
throw e;
} finally {
//@After
log.info("[around][리소스 릴리즈] {}", joinPoint.getSignature());
}
}
}
PointCut
- 공통 기능을 적용시킬 지점
-
정규표현식 or AspectJ문법 으로 정의함
- 포인트컷 적용 시기
- 프록시 객체 생성시
- 프록시 빈을 생성할 때 프록시 객체를 생성할지 원본객체를 생성할지 결정
- 어드바이저 적용시
- 프록시가 호출되었을 때 어드바이스를 적용할지 말지 결정
JoinPoint
- 공통 기능을 적용 가능한 지점들의 집합 (PointCut ⊂ JoinPoint)
- 메서드실행 지점, 생성자호출 지점, 필드값접근 지점 등
- 스프링 AOP는 런타임 방식이기 때문에 JointPoint가 메서드 실행지점으로만 제한된다
AspectJ 표현식 (PointCut 표현식)
execution([접근제어자], 리턴타입 [패키지] 메서드이름(파라미터))
-
*
: 모든값 -
..
: 0개 이상 (=0..*
)
ex) execution(* com.people..*.*(..))
- 접근제어자 : 생략
- 리턴타입 : *
- 패키지 : com.people 패키지와 하위패키지(
com.people..
)까지 포함한 모든파일
cf) com.people 패키지만 포함할 시com.people.
사용 - 메서드이름 : *
-
파라미터 : .. (0개 이상)
-
execution(* *(..))
: 모든 파일
AOP 적용
어노테이션
- 스프링
- xml : <aop:aspectj-autoproxy /> 설정
- config : @EnableAspectJAutoProxy 설정
- 스프링부트
- 자동으로
AnnotationAwareAspectJAutoProxyCreator
를 스프링 빈등록 함
- 자동으로
-
@Aspect
: 공통 기능의 클래스(Aspect) 설정-
AnnotationAwareAspectJAutoProxyCreator
에 의해 어드바이저로 변환되어AspectJAdvisorBuilder
에 저장
-
-
@Pointcut
: 적용할 범위(PointCut) 설정 -
@Around
: Around Advice 설정-
@Around("execution(* hello.hellospring..*(..))")
: hello.hellospring 패키지 아래의 모든 메서드에 적용 -
@Around("@annocation(LogExecutionTime)")
: @LogExecutionTime이 붙은 메서드에 적용 -
@Around("메서드")
: Pointcut을 설정한 메서드
-
@Aspect
public class ProfilingAspect {
@Around("execution(public * board..*(..))")
public Object trace(ProceedingJoinPoint joinPoint) throws Throwable {
String message = joinPoint.getSignature().toShortString();
System.out.println(message + " 시작");
long start = System.currentTimeMillis();
Object result = joinPoint.proceed(); // 핵심 로직
long finish = System.currentTimeMillis();
System.out.println("종료 실행 시간 : " + (finish - start) + "ms");
return result;
}
}
-
joinPoint.proceed()
: 실제 타겟 객체의 핵심 로직을 수행 - Spring은 타겟 객체의 프록시를 만들며 호출시 프록시에 먼저 접근하고
proceed()
로 실제 타겟 객체인 핵심로직 호출
XML
- <aop:config> : AOP 설정
- <aop:aspect> : Aspect 설정
- ref : 공통 기능의 클래스
- <aop:pointcut> : PointCut 설정
- expression : PointCut 표현식
- <aop:around> : Around Advice 설정
- pointcut-ref : 적용할 PointCut, method : 적용할 메서드 (Profiler 클래스의 trace()에 적용)
<bean id="profiler" class="ch07.Profiler" />
<aop:config>
<aop:aspect id="traceAspect" ref="profiler">
<aop:pointcut id="publicMethod" expression="execution(public * board..*(..))"/>
<aop:around pointcut-ref="publicMethod" method="trace"/>
</aop:aspect>
</aop:config>
→ Profiler 클래스의 trace()를 publicMethod의 지점에서 Around 시점으로 실행함
프록시와 내부호출
- AOP를 적용하기 위해서는 프록시 객체를 통해 대상객체(target)이 호출되어야 한다
- 프록시를 거치지 않고 대상객체를 직접 호출하게 된다면 공통 로직을 거치지 않게 된다
@Slf4j
public class CallService {
public void external() {
log.info("call external");
internal(); // 내부 메서드 호출(this.internal())
}
public void internal() {
log.info("call internal");
}
}
@Slf4j
@Aspect
public class CallLogAspect {
@Before("execution(* hello.aop.internalcall..*.*(..))")
public void doLog(JoinPoint joinPoint) {
log.info("aop={}", joinPoint.getSignature());
}
}
public class Main {
public static void main(String[] args) {
CallService service = new CallService();
service.external();
service.internal();
}
}
// external()
CallLogAspect : aop=void hello.aop.internalcall.CallService.external()
CallService : call external
CallService : call internal // AOP호출 X
// internal()
CallLogAspect : aop=void hello.aop.internalcall.CallService.internal()
CallService : call internal
-
external()
에서internal()
을 호출할 때는 자신의 실제 객체에서 호출한다
해결방안
- 메서드를 다른 클래스로 분리한다
@Slf4j
public class CallService {
private final InternalService internalService;
CallService(InternalService internalService) {
internalService = new InternalService();
}
public void external() {
log.info("call external");
internalService.internal(); // 외부 메서드 호출
}
}
@Slf4j
public class InternalService {
public void internal() {
log.info("call internal");
}
}
public class Main {
public static void main(String[] args) {
CallService service = new CallService();
service.external();
}
}