Post

Spring AOP / @annotation resolve

AOP(Aspect Oriented Programming)는 언제 유용한가?

가끔 AOP를 쓰지 말아야 할 곳에 AOP를 쓰는 경우를 보게 된다.

어떤 기능은, 쓸 수 있다고 쓰는게 아니라, 쓰는게 효용이 있을 때 확실한 근거를 가지고 올바르게 써야한다.

AOP를 쓰기 전에 이런 물음을 던져보아야 한다. ‘어떤 기능이 현재 클래스의 관심 밖이라면, 별도 클래스로 분리해서 호출하면 되는 것 아닌가?’

1
2
3
4
5
6
7
class A(otherClass: Cls) {
  fun method(p: Param) {
    otherClass.doSomething(p)
    ...
  }
}

맞다. 위와 같이 짤 수 있고, 어색하지 않다면 이런 코드를 굳이 AOP로 바꾸는게 별다른 이점은 없다.

하지만 AOP가 분명히 유리한 경우가 있다. 아래는 그 예시다.

  • 메서드 전후 로깅, 프로파일링, 카운팅이 필요한 경우
  • 메서드 진입 전 공통 접근제어가 필요한 경우 (특정 기능에 대한 점검 설정 등)
  • 메서드 전체를 감싸는 트랜잭션이 필요한 경우
  • Circuit Breaker

이런 기능들은 시스템 전반에서 처리하는 비즈니스와 큰 관련이 없는 경우가 대부분이다. tx를 예로 들어, AOP를 쓰지 않는 경우와 비교해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class A {
  val txUtil: Cls
  
  // 메서드 시작과 끝에 붙이는 방식. 실수하기 쉽고 불편하다.
  // 게다가 method1 안에 비즈니스 로직과 tx처리 코드가 함께 위치하니 지저분하다.
  fun method1(p: Param) {
    txUtil.begin()
    ...
    txUtil.commit()
  }
  // 제일 바깥에 람다를 쓰면 조금 낫긴 하지만 indent depth가 늘어난다.
  // 추가로 loggingUtil도 써야 한다면? indent depth는 계속 늘어날거다.
  fun method(p: Param) {
    txUtil.tx { 
      ...
    }
  }
  
  // 여러개의 annotation이 붙어도 깔끔하고, 비침습적이다.
  @Transactional
  fun method(p: Param) {
    ...
  }
}

반면 AOP를 쓰지 말아야 할 곳에 잘못 쓰는 경우, 아래와 같은 문제가 발생 할 수 있다.

1
2
3
4
5
6
7
8
9
10
class A(otherClass: Cls) {
  @DoBusinessLogicB
  fun method(p: Param) {
    // 새로 추가되는 BusinessLogicC는 항상 BusinessLogicB 보다 늦게 실행된다.
    // C가 먼저 실행되도록 하려면 C도 AOP로 만들어야 한다.
    // 애초에 B를 AOP로 구현하지 말았어야 한다.
    ...
  }
}

스프링에서 @annotation resolve는 AOP proxy를 이용한다.

스프링에는 달아 놓기만 하면 알아서 동작하는 많은 애너테이션들이 있다. 런타임에 이런 애너테이션이 동작하려면, 원래는 애너테이션을 적용하기 위한 별도의 proxy 함수가 필요하다.

1
2
3
4
5
- 별도의 proxy 함수를 호출
-- 리플렉션으로 애너테이션 체크
-- 애너테이션 동작 - 실제 함수 호출
( 애너테이션에 따라 시점은 다를 수 있음. )

그러나 스프링에서는 이런 별도의 proxy 함수를 호출하지 않아도 annotation이 동작한다. 이게 가능한 이유는 AOP proxy가 동작하기 때문이다.

스프링 컨테이너는 어떤 Bean을 주입할 때 proxy로 감싼 aspect-aware bean을 주입해준다.

따라서 어떤 개체가 DI 받게되는 bean은 aspect-aware bean이고, 이를 통해 메서드를 호출하면 실은 proxy 함수가 호출된다. (이게 스프링에서 AOP를 구현하는 방식이다.) 따라서 별도의 proxy 함수를 직접 불러주지 않아도, @애너테이션을 달아 놓는 것 만으로도 동작하게 된다.

이런 식으로 공통 기능을 별도로 분리해내서 핵심 기능 작성에만 집중할 수 있도록 하는 방식을 AOP라고 하며, 스프링의 AOP는 proxy 기반으로 동작한다.

internal use method에 @애너테이션을 붙여도 동작하지 않는 현상

아래는 @HystrixCommand를 예로 들어 설명했지만, @Transactional 같은 모든 애너테이션에도 적용 되는 얘기다.

1
2
3
4
5
6
7
8
9
public class PartnerApiClient {
    public UserInfo getUserInfo(String userCi) {
        this._getUserInfo(userCi);
    }
    @HystrixCommand(fallbackMethod = "fallback")
    private UserMileageInfoResponseWrapper _getUserInfo(String userCi)  {...}
    private UserMileageInfoResponseWrapper fallback(String userCi) {... }
}

애너테이션에 따르면 분명 _getUserInfo에서 실패하면 fallback 메서드로 빠져야 하는데 그렇지 않다. 왜일까? _getUserInfo가 private이라? 아니다. 이는 Spring의 AOP 지원이 프록시 기반이라는 점에서 기인한다.

The general idea is that spring AOP is proxy-based, i.e. it assumes that when the bean is used as a dependency and its method(s) should be advised by particular aspect(s) the container injects aspect-aware bean proxy instead of the bean itself.

https://github.com/Netflix/Hystrix/issues/1020#issuecomment-199416640

http://blog.harmonysoft.tech/2009/07/spring-aop-top-problem-1-aspects-are.html

위에서 애너테이션이 동작하려면 aspect-aware bean이어야 하며, 스프링 컨테이너는 어떤 Bean을 DI할 때 메서드들을 proxy 함수로 wrapping한 aspect-aware bean을 주입해준다고 했다.

반면 this.\_getUserInfo() 처럼 Bean 내부에서 자기 자신의 메서드를 호출하게 되면, 컨테이너로부터 DI받은 Bean을 통해 메서드를 호출하는게 아니라 proxy로 감싸져 있지 않은 자기 자신 메서드를 호출하는 것이다. 즉, aspect-aware bean이 아니라 pure-bean의 메서드를 호출하게 되는 것. 그래서 AOP가 동작하지 않아 @HystrixCommand가 동작하지 않는다.

해결 방법은 4가지.

1. self-invocation 하지 않도록 리팩토링 하는 방법.

2. replace self-calls with aspect-aware proxy calls, i.e. use the statement like

1
2
3
4
((PartnerApiClient)AopContext.currentProxy())._getUserInfo(userCi);

@EnableAspectJAutoProxy(exposeProxy = true) // Application 클래스에 이 것도 추가해주어야 함.

이 방법은 AopContext라는 API를 사용하면서 프레임워크와 결합이 생긴다.

3. use aspectj weaving ( 별도의 API가 비즈니스 로직에 들어가지 않으니 프레임워크 결합 없이 처리할 수 있어 추천하는 방법 )

https://minwan1.github.io/2017/10/29/2017-10-29-Spring-Transaction,AspectJ-Compile/

4. 아예 @애너테이션을 안쓰고 Programmatic한 방법으로 직접 API를 불러 사용하는 방법.

예를 들면 @Transactional 대신 PlatformTransactionManager를 직접 DI 받아서 사용한다던가. 단, 2번 처럼 프레임워크 API를 사용하면서 생기는 프레임워크 의존성에 대해서는 생각해보아야 함.

참고) Spring에서 annotation은 interface로 부터는 상속되지 않고, abstract class로부터는 상속된다.

https://stackoverflow.com/questions/18585374/spring-aop-inherited-annotation-from-an-interface

물론 class에서 상속 받는 것도 @Inherited 메타 애너테이션이 붙어 있는 애너테이션이어야 가능하다.

이런 애너테이션 상속은, 프레임워크에서 해당 메서드(필드)에 달려있는 애너테이션을 어디까지 검색할 것인지에 달려있으므로 프레임워크 마다 다를 수 있다.

Meta annotation은 Spring feature다.

https://stackoverflow.com/questions/33345605/java-custom-annotation-aggregate-multiple-annotations

그래서 JUnit이나 다른 애너테이션과는 동작하지 않을 수 있다.

AOP 구현 기술 - CGLib과 Dynamic Proxy

https://velog.io/@suhongkim98/JDK-Dynamic-Proxy%EC%99%80-CGLib

This post is licensed under CC BY 4.0 by the author.