ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Spring Doc - Core - AOP
    Docs/Spring 2021. 5. 3. 17:10

    Spring framework Document 읽기

    Aspect Oriented Programming with Spring

    • OOP의 핵심 단위로 객체(class)가 있다면 AOP의 핵심 단위로는 Aspect가 있습니다.
    • AOP는 DI만큼이나 Spring의 핵심 기능입니다.
    • Spring은 "schema-based" 방식과 "@AspectJ" 방식으로 간편하게 커스텀 aspect를 만드는것을 지원합니다.

    AOP Concepts

    • 용어 정리

      전체 코드 중에서 aop를 적용 가능한 위치들(Joinpoint)
      "Aservice.Afunction"에 aop를 적용시킬 것이다(Pointcut)
      "Aservice.Afunction"이 실행되기 전,후에("around") "로깅"을 할것이다(Advice)
      -> 로깅을 위해서 Aservice.Afunction에 aop를 적용할 것이다 (Aspect)

      1. Aspect
        • 여러 클래스를 가로지르는 관점의 단위
        • Spring AOP의 aspect는 "schema-based" 방식과 "@AspectJ" 방식으로 구현할 수 있습니다.
      2. Join point
        • 실행중인 프로그램의 메소드의 실행, 예외의 처리 같은 어떠한 지점들, AOP를 적용시킬 후보들
        • Spring AOP에서는 method 실행 에만 가능합니다.
      3. Advice
        • 선택한 join point (=pointcut) 에서 수행할 동작
        • @Before, @AfterReturning, @AfterThrowing, @After(finally) @Around로 동작을 수행할 위치를 지정가능
      4. Pointcut
        • Joint point 중에서 aop를 적용하기 위해 선택한 지점
      5. Introduction
        • 프록시 대상에 메소드나 필드를 추가해주는 방법
      6. Target object (Advised Object)
        • Aspect에 의해 다뤄지는 객체, Spring에서는 언제나 runtime에 proxy 객체 로 생성됩니다.
      7. AOP proxy
        • Aspect를 적용하기 위해 AOP 프레임워크가 생성한 proxy 객체
        • Spring에서는 JDK dynamic proxy 또는 CGLIB proxy 가 사용됩니다.
          • JDK dynamic proxy는 JDK에 포함되어있고 CGLIB는 오픈소스지만 spring-core에 repackage 되어있습니다.
          • Proxy 대상이 interface의 구현체라면 JDK dynamic proxy를 사용하며 그렇지 않다면 CGLIB가 사용됩니다.
          • JDK dynamic proxy는 reflection을 사용해서 proxy object를 생성하기 때문에 성능 저하의 원인이 될 수 있으며
            CGLIB는 상속을 이용해서 proxy object를 생성하기 때문에 final이나 private, static method에 대한 AOP가 불가능 합니다.
      8. Weaving
        • Target object를 생성하기 위해서 aspect를 다른 application이나 객체와 연결하는 행위
        • AspectJ 컴파일러를 사용해서 compile time 에 완료될 수 있고, Spring AOP 처럼 run time 에 수행될 수도있습니다.

    Spring AOP의 기능과 목표

    • Spring AOP는 pure java로 구현되어있기 때문에 추가적인 컴파일 설정이 필요 없습니다.

    • 현재 method에 관련된 join point만 제공하기때문에 field interception이 필요하다면 AspectJ를 사용해야합니다.

    • 다른 AOP 프레임워크와 다른점은 Spring IoC 컨테이너와 결합하여 사용하기 쉽게하기 위해 완벽한 AOP를 구현하지 않는다는 점입니다.

      • Method excution에 대한 처리만 가능하다는점, proxy 방식으로만 동작한다는 점을 의미합니다.
      • 완벽한 AOP를 위해서는 AspectJ같은 AOP 프레임워크가 필요하며, 필요시 Spring기반의 Spring AoP + AspectJ 통합 환경을 구성할 수 있습니다.
      • 상세한 aop 설정은 불가능 하지만, enterprise java 환경에서 필요로 하는 일반적인 설정들은 간단하게 가능합니다.

        Spring의 핵심 원칙중 하나는 비침습성(non-invasiveness)으로 spring에 의존적인 코드를 사용하는것을 강제하지 않습니다. Spring AOP, AspectJ 혹은 둘다 사용하는것 모두 사용자가 선택할 수 있고, @AspectJ 스타일의 설정, xml 스타일의 설정 등도 모두 선택하여 사용할 수 있습니다.

    • Spring AOP는 proxy 기반 이기 때문에 객체내 호출(self invacation)은 가로채지 못합니다.

      • 객체 내 호출에 AOP를 적용하기 위해서는 native AspectJ weaving을 사용하거나
        AopContext를 통해 직접 proxy 객체를 다시 꺼내와서 호출하는 방법이 있습니다.

        // 참고용 커스텀 클래스
        public class AopUtils {
          public static <T> T proxy(T current) {
              try {
                  return (T)AopContext.currentProxy();
              } catch (IllegalStateException e) {
                  return current;
              }
          }
        }
        @component
        public class ProxyTest {
        
          public Object getValue() {
            //getCachedValue를 직접 호출하면 this.getCachedValue()가 호출되기 때문에 @Cacheable이 동작하지 않습니다.
            return AopUtils.proxy(this).getCachedValue();
          }
        
          @Cacheable(CACHE_KEY)
          public Object getCachedValue() {
            //return value
          }
        }
        • 위의 방식을 사용하기 위해서는 @EnableAspectJAutoProxy(exposeProxy = true)등의 방식으로 AopContext를 노출시켜줘야 합니다.

    @AspectJ

    • @AspectJ는 AspectJ의 어노테이션이지만, spring에서 pointcut을 표현하는데 사용할 수 있습니다.

      • @AspectJ 어노테이션을 사용해도 여전히 pure Spring AOP로 동작하기 때문에 AspectJ의 컴파일러나 위버(weaver), 의존성 추가가 필요하지 않습니다.

        AspectJ의 의존성 추가가 필요 없다는 의미로 spring aop의 의존성은 당연히 필요합니다.

    • @AspectJ 어노테이션을 사용하기 위해선 configuration에 @EnableAspectJAutoProxy를 추가해주면 됩니다.

      • Auto-proxying이란 aspect에 의해 advised되는 bean을 자동으로 proxy객체로 생성함을 의미합니다.

        @Aspect

      • @Aspect를 사용해서 aspect를 만들어 pointcut, advice, introduction 등을 선언 할 수 있습니다.

      • @Aspect어노테이션은 classpath에 존재한다고 해도 component scan을 통해서 자동으로 감지되지 않으며 @component등을 추가로 붙여서 감지되도록 해야 합니다.

      • @Aspect는 auto-proxying의 대상에서 제외하는 마커 인터페이스로 사용되기 때문에 다른 aspect의 대상이 되지 못합니다.

        @Pointcut

      • @Pointcut은 advice를 적용할 위치를 찾는데 사용되며 expression과 signature 두 파트로 나뉩니다.

        
          // 모든 public 메서드의 경우
          @Pointcut("execution(public * (..))")             // the pointcut expression
          private void anyPublicOperation() {}              // the pointcut signature
        
          // com.xyz.someapp.trading 패키지에 포함된 경우
          @Pointcut("within(com.xyz.someapp.trading..)")    // the pointcut expression
          private void inTrading() {}                       // the pointcut signature
        
          // com.xyz.someapp.trading 패키지에 포함되는 모든 public 메서드
          @Pointcut("anyPublicOperation() && inTrading()")  // the pointcut expression
          private void tradingOperation() {}                // the pointcut signature
        • Expression은 AspectJ5의 pointcut 표현식을 따릅니다.
        • Signature의 return type은 반드시 void 여야 합니다.
      • PointCut Designators(PCD)

        • Spring에서는 method excution만 지원하기 때문에 아래 종류의 PCD만 제공합니다.
          PCD
          • Spring에서는 추가적으로 bean name으로 적용할 수 있는 bean PCD를 제공합니다.
        • PCD는 크게 3가지 그룹으로 나눌 수 있습니다.
          1. Kinded
            • 특정 종류의 join point 선택에 사용
            • execution, get, set, call, handler
          2. Scoping
            • join point의 그룹 선택에 사용
            • within, withincode
          3. Contextual
            • 컨텍스트와 관련 내용에 사용
            • this, target, @annotation
      • 사용법 cheat sheet

      • Pointcut 쓰는법

        • 포인트컷 매칭은 AspectJ가 컴파일타임에 최적화 시키기 때문에 일반적으로 크게 신경 쓸 필요는 없지만, 성능을 더욱 향상 시키기 탐색범위를 줄여주는 것이 좋습니다.
        • 잘 쓰여진 Pointcut은 최소한 Kinded && Scoping 타입을 포함해야 합니다.
        • Scoping 타입 없이 Kinded 또는 Contextual 타입을 사용하는것은 동작에는 문제가 없지만 weaving 성능에 영향을 줄 수 있습니다.
        • Scoping 타입은 매우 빠르게 처리되고, 탐색범위를 좁히는데 큰 도움이 되기 때문에 가능하면 반드시 포함해야 합니다.

          @Advice

      • Advice는 pointcut 표현식과 함께 표현되며, 매칭된 pointcut의 주변(@before, @after, @around)에서 수행됩니다.

          @Aspect
          public class Example {
              //@Before = advice
              //execution(* com.xyz.myapp.dao..(..)) = pointcut
              @Before("execution(* com.xyz.myapp.dao..(..))")
              public void doAccessCheck() {
                  // ...
              }
        
              @AfterReturning(
                  pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",
                  returning="retVal")
              public void doAccessCheck(Object retVal) {
                  // retVal 자체를 다른 객체로 변경 할 수는 없습니다. (return type도 void)
              }
        
              @Around("com.xyz.myapp.SystemArchitecture.businessService()")
              public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
                  // start stopwatch
                  Object retVal = pjp.proceed();
                  // stop stopwatch
                  return retVal;
              }
          }
      • Parameter

        • Advice 메서드는 첫번째 파라미터로 JoinPoint를 받을 수 있습니다.

        • JoinPoint말고도 args@annotation를 통해서 원하는 파라미터를 넣어주는것도 가능합니다.

          • @Pointcut("com.xyz.myapp.SystemArchitecture.dataAccessOperation() && args(account,..)")
            private void accountDataAccessOperation(Account account) {}
            
            @Before("accountDataAccessOperation(account)")
            public void validateAccount(Account account) {
              // ...
            }
            
            //한줄로도 선언 가능
            @Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation() && args(account,..)")
            public void validateAccount2(Account account) {
                // ...
            }
            • args(account,..)를 선언함으로써 적어도 하나의 파라미터가 있는 dataAccessOperation에 대한 pointcut을 명시할 수 있고, account 객체를 advice에 제공해줄 수 있습니다.
          • @Retention(RetentionPolicy.RUNTIME)
            @Target(ElementType.METHOD)
            public @interface Auditable {
                AuditCode value();
            }
            
            @Before("com.xyz.lib.Pointcuts.anyPublicMethod() && @annotation(auditable)")
            public void audit(Auditable auditable) {
                AuditCode code = auditable.value();
                // ...
            }

    Introductions

    • Introduction을 사용하면 adviced된 객체를 특정 인터페이스의 구현체인것 처럼 동작하게 만들 수 있습니다.

      @Aspect
      public class UsageTracking {
      
        @DeclareParents(value="com.xzy.myapp.service.*+", defaultImpl=DefaultUsageTracked.class)
        public static UsageTracked mixin;
      
        @Before("com.xyz.myapp.SystemArchitecture.businessService() && this(usageTracked)")
        public void recordUsage(UsageTracked usageTracked) {
            usageTracked.incrementUseCount();
        }
      }
      • @DeclareParents를 통해 introduction을 생성 할 수 있으며 value 값인 com.xzy.myapp.service.*+ 패키지에 속한 bean들이 UsageTracked 인터페이스의 구현체 됩니다.

        포인트컷 포현식을 잘 못 이해하고 있는건지 모르겠지만
        "com.xyz.myapp.SystemArchitecture.businessService()" 가
        "com.xzy.myapp.service" 패키지가 아니니깐 동작 안해야 하는거 아닌가.. 모르겠네요

    Aspect Instantiation Models

    • Aspect 인스턴스는 default로 application context 단위 싱글톤으로 생성됩니다.

    • Aspect가 싱글톤이 아닌 다른 lifecycle을 갖게 하려면 perthis , pertarget를 사용하여 지정해 줄 수 있습니다.

        @Aspect("perthis(com.xyz.myapp.SystemArchitecture.businessService())")
        public class MyAspect {
      
          private int someState;
      
          @Before(com.xyz.myapp.SystemArchitecture.businessService())
          public void recordServiceUsage() {
              // ...
          }
      
        }
      • 명시한 joinpoint당 하나의 aspect 인스턴스가 생성됩니다.

    Spring AOP가 아닌 AspectJ 사용하기

    'Docs > Spring' 카테고리의 다른 글

    Spring Doc - Core - Web MVC  (0) 2021.05.03
    Spring Doc - Core - Transaction  (0) 2021.05.03
    Spring Doc - Core -IOC Container  (0) 2021.05.03

    댓글

Designed by Tistory.