스프링의 AOP는 AspectJ의 문법을 차용하고, 프록시 방식의 AOP를 제공한다. AspectJ를 직접 사용하는 것이 아니다.
스프링 AOP를 사용할 때는 @Aspect를 사용하는데 이것 또한 AspectJ가 제공하는 애노테이션이다.
포인트컷 범위 (Around)에 해당하는 모든 메서드들은 AOP에 적용이 된다.
- 메서드 이름과 파라미터를 합쳐서 포인트컷 시그니처 라고 한다.
- 메서드 반환타입은 void여야 한다.
- 다른 Aspect에서 참조하려면 public로 열어줘야함
- 포인트 컷은 && , || , ! 연산자들을 조합해서 사용 가능함
어드바이스는 기본적으로 순서를 보장하지 않는다.
순서를 보장하고 싶으면 포인트컷을 따로 클래스를 빼고 Order() 를 사용하면 된다.
@Around
: 메서드 호출 전후에 수행, 가장 강력한 어드바이스, 조인 포인트 실행 여부 선택, 반환 값 변환, 예외
변환 등이 가능
@Before
: 조인 포인트 실행 이전에 실행
@AfterReturning
: 조인 포인트가 정상 완료후 실행
@AfterThrowing
: 메서드가 예외를 던지는 경우 실행
@After
: 조인 포인트가 정상 또는 예외에 관계없이 실행(finally)
@Around를 제외한 나머지 어드바이스들은 @Around가 할 수 있는 일의 일부만 제공할 뿐이다. 따라서 @Around 어드바이스만 사용해도 필요한 기능 수행 가능
모든 어드바이스들은 JoinPoint를 파라미터로 사용가능
@Around 무조건 ProceedingJoinPoint를 사용해야함
proceed() : 다음 어드바이스나 타겟을 호출한다.
- @Around와 다르게 작업 흐름을 변경할 수는 없다.
- @Around는 ProceedingJointPoint.proceed() 를 호출해야만 다음 대상이 호출되지만, @Before은 종료시 자동으로 다음 타겟이 호출됨
getArgs()
: 메서드 인수를 반환
getThis()
: 프록시 객체를 반환
getTarget()
: 대상 객체를 반환
getSignature()
: 조언되는 메서드에 대한 설명을 반환
toString()
: 조언되는 방법에 대한 유용한 설명을 인쇄
-> 이렇게 많은 어드바이스들이 존재하는 이유는 @Around하나로 많은 기능을 제공하지만 그만큼 실수할 가능성이 있기때문, 또한 다들 개발자가 중간에 작업을 할경우 어드바이스 이름들이 명시적이여서 실수 가능성을 낮춰준다.
execution
: 메소드 실행 조인 포인트를 매칭한다. 스프링 AOP에서 가장 많이 사용within
: 특정 타입 내의 조인 포인트를 매칭한다.args
: 인자가 주어진 타입의 인스턴스인 조인 포인트this
: 스프링 빈 객체(스프링 AOP 프록시)를 대상으로 하는 조인 포인트target
: Target 객체를 대상으로 하는 조인 포인트@target
: 실행 객체의 클래스에 주어진 타입의 애노테이션이 있는 조인 포인트@within
: 주어진 애노테이션이 있는 타입 내 조인 포인트@annotation
: 메서드가 주어진 애노테이션을 가지고 있는 조인 포인트를 매칭@args
: 전달된 실제 인수의 런타임 타입이 주어진 타입의 애노테이션을 갖는 조인 포인트bean
: 스프링 전용 포인트컷 지시자, 빈의 이름으로 포인트컷을 지정한다.
execution(접근제어자? 반환타입 선언타입?메서드이름(파라미터) 예외?)
- 메소드 실행 조인포인트 매칭
- ?는 생략 가능
- *같은 패턴을 지정할 수 있다.
AspectJExpressionPointcut 에 pointcut.setExpression
을 통해서 포인트컷 표현식을 적용할 수 있다.
pointcut.matches(메서드, 대상 클래스)를 실행하면 지정한 포인트컷 표현식의 매칭여부를 boolean 으로 반환
- 접근제어자?:
public
- 반환타입:
String
- 선언타입?:
come.example.member.MemberServiceImpl
- 메서드이름:
hello
- 파라미터:
(String)
- 예외?: 생략
- 접근제어자?: 생략
- 반환타입:
*
- 선언타입?: 생략
- 메서드이름:
*
- 파라미터:
(..)
- 예외?: 없음
.
정확하게 해당 위치 패키지..
해당 위치의 패키지와 그 하위 패키지도 포함부모 타입을 표현식에 선언한 경우 부모 타입에서 선언한 메서드가 자식 타입에 있어야 매칭된다
(String)
: 정확하게 String 타입 파라미터
()
: 파라미터가 없어야 한다.
(*)
: 정확히 하나의 파라미터, 단 모든 타입을 허용한다.
(*, *)
: 정확히 두 개의 파라미터, 단 모든 타입을 허용한다.
(..)
: 숫자와 무관하게 모든 파라미터, 모든 타입을 허용한다. 참고로 파라미터가 없어도 된다. 0..*
로 이해하면 된다.
(String, ..)
: String 타입으로 시작해야 한다. 숫자와 무관하게 모든 파라미터, 모든 타입을 허용한다.
예) (String)
, (String, Xxx)
, (String, Xxx, Xxx)
허용
- @target(com.example.member.annotation.ClassAop)
- @within(com.example.member.annotation.ClassAop)
주로 클래스 aop 적용범위 설정할때 사용
@target
: 실행 객체의 클래스에 주어진 타입의 애노테이션이 있는 조인 포인트, 인스턴스의 모든 메서드를 조인 포인트로 적용
@within
: 주어진 애노테이션이 있는 타입 내 조인 포인트, 해당 타입 내에 있는 메서드만 조인포인트로 적용
- args , @args, @target 은 단독으로 사용하면 안됨 왜냐 이것들은 실제 객체 인스턴스가 생성되고 실행될때(런타임) 어드바이스 적용 여부를 확인할 수 있음
- 프록시가 있어야 실행 시점에 판단 가능, 프록시 생성 시점은 애플리케이션 로딩시점
- 프록시가 없으면 모든 스프링빈에 AOP프록시를 적용하려고함, 스프링빈 중에는 final로 지정된 빈들도 있어서 여기서 오류발생 가능성 있음
- 그래서 적용 대상을축소하는 표현식과 함께 사용해야함
Trace 애노테이션 AOP
Retry 애노테이션 AOP
@annotation(retry) , Retry retry를 통해 어드바이스에 애노테이션을 파라미터로 전달함
스프링은 프록시 방식의 AOP 를 사용한다. 즉 AOP를 사용하려면 프록시를 통해서 대상객체(target)을 호출해야한다.
AOP를 적용하면 스프링은 대상 객체 대신에 프록시를 스프링 빈으로 등록한다. 따라서 스프링은 의존관계 주입시에 항상 프록시 객체를 주입한다. 프록시 객체가 주입되기 때문에 대상 객체를 직접 호출하는 문제는 일반적으로 발생하지 않 는다.
이렇게 외부에서 호출이아닌 내부에서 자기자신 인스턴스 메서드 호출시에는 실제 대상 객체의 메서드를 호출한다.
스프링 AOP는 메서드 내부 호출에 프록시를 적용할 수 없다. 이를 해결하기 위해 AOP를 직접 적용하는 AspectJ를 사용하면 해결 되지만 이 방식은 난이도가 상당히 높기 때문에 추천하지는 않음.
생성자 주입은 빈 순환 참조를 만듬(자기자신을 생성하면서 주입해야하니까).
수정자 주입은 스프링이 생성된 이후에 주입할 수 있기 때문에 에러발생 x
이렇게 해서 주입받은 자기 자신은 타겟(실제객체)가 아닌 프록시를 주입받게 된다.
ObjectProvider(Provider), ApplicationContext를 사용해 스프링 빈을 지연해서 조회하면 된다.
ObjectProvider는 객체를 스프링컨테이너에서 조회하는 것을 스프링 빈 생성 시점이 아니라 실제 객체를 사용하는 시점으로 지연할 수 있다.
getObject()를 호출하는 시점에 스프링 컨테이너에서 빈을 조회한다.
MemberServiceImpl은 JDK동적 프록시를 사용했으며 MemberService를 구현했다. 그러므로 MemberService 인터페이스 기반으로 프록시를 생성함(JDK동적 프록시 이기 때문에)
인터페이스 기반으로 프록시를 생성하기 때문에 여기서 생성된 프록시를 대상 클래스(Impl)로 캐스팅하면 예외가 발생한다.
spring.aop.proxy-target-class=false
설정을 사용해서 스프링 AOP가 JDK 동적 프록시를 사용하 도록 했다.
이런식으로 테스트 해보면 이런 에러가 나온다.
BeanNotOfRequiredTypeException: Bean named 'memberServiceImpl' is expected to be
of type 'hello.aop.member.MemberServiceImpl' but was actually of type
'com.sun.proxy.$Proxy54'
JDK 프록시는 인터페이스 기반으로 만들어지므로 MemberServiceImpl 주입시 에러발생
이런식으로 AOP가 CGLIB 프록시를 사용하도록 설정하면 문제 없다.
이렇게 보면 CGLIB가 만병통치약 같지만 단점들도 존재한다.
스프링 3.2, CGLIB를 스프링 내부에 함께 패키징
기본생성자 문제는 objenesis
라는 특별한 라이브러리를 사용해서 기본 생성자 없이 객체 생성이 가능하다. 참고로 이 라이브러리는 생성자 호출 없이 객체를 생성할 수 있게 해준다.
생성자 2번 호출문제도 objenesis
라는 특별한 라이브러리 덕분에 가능해졌다.
AOP를 적용할때는 final을 잘 안쓰기 때문에 이 부분은 크게 문제가 되지 않는다.