이 글은 특정 구현에 종속되는 내용을 제외한 이론 위주의 정리 글입니다.

 

AOP (Aspect-Oriented Programming) 란?


AOP는 프로그래밍 개발 사상 중 하나이며, Spring Framework을 구현한 주요한 개념입니다.

 

저는 이 개념을 애플리케이션 내부의 컴포넌트들에 존재하는 비즈니스 로직이 아닌 보일러 템플릿 코드들을 (저는 개인적으로 인프라 로직이라고 명명하여 부릅니다.) 모아 응집시켜 각각의 컴포넌트로 분리하는 것으로 이해하고 있으며, 개발자들은 이를 활용하여 애플리케이션 서비스에는 비즈니스 로직들만을 남겨 개발 유지보수 경험을 향상시킵니다.

 

 

인프라 로직?


Application 전 영역에서 나타날 수 있는, 중복될 수 있으며 비즈니스가 아닌 로직을 의미합니다.

  • 성능 검사, Flag 처리(활성화, 비활성화)
  • 로거 - 로깅
  • 알림
  • 예외처리
  • 인증 - 인가
  • 트랜잭션 처리
  • 의존성 주입

등 실제 도메인에서 필요한 비즈니스 로직이 아닌 것들을 의미합니다.

 

AOP는 OOP를 대체하는 것이 아니라 보완하는 성격의 프로그래밍 사상입니다. OOP는 클래스를 이용하여 역할에 맞게 로직을 응집하고, 가시하게끔 하는 것이고 AOP는 그런 클래스들을 바라보는 거시적인 시점에서 좀 더 역할에 맞게끔 로직들을 분리해냄으로써 컴포넌트의 결합성을 떨어트리고 재사용 가능케하는 것이기 때문입니다.

 

즉 "AOP와 OOP 중 무엇이 좋냐" 라고 비교하는 것은 잘못된 질문이라고 생각합니다.

 

이러한 AOP는 방문자, 데코레이터, 프록시 패턴 등을 통해 적용할 수 있습니다.

 

 

AOP의 개념?


  • Aspect : 비즈니스 로직을 제외한 부가 기능에 대한 코드들을 응집시켜 컴포넌트로 만든 것입니다.
  • Target : Aspect를 적용할 대상을 의미합니다. (Class, Method)
  • Advice : 어느 시점에 Aspect를 적용할지 결정하는 것을 의미합니다.
  • JoinPoint : Advice가 적용될 수 있는 위치들, 즉 Method 진입 지점, 생성자 호출 시점, 객체 동작 시점이나 필드에서 값을 꺼낼 때 등 적용 가능한 다양한 상황을 의미합니다.
  • PointCut : 실제 Advice가 적용될 지점을 설정합니다.

 

 

 

Spring AOP?


Spring AOP는 Spring에서 기본적으로 사용할 수 있는 Dynamic Proxy 기반의 AOP 구현체입니다.

  • JDK Dynamic proxy, CGLIB API 통해 동작합니다.
  • Spring Container에 등록되는 Bean들에만 적용 가능합니다.

해당 라이브러리의 목적은 모든 AOP 스펙을 제공하기보다는 기능을 간편하게 적용하면서 메서드 래밸의 중복 코드의 제거와 객체 간의 강결합을 해결하기 위함입니다.

 

 

DK Dynamic Proxy와 CGLIB가 사용되는 시점?


JDK Dynamic Proxy

대상 객체가 최소 하나의 인터페이스를 구현하였을 경우 사용합니다.

 

JDK Dynamic Proxy의 문제점

Advise 대상이든 아니든 모든 Method Call 마다 reflection API의 invoke를 진행하게 됩니다.

  • 즉 invoke를 우선 진행하고 Advise 유무를 판단합니다.

 

CGLIB

대상 객체가 인터페이스를 가지지 않았을 경우 사용합니다.

  • 인터페이스를 가져도 사용할 수는 있습니다 aop:config의 proxy-target-class를 true로 설정하면 됩니다.
  • 대상 객체가 정의한 모든 메서드를 프록시 하여야하는 경우 사용합니다. 하지만 final 지시자는 Override 할 수 없으므로 Advice 할 수 없습니다.

CGLIB의 문제점

  • 성능면에서 JDK 에 비해 우수하나 final method, class 은 Advice 할 수 없습니다.
  • 버전 별로 API가 급변함으로 호환성이 좋지 않습니다. 그렇기에 하이버네이트와 같은 프레임워크들은 특정 버전을 내장하여 개발됩니다.

 

AOP Weaving


Compile-Time Weaving : AspectJ
컴파일 시에 소스코드를 받아 바이트코드 변환할 때 Aspect를 적용합니다. ( .java → .class )

  • 기존 Java Compiler를 확장한 AspectJ Compiler 라는 것을 사용하게 됩니다.
  • 컴파일 시에 바이트 코드 조작을 통해 구현부에 코드를 직접 삽입하여 위빙을 수행합니다.
  • 해당 방법의 경우 Lombok, MapStruct 과 같은 Compile 시 간섭하는 라이브러리와 충돌이 일어날 수 있다고 합니다.
  • 위빙 방식 중에서 제일 빠른 퍼포먼스를 보여줍니다.

 

Post-Compile Weaving (Binary Weaving) : AspectJ
이미 컴파일된 클래스 파일에 바이트코드를 삽입하여 Weaving을 적용하는 방식입니다. (.class → .jar)

 

 

Class-Load Time Weaving : AspectJ
Class Loader가 클래스를 로딩할 때 바이트코드를 삽입하여 Weaving 합니다. (객체가 메모리에 올라갈 때)

  • Spring Container 에 객체가 로드되기 전에, 객체 정보를 핸들링함으로 성능이 저하됩니다.
  • JVM에서 제공하는 agent를 통해서 기능을 지원받아 적용합니다.

 

Runtime Weaving : Spring AOP
실제 코드에 변형이 존재하지 않으며, 메서드 호출 시 프록시를 통해 이루어지는 방식입니다.

  • Spring Container에 객체가 로드될 때, ProxyPostProcessor와 ProxyFactoryBean을 통해 객체 정보를 생성하고 Bean으로 반환하여 컨택스트에 저장하게 됩니다. 즉 Spring Bean에게만 적용되는 것입니다.
  • 메서드 수준의 AOP 만을 지원합니다.
  • Point Cut에 대한 Advice수가 늘어날수록 성능이 떨어진다는 단점이 있습니다. (성능 퍼포먼스 상 8~35배 차이)

Spring AOP의 ProxyFactoryBean 은 설정 대상 객체의 Interface 유무에 따라 proxy를 자동 설정합니다. 있으면 JDK, 없으면 CGLIB입니다. (Boot 2.0 이후는 밑에 언급하였습니다.)

 

DefaultAdvisorAutoProxyCreator 후처리기가 추가되어 있는 경우에는 ProxyFactoryBean이 없더라도 프록시 설정을 적용할 수 있습니다. 이 빈은 어드바이저 정보를 통해 Bean을 프록시로 Wrapping 합니다.

 

 

Spring AOP와 AspectJ를 언제 사용하여야 할까?


Spring AOP

  • Spring Bean에서 메서드 실행만을 Advice하는 것이 AOP 요구사항의 전부라면 Spring AOP를 도입할만 합니다.
    • AspactJ 컴파일러나 위버 등 별도의 도입 요구사항이 존재하지 않습니다.

AspectJ

  • Spring Container에서 관리하지 않는 객체(도메인 객체 등)를 Advice 해야한다면, AspectJ를 도입하여야 합니다.
  • Self Invocation 시 @Transaction, @Caching 처리를 적용하기 위해서는 AspactJ 를 고려할만 합니다.
  • Public 이외의 메서드, 필드, 클래스 등에 Advice를 적용하고 싶은 경우 AspactJ를 고려할만 합니다.

 

Spring AOP - JDK Dynamic Proxy는 Target 메서드 호출마다 인터셉팅하는가?


Spring은 Bean을 등록할 때 Reader를 통해 읽어들여진 Bean Definition을 Parser로 해석하고 대해 PostProcessor를 통해 등록 Process가 진행되게 됩니다.

 

객체 정보에 선언적인 AOP와 Transaction 등이 적용되었다면, ProxyFactoryBean을 통해 Proxy 객체를 생성하고, 해당 객체를 ApplicationContext에 반환하게 됩니다. 그리고 business Logic에서 DI가 있어야 하였을 때, 해당 Proxy 객체를 Injection하여 Proxy를 통해 Logic을 실행하게 됩니다.

 

이러한 흐름을 가지기 때문에, 어플리케이션에서 Business Logic을 처리할 때 AOP가 적용된 모든 객체 호출은 Proxy를 통해 인터셉트되는 요청 흐름을 가지게 됩니다.

@Autowired
XxxService xxxService;

// 위의 로직은 ApplicationContext에서 발생하는 DL, DI 생략하면 밑의 코드와 같다고 볼 수 있습니다.
XxxService xxxService = (XxxService) Proxy.newProxyInstance(
        XxxService.class.getClassLoader(), new Class[]{XxxService.class},
            (InvocationHandler) (proxy, method, args) -> {
                XxxService xxxService = new DefaultXxxService();
                                Method targetMethod = null;

                                // Verification?
                                if (proxyMethodMap.containsKey(method)) {
                                        targetMethod = cacheMethodMap.get(method);
                                } else {
                                        Object invoke = method.invoke(xxxService, args);
                                        return invoke;
                              }

                // Before Proxy....

                                // Invoke
                Object invoke = targetMethod.invoke(xxxService, args);

                                // After Proxy....

                return invoke;
            });

 

추가적으로 Spring Boot 2.0 부터는 CGLIB 설정을 변경하여 기본적으로 강제하게 됩니다.  spring.aop.proxy-target-class=true 그러므로 인터페이스 유무와 상관없이 CGLIB가 사용됩니다. 

 

 

잘못된 내용은 댓글로 작성 부탁드립니다!

 

 

참고자료

+ Recent posts