AOP : Aspect Oriented Programming

  • 관점 지향 : 어떤 로직을 기준으로 핵심 관점부가 관점으로 나누어 모듈화 하는 방법
  • 공통 관심 사항을 구현한 코드를 핵심 코드 안에 삽입하는 방식으로 구현

  • 공통 관심사항 : 공통 기능(전체에 적용)
  • 핵심 관심사항 : 핵심 비즈니스로직

Spring AOP 작동원리

  • 프록시 패턴을 사용하여 구현
  • 런타임 방식 사용 (링크-프록시), (링크-디자인패턴)
    • 런타임시 클래스를 빈으로 만들 때 공통 기능이 있는 프록시 빈으로 감싸서 만든다
  • 스프링 AOP는 런타임 방식이기 때문에 JointPoint가 메서드 실행지점으로만 제한된다
  • 스프링 빈에만 AOP를 적용할 수 있다

image

빈 후처리기(BeanPostProcessor)

  • 스프링은 일반적으로 @ComponentScan으로 실제 객체를 빈으로 등록한다
  • 하지만, 프록시 빈으로 감싸서 생성하기 위해서는 빈을 생성할 때 후처리를 해야한다
  • 스프링부트는 자동설정으로 빈 후처리기인 AnnotationAwareAspectJAutoProxyCreator를 빈으로 자동 등록하고 사용한다
  • AutoProxyCreator : 자동 프록시 생성기
    • 스프링 빈으로 등록된 Advisor를 자동으로 찾아서 프록시를 생성한다
    • 프록시 생성시 PointCut을 체크해 프록시로 만들지 말지 결정
    • @Aspect 를 Advisor로 변환하는 역할도 함
    • 변환된 어드바이저는 AspectJAdvisorBuilder에 저장된다

  1. 스프링은 빈 객체를 생성한다
  2. 생성된 빈 객체를 스프링 컨테이너에 등록하기 전에 빈 후처리기에 전달한다
  3. 모든 Advisor빈을 조회해 해당 빈이 적용 대상인지 확인한다
    3.2 @Aspect을 통해 변환된 어드바이저도 조회해 확인한다
  4. 적용 대상이면 프록시를 생성하여 스프링 컨테이너에 등록하고 아니라면 원본 객체를 등록한다

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문법 으로 정의함

  • 포인트컷 적용 시기
  1. 프록시 객체 생성시
    • 프록시 빈을 생성할 때 프록시 객체를 생성할지 원본객체를 생성할지 결정
  2. 어드바이저 적용시
    • 프록시가 호출되었을 때 어드바이스를 적용할지 말지 결정

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

image

  • 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();
	}
}