Sunday Study

Aop 2

스프링 AOP 개념 보충, 원리, 적용법

용어 보충

조인 포인트

포인트컷

타겟

어드바이저, 어드바이스, 애스펙트

image

@Bean // @Configuration 이 붙은 클래스 내부 메소드
public Advisor advisor(Object o) {
		AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
		pointcut.setExpression("execution(* hello.proxy.app..*(..))");
		Advice advice = new SomethingImplemented(o);//Advice는 인터페이스,구현체명은 임의로 정함
		//advisor = pointcut + advice
		return new DefaultPointcutAdvisor(pointcut, advice);
}

위빙

원하는 곳에 원하는 기능을 AOP를 통해 적용하는 행위를 말함

아래 세가지에 위빙을 수행할 수 있음(단, 스프링AOP는 세 번째에만)

AOP 적용의 내부 원리

프록시 및 어드바이저 생성

image

  1. 실행: 스프링 애플리케이션 로딩 시점에 자동 프록시 생성기를 호출한다.
  2. 모든 @Aspect 빈 조회: 자동 프록시 생성기는 스프링 컨테이너에서 @Aspect 애노테이션이 붙은 스프링 빈을 모두 조회한다.
  3. 어드바이저 생성: @Aspect 어드바이저 빌더를 통해 @Aspect 애노테이션 정보를 기반으로 어드바이저를 생성한다.
  4. @Aspect 기반 어드바이저 저장: 생성한 어드바이저를 @Aspect 어드바이저 빌더 내부에 저장한다.

@Aspect 어드바이저 빌더

스프링 컨테이너 실행시 동작 (AOP 관점)

image

  1. 생성: 스프링 빈 대상이 되는 객체를 생성한다. ( @Bean , 컴포넌트 스캔 모두 포함)
  2. 전달: 생성된 객체를 빈 저장소에 등록하기 직전에 빈 후처리기에 전달한다.
  3. 3-1. Advisor 빈 조회: 스프링 컨테이너에서 Advisor 빈을 모두 조회한다. → 스프링이 기본으로 구현해놓은 어드바이저들 3-2. @Aspect Advisor 조회: @Aspect 어드바이저 빌더 내부에 저장된 Advisor 를 모두 조회한다. → 우리가 직접 구현한 어드바이저들
  4. 프록시 적용 대상 체크: 앞서 3-1, 3-2에서 조회한 Advisor 에 포함되어 있는 포인트컷을 사용해서 해당 객체가 프록시를 적용할 대상인지 아닌지 판단한다. 이때 객체의 클래스 정보는 물론이고, 해당 객체의 모든 메서드를 포인트컷에 하나하나 모두 매칭해본다. 그래서 조건이 하나라도 만족하면 프록시 적용 대상이 된다. 예를 들어서 메서드 하나만 포인트컷 조건에 만족해도 프록시 적용 대상이 된다.
  5. 프록시 생성: 프록시 적용 대상이면 프록시를 생성하고 프록시를 반환한다. 그래서 프록시를 스프링 빈으로 등록한다. 만약 프록시 적용 대상이 아니라면 원본 객체를 반환해서 원본 객체를 스프링 빈으로 등록한다.
  6. 빈 등록: 반환된 객체는 스프링 빈으로 등록된다.

프록시의 적용 내부 원리

image

원래 프록시를 사용할 수 있는 기술이 JDK Proxy와 CGLIB Proxy가 있었다.

스프링은 이 둘을 추상화 했고, 개발자는 Advice라는 인터페이스만 잘 구현하면 프록시를 원하는 대로 사용할 수 있도록 돕고 있다. (참고로 위 두 기술은 원하는 대로 교체해 사용 가능)

MethodInterceptor —(상속)—> Interceptor —(상속)—> Advice

public interface MethodInterceptor implements Interceptor {
		Object invoke(MethodInvocation invocation) throws Throwable;
}
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
		// 이전 로직
		Object result = invocation.proceed();
		// 이후 로직
		return result;
}
public void test() {
		MemberService memberService = new MemberService();
		ProxyFactory proxyFactory = new ProxyFactory(memberService);
		proxyFactory.addAdvice(new CustomAdvice()); //바로 위 예제와 같은 커스텀 어드바이스
		MemberService proxy = (MemberService) proxyFactory.getProxy();
		proxy.anyMethod(); // 임의로 추가한 내용
}

포인트 컷 종류

**포인트 컷의 표현식은 &&,   , ! 등으로 결합이 가능하다**
// 모든 공개 메서드 실행
execution(public * *(..))

// set 다음 이름으로 시작하는 모든 메서드 실행
execution(* set*(..))

// MemberService 인터페이스에 의해 정의된 모든 메서드의 실행
execution(* com.abc.service.MemberService.*(..))

// service 패키지에 정의된 메서드 실행
execution(* com.abc.service.*.*(..))

// 서비스 패키지 또는 해당 하위 패키지 중 하나에 정의된 메서드 실행
execution(* com.abc.service..*.*(..))

// 서비스 패키지 내의 모든 조인 포인트
within(com.abc.service.*)

// 서비스 패키지 또는 하위 패키지 중 하나 내의 모든 조인 포인트
within(com.abc.service..*)

// MemberService가 프록시가 인터페이스를 구현하는 모든 조인 포인트
this(com.abc.service.MemberService)

// MemberService 대상 객체가 인터페이스를 구현하는 모든 조인 포인트
target(com.abc.service.MemberService)

// 단일 매개변수를 사용하고 런타임에 전달된 인수가 Serializable과 같은 모든 조인 포인트
args(java.io.Serializable)

// 대상 객체에 @Transactional 애너테이션이 있는 모든 조인 포인트
@target(org.springframework.transaction.annotation.Transactional)

// 실행 메서드에 @Transactional 애너테이션이 있는 조인 포인트
@annotation(org.springframework.transaction.annotation.Transactional)

// 단일 매개 변수를 사용하고 전달된 인수의 런타임 유형이 @Classified 애너테이션을 갖는 조인 포인트
@args(com.abc.security.Classified)

// tradeService 라는 이름을 가진 스프링 빈의 모든 조인 포인트
bean(tradeService)

// 와일드 표현식 *Service 라는 이름을 가진 스프링 빈의 모든 조인 포인트
bean(*Service)

JoinPoint vs ProceedingJoinPoint

JoinPoint 제공 메소드

ProceedingJoinPoint에만 있는 메소드

적용 방법

코틀린

@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class LogExecutionTime

자바의 @interface 와 다르게 annotation class로 생성하는 모습

@Aspect
@Component
class ExampleAspect {
		private val log = LoggerFactory.getLogger(javaClass) // slf4j 객체 가져옴

    @Before("@annotation(LogExecutionTime)")
    fun before(joinPoint: JoinPoint) {
        log.info("before")
    }

		@After("@annotation(LogExecutionTime)")
    fun after(joinPoint: JoinPoint) {
        log.info("after")
    }

    @Around("@annotation(LogExecutionTime)")
    @Throws(Throwable::class)
    fun logExecutionTime(joinPoint: ProceedingJoinPoint): Any? { //Around라서 다른 매개변수
        val start = System.currentTimeMillis()

        var proceed = joinPoint.proceed() //꼭 호출해야지 메소드가 돌아감
        val executionTime = System.currentTimeMillis() - start

        log.info("${joinPoint.signature} excuted in ${executionTime}ms")
        return proceed // 꼭 리턴해야 다음 로직이 수행됨
	  }

    @AfterReturning(value = "@annotation(LogExecutionTime)", returning = "resource")
    fun afterReturning(resource: Any?) {
        log.info("after Returning $resource")
    }

    @AfterThrowing(value = "@annotation(LogExecutionTime)", throwing = "exception")
    fun afterThrowing(exception: Exception) {
        log.info("after Throwing", exception)
    }

}

before(), after()는 메소드 실행 전 후 로그 찍는 로직

logExecutionTime은 메소드 실행 시간을 재는 로직

afterReturning, afterThrowing은 메소드의 리턴후 혹은 예외 발생시의 모습

자바 (저번 주 예시와 같음)

@Slf4j
@Aspect
@Component
public class LogIntroduction {
    @Pointcut("execution(* com.dragonguard.backend..*Controller*.*(..))")
    public void allController() {
    }

    @Pointcut("execution(* com.dragonguard.backend..*Service*.*(..))")
    public void allService() {
    }

    @Pointcut("execution(* com.dragonguard.backend..*Repository*.*(..))")
    public void allRepository() {
    }

    @Before("allController()")
    public void controllerLog(JoinPoint joinPoint) {
        log.info(
                "METHOD : {}, ARGS : {}",
                joinPoint.getSignature().toShortString(),
                joinPoint.getArgs());
    }

    @Before("allService() || allRepository()")
    public void serviceAndRepositoryLog(JoinPoint joinPoint) {
        log.debug(
                "METHOD : {}, ARGS : {}",
                joinPoint.getSignature().toShortString(),
                joinPoint.getArgs());
    }
}