[Spring] Spring AOP - Spring에서는 AOP를 어떻게 이용할까?

반응형

앞서 우리는 AOP(Aspect Oriented Programming, 관점 지향 프로그래밍)에 대한 기본과 개념에 대해 알아봤습니다. 만약 읽어보지 못했다면 이 글을 읽기 전 반드시 읽어보시길 권장드립니다.

 

2022.05.14 - [Programming/Spring] - [Spring] AOP (Aspect-Oriented Programming) 기본과 개념

 

[Spring] AOP (Aspect-Oriented Programming) 기본과 개념

Spring에는 AOP(Aspect-Oriented Programming, 관점 지향 프로그래밍)이라는 핵심 기능을 제공합니다. 음? OOP(객체 지향 프로그래밍)라는 것은 들어봤는데, AOP는 무엇일까요? AOP vs OOP ? 관점 지향 프로그래밍

blog.neonkid.xyz

 

 

 

Spring AOP

앞서 다룬 일반적인 AOP와 마찬가지로 Spring AOP 또한 프록시를 생성하여 Advice를 주입하는 방식으로 진행됩니다. 다만 일반적인 AOP와 달리 ProxyFactory를 이용해 프록시로 위빙하는 방식을 사용하지 않고, Spring이 제공하는 선언적인 AOP 구성 매커니즘인 ProxyFactoryBean 클래스와 aop 네임스페이스, @AspectJ 어노테이션와 같은 선언적 AOP 매커니즘을 사용해 프록시를 선언적으로 생성하여 AOP를 구현합니다.

 

좀 더 구체적으로 프록시 생성이 어떻게 되는지 알아보겠습니다.

 

 

Spring은 런타임 시점에 ApplicationContext(스프링에서 Bean을 관리하는 객체)의 Bean에 정의된 흩어진 관심사를 분석하고 ProxyBean을 동적으로 생성합니다.

 

그리고 호출자(Caller)에 Target Bean을 주입하여 이를 직접 호출하게 하는 대신 ProxyBean을 주입하여 ProxyBean이 실행 조건(즉, JoinPoint, PointCut, Advice 등)을 분석하고 이에 따라 적절한 Advice를 위빙하는 것입니다.

 

그렇다면 Spring AOP가 일반적인 AOP 프로그래밍보다 더 단순화 되었다고 했는데, 어떻게 단순화 된 것일까요?

 

 

 

Spring AOP Components

우리는 이전 글에서 AOP의 개념과 용어를 다뤘습니다. Spring AOP에서는 이들 개념을 좀 더 심플하게 만들었는데요. 각 종류에 대해서 알아보겠습니다.

 

  • Spring JoinPoint

    Spring AOP의 JoinPoint는 오직 메서드 호출 조인포인트만을 제공한다. 
    (하지만 필요에 따라 AspectJ 같은 다른 AOP 구현체에서 제공하는 다른 조인 포인트도 사용이 가능)

  • Spring Aspect

    Spring AOP의 Aspect는 Advisor 인터페이스를 구현한 클래스의 인스턴스이다.
    Advisor의 하위 인터페이스로 PointcutAdvisor와 IntroductionAdvisor 두 가지가 존재한다.

  • Spring Advice

    특정 조인포인트에서 실행될 코드인 Advice를 Spring AOP에서는 아래와 같이 기본 인터페이스를 제공한다.
    (Before, After-Returning, After(finally), Around, Throws, Introduction)

    이 중에서도 Around는 이전 글에서 다뤘던 AOP 얼라이언스 표준인 MethodInterceptor를 적용하였다.

 

이전 글과 콜라보레이션 유지를 위해 위의 컴포넌트 중 Spring Advice를 사용하여 좀 더 Spring AOP에 맞게 코드를 작성해보면 아래와 같습니다.

 

Before 시점에 실행할 코드를 정의하기 위해 MethodBeforeAdvice 인터페이스를 사용하였고, After 시점에 실행할 코드를 정의하기 위해 AfterReturningAdvice를 쓴 것을 볼 수 있습니다. MethodInterceptor를 사용해서도 간단히 나타낼 수 있는 것을 별도의 클래스를 이용해 정의하여 서로 다른 관점으로 코드를 정의할 수 있는 모습입니다.

 

 

 

ProxyFactory

이전 글에서도 이번 글에서도 우리는 ProxyFactory를 이용하여 Advice를 추가하고 Target 클래스를 추가하여 대상 프록시 객체를 만들어내는 것을 알았습니다.

 

이처럼 ProxyFactory 클래스는 Spring AOP에서 위빙과 프록시 생성 과정을 제어하는 역할을 합니다. 여기에는 우리가 이전에 다뤘던 것처럼 단일 Target 클래스, 하나의 Advice를 적용할 수도 있지만 하나의 Target 클래스에 해당하는 모든 메서드에도 적용해 볼 수 있습니다.

 

그 외에도 동일한 ProxyFactory를 이용해 각기 다른 Aspect를 적용한 프록시를 여러 개 만들어 볼 수 있습니다. 같은 Target 클래스를 넣었지만 removeAdvice 메서드를 통해 기존에 적용한 SimpleBeforeAdvice를 제거한 후 SimpleAfterAdvice만을 넣고, 프록시 객체를 만들면 아래와 같은 출력의 모습을 볼 수 있습니다.

 

1번째 출력은 순수 객체를 사용한 것이고, 2번째 출력은 SimpleBeforeAdvice만을 적용시켰을 때 프록시 객체, 3번째 출력은 기존의 SimpleBeforeAdvice를 제거하고, SimpleAfterAdvice만을 적용시켰을 때 프록시 객체의 모습입니다.

 

 

 

Spring AOP Proxy

앞서 Proxy는 Target 클래스를 감싸 요청을 대신 받아주는 Wrapping 클래스라는 것을 알았습니다. 그리고 이 Proxy는 Weaving을 통해 객체를 생성하는데, Spring AOP는 이러한 생성 과정을 아래의 두 가지로 표현합니다.

 

  • CGLib Proxy
  • JDK Dynamic Proxy

 

예를 들면 OrderService라는 주문을 관리하는 비즈니스 로직 클래스가 있습니다. 해당 클래스에는 새로운 주문 데이터를 생성하는 save 메서드가 있는데, 실제로 Spring이 이 클래스에 있는 save 메서드를 호출할 때 OrderService 객체에 바로 접근하는 것이 아니라 weaving으로 생성된 프록시를 통해 간접적으로 접근하는 것입니다.

 

Spring AOP는 AspectJ와 달리 Load-Time Weaving을 사용하지 않고 Run-Time Weaving 기법을 사용합니다. 앞서 설명한 두 가지 프록시 생성 매커니즘 중 CGLIB Proxy는 Run-Time Weaving을 사용하는 프록시이고, Spring AOP가 사용하는 프록시 생성은 기본적으로 CGLIB Proxy를 사용합니다.

 

JDK Proxy와 CGLib Proxy의 결정적인 차이점은 바로 인터페이스를 Proxy 객체로 하냐, 클래스를 Proxy 객체로 하냐에 차이인데요. 과거에는 Interface로 먼저 구현 스펙을 갖춘 다음 class로 구현하는 OOP를 사용했다면 지금은 바로 class로 정의하는 경우가 많은데, 이에 따라서 Spring에서도 JDK Proxy를 사용할지 CGLib Proxy를 사용할지를 결정합니다.

 

 

 

CGLib Proxy 

CGLIb가 Proxy를 컴파일 타임이 아닌 런타임 시점에 프록시 객체를 생성하는 것까지 알았지만 실제로 어떻게 구동되는지 알면 더 좋겠죠?

 

이전 글에서 우리는 MethodInterceptor를 통해 Advice를 만들고, ProxyFactory를 통해 프록시 객체를 생성했었는데요. 이 과정을 하나로 묶은 모듈이 바로 Enhancer입니다.

 

새로운 프록시를 하나 만들고 있는데요. 이 때 우리가 주어준 Target 클래스를 superClass로 주는 것을 보아 해당 프록시 객체를 만들 때 이를 상속하는 것을 알 수 있습니다. 

 

그리고, 만들어진 프록시 객체로 요청이 들어왔을 때 그 요청을 가로채서 먼저 수행할 로직을 우리가 정의한 Advice 로직으로 먼저 간 후 실제 객체에 요청을 전달한 다음, 돌아올 때도 같은 방법으로 전달하는 것입니다.

 

마지막으로 create 메서드를 이용해 만들어진 프록시 객체를 반환합니다.

 

 

create 메서드를 보면 Target 클래스 타입을 정의하고 하지 않고의 메서드가 오버로딩 된 거 외에는 별다른 것이 없습니다.

 

그런 다음 실제 프록시 객체가 생성되는 메서드는 non-static create 메서드이며 이 메서드도 보면...

 

이 메서드에 320번째 줄을 보면 Target 클래스들의 타입을 가져와 인스턴스를 반환하는 것을 볼 수 있는데, 

 

실제로 코드를 보면 classOnly 플래그가 주어진 경우는 단순히 클래스 타임만을 반환하고, 그렇지 않은 경우는 Reflection을 이용해 클래스를 분석하고 객체를 만들어 반환하는 모습을 볼 수 있습니다.

 

클래스 생성자에 파라미터 값이 있으면 newInstasnce 메서드를 부를 때 파라미터 타입과 파라미터 값을 주고, 그렇지 않으면 빈값과 빈 배열로 객체를 생성해 반환합니다.

 

하지만 우리는 위 코드에서 create 메서드를 호출했을 때 이미 argumentType이 null이 되고 있음을 확인했습니다.

 

따라서 빈 생성자로 된 (argument가 모두 null인) 프록시 객체를 만들어주는 것으로 런타임 시점에 만들어지는 것입니다.

 

Kotlin의 경우 기본적으로 final 클래스이고, 심지어 Java와 달리 빈 생성자를 기본으로 만들지 않는데, 그러면 Kotlin + Spring 조합은 어떻게 동작하는 것인가요?

 

두 가지 방법이 있습니다. 코틀린의 plugin을 통해서 컴파일 시점에 모든 클래스에 기본 생성자를 만들어주도록 하는 것을 고려해 볼 수 있습니다.

 

본래 Spring은 Default Constructor(기본 생성자)가 필요합니다. 왜냐하면 모든 객체들이 Proxy 객체로 접근하는데, Spring AOP가 사용하는 CGLib Proxy는 스스로 Default Constructor를 생성하지 못합니다. 그래서 Spring 4.x 버전부터는 Objenesis 라이브러리를 이용해 기본 생성자를 CGLib에서 생성할 수 있도록 도와줍니다. 

 

 

Objenesis : About

About Objenesis is a small Java library that serves one purpose: To instantiate a new object of a particular class. When would you want this? Java already supports this dynamic instantiation of classes using Class.newInstance(). However, this only works if

objenesis.org

이런 문제가 있음에도 불구하고, Spring AOP가 CGLib Proxy를 계속 고집하는 이유는 JDK Dynamic Proxy에 비해 성능이 좋기 때문입니다. JDK Dynamic Proxy는 기본적으로 Java Reflection을 이용하며 이는 꽤 많은 시간 비용을 소모합니다. 

(그리고 현재는 CGLib Proxy가 가지고 있던 문제들 대부분을 개선 하였음)

 

마지막으로 Kotlin은 기본적으로 final class(상속이 안되는 클래스)로 코드를 정의하는데, 원래라면 개발자가 직접 클래스를 구현할 때마다 open 키워드를 붙여줘야 합니다.

 

하지만 이 또한 plugin을 사용해 볼 수 있는데, allopen 이라는 플러그인을 사용하면 컴파일 시점에 모든 클래스들을 open class로 바꿔 컴파일하는 플러그인입니다. 차후 이 플러그인은 plugin.spring이라는 것에 흡수되고, 코틀린 + 스프링 조합을 사용할 때 필요한 모든 plugin으로 관리되어 사용해 위 문제를 해결한 것입니다.

 

 

 

마치며..

기본적으로 표준 AOP와 Spring AOP의 모습은 크게 다르지 않습니다. 하지만 표준 AOP 인터페이스로 구현할 수 있는 프로그래밍 허들을 낮추고 최대한 간소화된 모습을 보여줬습니다.

 

이 글을 통해서 초기 Kotlin이 왜 Spring과 융화하기 어려웠는지를 알 수 있습니다. Spring은 일반 Java와 달리 객체 접근 방법이나 생성 방법 등이 다르고, 심지어 Kotlin의 클래스 구현과 Java의 클래스 구현 역시 달랐기 때문에 초창기 코틀린 언어로 Spring을 사용하는 것은 굉장히 어려운 일이던 것이죠.

 

다음 글에서는 Spring AOP 컴포넌트 중 Spring Advice를 좀 더 자세히 다뤄보도록 하겠습니다.

 

 
반응형

Tistory Comments 0