스프링 AOP 에 대해서

박종현·2023년 1월 1일
1
post-thumbnail

Udemy 강의내용 정리 입니다.
Spring Professional Certification Exam Tutorial 02 - AOP(Aspect Oriented Programming)

AOP (Asepect Oriented Programming)

Asepect Oriented Programming의 약자로 관점 지향 프로그래밍이라고 불리며 프로그램 로직으로부터 부가적인 관점에서의 문제해결(Cross Cutting Issue)을 위한 그룹을 분리하여 객체지향 프로그래밍을 보완하는 프로그래밍 페러다임

관점 지향 프로그래밍은 흩어진 Aspect를 모듈화 할 수 있는 프로그래밍 기법을 말한다. 관점 지향은 어떤 로직을 기준으로 핵심적인 관점, 부가적인 관점으로 나누어 본다는 말이고 따라서 관점을 기준으로 각각 모듈화하는 프로그래밍 기법인 것

  • 기존의 비즈니스 로직에 해당하는 코드를 변경하지 않고 별도의 레이어에 공통 관심에 해당하는 부가기능을 추가함으로써 달성될 수 있다
  • 여러 계층에서서 공통관심에 대한 코드를 반복할 필요가 없으므로 코드의 중복을 해결한다
  • 비즈니스 로직과 관련없는 코드가 비즈니스 로직 코드와 섞이는 것을 막을 수 있다
  • 공통 관심의 예시
    • 로깅(Logging)
    • 성능 모니터링(Performance Mornitoring)
    • 트랜젝션(Transaction)
    • 캐싱(Caching)

용어 정리

  • Cross Cutting Issue 핵심 비지니스 기능과 구분되는 공통으로 달성되어야 하는 공통 관심사항

  • 조인포인트(Joinpoint) 클라이언트가 호출하는 모든 비즈니스 메소드,

    조인포인트 중에서 포인트컷되기 때문에 포인트컷의 후보로 생각할 수 있다.(Advice 를 적용할 수 있는 지점)

  • 포인트컷(Pointcut) ⇒ 특정 조건에 의해 필터링 된 조인포인트, 코드에 의한 행위(Advice)가 발생해야하는 위치

    @Pointcut("@annotation(com.spring.professional.exam.tutorial.module02.question02.annotations.InTransaction)")
    public void transactionAnnotationPointcut() {}
  • 어드바이스(Advice) ⇒ Cross Cutting Issue를 구현하기 위한 부가 기능에 해당하는 코드

    @Before("transactionAnnotationPointcut()")
    public void beforeTransactionAnnotationAdvice() {
        System.out.println("Before - transactionAnnotationPointcut");
    }
    
    /* Inline Pointcut & Advice*/
    @Before("@annotation(com.spring.professional.exam.tutorial.module02.question02.annotations.InTransaction)")
    public void beforeTransactionAnnotationAdvice() {
        System.out.println("Before - transactionAnnotationPointcut");
    }
  • 애스팩트(Aspect) ⇒ 포인트컷과 어드바이스의 결합하여 모듈화 한 것. 어떤 포인트컷 메소드에 대해 어떤 어드바이스 메소드를 실행할지 결정한다.

    @Component
    @Aspect
    public class CurrencyServiceAspect {
        @Pointcut("@annotation(com.spring.professional.exam.tutorial.module02.question02.annotations.InTransaction)")
    		public void transactionAnnotationPointcut() {
    		}
    		@Before("transactionAnnotationPointcut()")
    		public void beforeTransactionAnnotationAdvice() {
    		    System.out.println("Before - transactionAnnotationPointcut");
    		}
    }

  • 위빙(Weaving) : 포인트컷으로 지정한 메소드가 호출될 때, 어드바이스에 해당하는 횡단 관심 메소드가 삽입되는 과정을 의미한다.(Aspect 적용) 이를 통해 에플리케이션 코드(비즈니스로직)과 Aspect(Cross Cutting Issume Implement Code)가 결합된다.

    Type of Weaving

    • Compile Time Weaving ⇒ 컴파일 시점에 Aspect가 바이트코드로 변환되어 조인포인트에 직접 결합된다(직접적인 코드 수정)
    • Load Time Weaving ⇒ 클래스가 로딩되는 시점에 포인트컷에 해당하는 조인포인트에 어드바이스가 적용되어 클래스의 바이트코드가 수정된다
    • Rumtime Time Weaving ⇒ 실제 런타임상에서 조인포인트 메서드가 호출시 위빙이 이루어진다. (Source file, class 파일에도 변화가 없다.) 타겟 클래스의 프록시를 생성하여 비즈니스 코드와 Advice가 포함된 메소드를 호출하는 방식이다. Spring AOP 에서 이 방식을 사용하고 있다

AOP Implements

Spring AOP (Default)

  • 프록시 기반의 AOP 구현체이다. 프록시 객체를 사용하는 이유는 접근 제어 및 부가 기능을 추가하기 위해서이다.
  • 스프링 빈에만 AOP를 적용할 수 있다.
  • 모든 AOP 기능을 제공하는 것이 목적이 아니라, 스프링 IoC와 연동하여 엔터프라이즈 애플리케이션에서 가장 흔한 문제(중복코드, 프록시 클래스 작성의 번거로움, 객체 간 관계 복잡도 증가 등등…)를 해결하기 위한 솔루션을 제공하는 것이 목적.

Spring Aop 프록시 타입

JDK Dynamic Proxy

  • Proxy Factory에 의해 런타임 시 동적으로 생성되는 오브젝트
  • JDK Dynamic Proxy는 반드시 인터페이스가 정의되어 있고 인터페이스에 대한 명세를 기준으로 프록시를 생성한다
  • 인터페이스 선언에 대한 강제성이 있으며 인터페이스에 명세된 메서드 이외의 구현에 대한 포인트컷 지정이 불가하다
  • 프록시 객체에 InvocationHandler를 포함하여 하나의 객체로 변환한다.
  • 객체에 대한 Reflection 기능을 사용하기때문에 퍼포먼스 하락이 발생할 수 있다.
  • JDK DynamicProxy 직접 생성 예제
    public class Runner {
        public static void main(String[] args) {
            SomeTarget someTarget = (SomeTarget) Proxy.newProxyInstance(
                    SomeTarget.class.getClassLoader(), SomeTargetImpl.class.getInterfaces(),
                    new SomeInvocationHandler(
                            new SomeTargetImpl()
                    )
            );
    
            SomeResult someResult = someTarget.findById(5);
            someTarget.save(someResult);
        }
    }
    
    public class SomeInvocationHandler implements InvocationHandler {
    
        private final SomeTarget target;
    
        public SomeInvocationHandler(SomeTarget target) {
            this.target = target;
        }
    
        @Override
        public Object invoke(Object obj, Method method, Object[] args) throws Throwable {
            System.out.println("before " + method.getName());
            Object result = method.invoke(target, args);
            System.out.println("after " + method.getName());
            return result;
        }
    }

CGLib Proxy

  • GLIB Proxy는 순수 Java JDK 라이브러리를 이용하는 것이 아닌 CGLIB라는 외부 라이브러리를 사용한다
  • CGLib클래스의 Enhancer를 기반으로 프록시를 생성
    • 프록시 생성에 필요한 모듈은 Enhancer, Callback, CallbackFilter로 구성된다
    • Enhancer ⇒ Target 클래스의 non-final 메소드를 오버라이딩 하는 서브 클래스 생성
    • Callback ⇒ 프록시로 호출이 들어오면 Target으로 가기전에 모든 호출을 가로채
      callback 자체의 부가 로직 수행 및 origin 메소드 호출
      하는 역할 담당
    • CallbackFilter ⇒origin의 메소드별로 호출될 때 가로채고자 하는 Callback을 지정
      해주는 역할
  • 인터페이스가 아닌 구체클래스를 기반으로 프록시를 생성한다
  • 상속을 이용하므로 final이나 private 와 같은 modifier가 적용된 메서드에 대해서는 오버라이딩이 불가하므로 Aspect를 적용할 수 없다.
  • CGLIB Proxy는 클래스를 상속받아 바이트 코드를 조작해서 프록시 객체를 생성하므로 JDK Dynamic Proxy보다 퍼포먼스가 빠른 장점이 있다

    CGLib Proxy의 단점, 버전에 따른 보완

    • 의존성의 추가 (Spring 3.2 이후 버전의 경우 Spring Core 패키지에 포함되어 있음)
    • default 생성자가 필요하다. (현재는 objenesis 라이브러리를 통해 해결)
    • 타겟의 생성자가 두 번 호출된다. (현재는 objenesis 라이브러리를 통해 해결)
  • CGLib 프록시 직접 생성 예제
    public class Runner {
        public static void main(String[] args) {
            Enhancer enhancer = new Enhancer();
            enhancer.setCallback(new SomeInterceptor());
            enhancer.setSuperclass(SomeTarget.class);
            SomeTarget someTarget = (SomeTarget) enhancer.create();
    
            SomeResult someResult = someTarget.findById(5);
            someTarget.save(someResult);
        }
    }
    
    public class SomeInterceptor implements MethodInterceptor {
    
        @Override
        public Object intercept(Object object, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
            System.out.println("before " + method.getName());
            Object result = methodProxy.invokeSuper(object, args);
            System.out.println("after " + method.getName());
            return result;
        }
    }

두 방식의 차이는 인터페이스의 유무로서 AOP의 타겟이 되는 클래스가 인터페이스를 구현하였으면 JDK Dynamic Proxy, 그렇지 않으면 CGLib 방식을 사용한다.

스프링 AOP를 통한 자동 프록시 생성 예시

public class Runner {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ApplicationConfiguration.class);
        context.registerShutdownHook();

        SomeTarget someTarget = context.getBean(SomeTarget.class);
        ConcreteSomeTarget concreteSomeTarget = context.getBean(ConcreteSomeTarget.class);

        SomeResult someResult = someTarget.findResultById(5);
        someTarget.saveResult(someResult);
        someTarget.deleteResult(someResult);

        concreteSomeTarget.findResultById(5);
        concreteSomeTarget.saveResult(someResult);
        concreteSomeTarget.deleteResult(someResult);
    }
}

@Component
@Aspect
public class SomeTargetAspect {
    @Before("execution(* com.spring.professional.exam.tutorial.module02.question03.service.a.SomeTarget.findResultById(..))")
    public void beforeFindEmployeeById() {
        System.out.println("before - SomeTarget");
    }

    @After("within(com.spring.professional.exam.tutorial.module02.question03.service.a.*)")
    public void afterExecutionWithinPackage() {
        System.out.println("before - SomeTarget");
    }
}

@Component
@Aspect
public class ConcreteSomeTargetAspect {
    @Before("execution(* com.spring.professional.exam.tutorial.module02.question03.service.b.ConcreteSomeTarget.findResultById(..))")
    public void beforeFindEmployeeById() {
        System.out.println("before - ConcreteSomeTarget");
    }

    @After("within(com.spring.professional.exam.tutorial.module02.question03.service.b.*)")
    public void afterExecutionWithinPackage() {
        System.out.println("after - ConcreteSomeTarget");
    }
}
  • Jdk Dynamic Proxy
  • CGLib Proxy

AspectJ

자바에서 완벽한 AOP 솔루션 제공을 목표로 하는 기술

  • [ .aj 파일]을 이용한 assertj 컴파일러를 추가로 사용하여 컴파일 시점이나 JVM 클래스 로드시점에 조작한다.
  • 런타임 시점에는 영향끼치지 않는다 ⇒ 즉 컴파일이 완료된 이후에는 앱 성능에 영향이 없다.

AspectJ 와 SpringAOP 비교

  1. 기능과 목표의 차이
    • Spring AOP는 프로그래머가 직면하는 일반적인 문제 해결을 위해 Spring IoC에서 제공하는 간편한 AOP 기능이다. 어디에나 쓸 수 있는 완벽한 AOP 솔루션이 아니라, Spring 컨테이너가 관리하는 Bean에만 AOP를 적용 할 수 있다.
    • AspectJ는 자바코드에서 동작하는 모든 객체에 대해 완벽한 AOP 솔루션 제공을 목표로 하는 기술이다. 성능이 뛰어나고 기능이 매우 강력하지만 그만큼 Spring AOP에 비해 사용방법이나 내부 구조가 훨씬 더 복잡하다.
  2. 위빙(Weaving) 방식의 차이
    • Spring Aop ⇒ 런타임 타임 위빙
    • AspectJ ⇒ 컴파일 타임 위빙, 로드 타임 위빙

Apply Aspect

@Aspect 어노테이션 인식

  1. aspectjweaver 혹은 spring-aop 종속성을 추가(스프링 aop ⇒ org.springframework:spring-aspects 사용하는 것이 일반적)
  2. Config 클래스(@Configuration 어노테이션 적용된 클래스)에 @EnalbeAspectJAutoProxy 어노테이션을 적용 (어노테이션 선언하지 않을시 @Aspect 를 스캔할 수 없다)
  3. 컴포넌트 혹은 빈에 @Aspect 어노테이션 적용

어드바이스(Advice) 타입 및 적용 예시

  • @Before : 메서드 실행 전에 실행하는 Advice
    • Authorization, Security
    • Logging
    • Data Validation
  • @AfterReturning : 메서드 정상 실행 후 실행하는 Advice
    • Logging
    • Resource Cleanup
  • @AfterThrowing : 메서드 실행시 예외 발생시 실행하는 Advice
    • Logging
    • Error Handling
  • @After : 메서드 정상 실행 또는 예외 발생 상관없이 실행하는 Advice
    • Logging
    • Data Validation for Method Result
  • @Around :  위 네가지 Advice를 모두 포함 , 모든 시점에서 실행할 수 있는 Advice
    • Transactorions
    • Distributed Call Tracing
    • Authorization, Security

@EnableAspectJAutoProxy 어노테이션은 @Aspect 어노테이션이 선언된 클래스들을 인식할 수 있도록 하고 빈들에 대한 프록시 객체를 생성한다.

프록시 객체를 생성하는 내부 프로세스는 AnnotationAwareAspectJAutoProxyCreator에 의해 이루어진다. 각각의 빈들에 대한 프록시 객체를 생성함으로써 스프링은 클라이언트의 요청을 intercept 하고 @Before / @After/ @AfterReturning / @AfterThrowing / @Around 어드바이스를 실행한다.

@Aspect 어노테이션 만으로는 클래스는 해당 클래스에 대한 빈을 생성하지 않는다. 컴포넌트 스캔이 되도록 @Component 어노테이션을 붙히거나 빈으로 등록해야한다.

JoinPoint & ProceedingJoinPoint Interface

  • JoinPoint Interface
    • JoinPoint.getArgs() : JoinPoint에 전달된 인자를 배열로 반환한다.
    • JoinPoint.getThis() : 프록시 객체를 반환한다.
    • JoinPoint.getTarget() : 프록시가 가리키는 타겟 객체를 반환한다.
    • JoinPoint.getSignature() : 조인되는 메서드에 대한 정보(리턴타입, 이름, 매개변수)를 반환한다.
      • 객체가 선언하는 모든 연산은 연산의 이름, 매개변수로 받아들이는 객체들을 시그니처라고 한다.
      • String getName() : 클라이언트가 호출한 메서드의 이름을 반환한다.
      • String toLongString() : 클라리언트가 호출한 메서드의 리턴타입, 이름, 매개변수를 패키지 경로까지 포함해서 반환한다.
      • String toShortString() : 클라이언트가 호출한 메서드 시그니처를 축약한 문자열로 반환한다.
  • ProceedingJoinPoint Interface
    • JoinPoint 인터페이스를 상속하며 Joinpoint 메서드 이외에 타겟을 호출하는 메서드가 추가되었다.
    • around 어드바이스에서 타겟 호출 전 후의 동작을 정의하기 위해 사용된다
    • proceed() : 다음 어드바이스나 타겟을 호출

포인트컷 표현식(Pointcut designator)

@Pointcut(...) 어노테이션의 인자에 표현식을 전달하여 포인트컷을 생성하거나 어드바이스(@Before / @After/ @AfterReturning / @AfterThrowing / @Around)에 인라인으로 표현식을 삽입할 수 있다

  • execution ⇒ 구체적인 포인트컷 지정

    @Pointcut(
    		execution([visibility_modifiers] [return_type] [package].[class].[method] [throws exceptions]
    )
  • within ⇒ 특정 타입 내의 조인 포인트를 매칭한다.

    @Pointcut(
    		within([package].[class])
    )
  • args ⇒ 인자가 주어진 타입의 인스턴스인 조인 포인트

    @Pointcut(
    		args([parameter_type1, parameter_type2, ..., parameter_typeN])
    )
  • bean ⇒ 스프링 전용 포인트컷 지시자로 빈의 이름으로 포인트컷을 지정한다.특정 빈을 포인트컷으로 지정

    @Pointcut(
    		bean([beanName])
    )
  • this ⇒ 스프링 빈 객체(스프링 AOP 프록시)를 대상으로 하는 조인 포인트

    @Pointcut(
    		this([type])
    )
  • target ⇒ Target 객체(스프링 AOP 프록시가 가리키는 실제 대상)를 대상으로 하는 조인 포인트

    @Pointcut(
    		target([type])
    )
  • @annotation ⇒ 메서드가 주어진 어노테이션을 가지고 있는 조인 포인트를 매칭

    @Pointcut(
    		@annotation([annotation_type])
    )
  • @args ⇒ 전달된 실제 인수의 런타임 타입이 주어진 타입의 어노테이션을 갖는 조인 포인트

    @Pointcut(
    		@args([annotation_type])
    )
  • @within ⇒ 주어진 어노테이션이 있는 타입 내 조인 포인트

    @Pointcut(
    		@within([annotation_type])
    )
  • @target ⇒ 실행 객체의 클래스에 주어진 타입의 어노테이션이 있는 조인 포인트

    @Pointcut(
    		@target([annotation_type])
    )
  • Pointcut 표현식에 사용되는 Wildcard

    Wildcard설명
    *기본적으로 임의의 문자열을 의미한다. 패키지를 표현할 때는 임의의 패키지 1개 계층을 의미한다. 메서드의 매개변수를 표현할 때는 임의의 인수 1개를 의미한다.
    ..패키지를 표현할 때는 임의의 패키지 0개 이상 계층을 의미한다. 메서드의 매개변수를 표현할 때는 임의의 인수 0개 이상을 의미한다.
    +클래스명 뒤에 붙여 쓰며, 해당 클래스와 해당 클래스의 서브클래스, 혹은 구현 클래스 모두를 의미한다.
  • 포인트컷 표현식은 다른 포인트컷의 이름을 참조할 수 있으며 논리 연산자(&&, ||, !, &, ^, ~)를 통해 결합하거나 배제할 수 있다. 이를 통해 더 구체적으로 조인포인트를 지정할 수 있다.

Aspect 적용 예시

public class Runner {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ApplicationConfiguration.class);
        context.registerShutdownHook();

        SomeTarget target = context.getBean(SomeTarget.class);

        System.out.println("\nCall Method1 -->");
        target.method1();

        System.out.println("\nCall Method2 -->");
        target.method2("value1", "value2");

        try {
            System.out.println("\nCall Method3 -->");
            target.method3();
        } catch (Exception ignored) {
            /* ignored on purpose */
        }
    }
}

@Component
public class SomeTarget {
    public void method1() {
        System.out.println("SomeTarget : method1 Called");
    }
    public String method2(String value1, String value2) {
        return "Something Value";
    }

    public void method3(){
        throw new RuntimeException();
    }
}

@Component
@Aspect
public class SomeTargetAspect {
    @Pointcut("within(com.spring.professional.exam.tutorial.module02.question05.repository.*)")
    public void allSomeTargetJoinPoint() {
    }

    @Before("allSomeTargetJoinPoint()")
    public void before(JoinPoint joinPoint) {
        System.out.println("before - " + joinPoint.getSignature().toShortString());
        System.out.println("inputArgs - "+ Arrays.toString(joinPoint.getArgs()));
    }

    @After("allSomeTargetJoinPoint()")
    public void after(JoinPoint joinPoint) {
        System.out.println("after - " + joinPoint.getSignature().toShortString());
    }

    @AfterThrowing(value = "allSomeTargetJoinPoint()", throwing = "exception")
    public void afterThrowing(JoinPoint joinPoint, Exception exception) {
        System.out.println("after throwing exception - " + joinPoint.getSignature().toShortString() + " - exception = " + exception);
    }

    @AfterReturning(value = "allSomeTargetJoinPoint()", returning = "returnValue")
    public void afterReturning(JoinPoint joinPoint, Object returnValue) {
        System.out.println("after returning " + joinPoint.getSignature().toShortString() + " - returnValue = " + returnValue);
    }

    @Around("allSomeTargetJoinPoint()")
    public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        System.out.println("around - before - " + proceedingJoinPoint.getSignature().toShortString());
        try {
            return proceedingJoinPoint.proceed();
        } finally {
            System.out.println("around - after - " + proceedingJoinPoint.getSignature().toShortString());
        }
    }
}
Call Method1 -->
around - before - SomeTarget.method1()
before - SomeTarget.method1()
inputArgs - []
SomeTarget : method1 Called
around - after - SomeTarget.method1()
after - SomeTarget.method1()
after returning SomeTarget.method1() - returnValue = null

Call Method2 -->
around - before - SomeTarget.method2(..)
before - SomeTarget.method2(..)
inputArgs - [value1, value2]
around - after - SomeTarget.method2(..)
after - SomeTarget.method2(..)
after returning SomeTarget.method2(..) - returnValue = Something Value

Call Method3 -->
around - before - SomeTarget.method3()
before - SomeTarget.method3()
inputArgs - []
around - after - SomeTarget.method3()
after - SomeTarget.method3()
after throwing exception - SomeTarget.method3() - exception = java.lang.RuntimeException

0개의 댓글