Udemy 강의내용 정리 입니다.
Spring Professional Certification Exam Tutorial 02 - AOP(Aspect Oriented Programming)
Asepect Oriented Programming의 약자로 관점 지향 프로그래밍이라고 불리며 프로그램 로직으로부터 부가적인 관점에서의 문제해결(Cross Cutting Issue)을 위한 그룹을 분리하여 객체지향 프로그래밍을 보완하는 프로그래밍 페러다임
관점 지향 프로그래밍은 흩어진 Aspect를 모듈화 할 수 있는 프로그래밍 기법을 말한다. 관점 지향은 어떤 로직을 기준으로 핵심적인 관점, 부가적인 관점으로 나누어 본다는 말이고 따라서 관점을 기준으로 각각 모듈화하는 프로그래밍 기법인 것
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
InvocationHandler
를 포함하여 하나의 객체로 변환한다.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;
}
}
Enhancer
⇒ Target 클래스의 non-final 메소드를 오버라이딩 하는 서브 클래스 생성Callback
⇒ 프록시로 호출이 들어오면 Target으로 가기전에 모든 호출을 가로채CallbackFilter
⇒origin의 메소드별로 호출될 때 가로채고자 하는 Callback을 지정final
이나 private
와 같은 modifier가 적용된 메서드에 대해서는 오버라이딩이 불가하므로 Aspect를 적용할 수 없다.CGLib Proxy의 단점, 버전에 따른 보완
- 의존성의 추가 (Spring 3.2 이후 버전의 경우 Spring Core 패키지에 포함되어 있음)
- default 생성자가 필요하다. (현재는 objenesis 라이브러리를 통해 해결)
- 타겟의 생성자가 두 번 호출된다. (현재는 objenesis 라이브러리를 통해 해결)
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");
}
}
자바에서 완벽한 AOP 솔루션 제공을 목표로 하는 기술
@Aspect
어노테이션 인식org.springframework:spring-aspects
사용하는 것이 일반적)@Configuration
어노테이션 적용된 클래스)에 @EnalbeAspectJAutoProxy
어노테이션을 적용 (어노테이션 선언하지 않을시 @Aspect
를 스캔할 수 없다)@Aspect
어노테이션 적용@Before
: 메서드 실행 전에 실행하는 Advice@AfterReturning
: 메서드 정상 실행 후 실행하는 Advice@AfterThrowing
: 메서드 실행시 예외 발생시 실행하는 Advice@After
: 메서드 정상 실행 또는 예외 발생 상관없이 실행하는 Advice@Around
: 위 네가지 Advice를 모두 포함 , 모든 시점에서 실행할 수 있는 Advice@EnableAspectJAutoProxy
어노테이션은 @Aspect
어노테이션이 선언된 클래스들을 인식할 수 있도록 하고 빈들에 대한 프록시 객체를 생성한다.
프록시 객체를 생성하는 내부 프로세스는 AnnotationAwareAspectJAutoProxyCreator
에 의해 이루어진다. 각각의 빈들에 대한 프록시 객체를 생성함으로써 스프링은 클라이언트의 요청을 intercept 하고 @Before
/ @After
/ @AfterReturning
/ @AfterThrowing
/ @Around
어드바이스를 실행한다.
@Aspect
어노테이션 만으로는 클래스는 해당 클래스에 대한 빈을 생성하지 않는다. 컴포넌트 스캔이 되도록@Component
어노테이션을 붙히거나 빈으로 등록해야한다.
JoinPoint.getArgs()
: JoinPoint에 전달된 인자를 배열로 반환한다.JoinPoint.getThis()
: 프록시 객체를 반환한다.JoinPoint.getTarget()
: 프록시가 가리키는 타겟 객체를 반환한다.JoinPoint.getSignature()
: 조인되는 메서드에 대한 정보(리턴타입, 이름, 매개변수)를 반환한다.String getName()
: 클라이언트가 호출한 메서드의 이름을 반환한다.String toLongString()
: 클라리언트가 호출한 메서드의 리턴타입, 이름, 매개변수를 패키지 경로까지 포함해서 반환한다.String toShortString()
: 클라이언트가 호출한 메서드 시그니처를 축약한 문자열로 반환한다.proceed()
: 다음 어드바이스나 타겟을 호출 @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개 이상을 의미한다. |
+ | 클래스명 뒤에 붙여 쓰며, 해당 클래스와 해당 클래스의 서브클래스, 혹은 구현 클래스 모두를 의미한다. |
포인트컷 표현식은 다른 포인트컷의 이름을 참조할 수 있으며 논리 연산자(&&
, ||
, !
, &
, ^
, ~
)를 통해 결합하거나 배제할 수 있다. 이를 통해 더 구체적으로 조인포인트를 지정할 수 있다.
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