[Spring] AOP(Aspect Oriented Programming)
AOP란?
- Aspect Oriented Programming은 프로그램 구조를 다른 방식으로 생각하게 함으로써 OOP를 보완한다.
- OOP에서 모듈화의 핵심단위는 클래스이지만 AOP에서 모듈화의 핵심단위는 관점(Aspect)이다.
- 관점은 다양한 타입과 객체에 걸친 트랜잭션 관리같은 관심을 모듈화할 수 있게 한다.
각각에 적용되는 횡단 관심사를 주요 관심사와 분리해서 관점별로 모듈화하는 것이다.
개념은 간단하지만 관련 용어나 적용하는 방식이 이해할게 많은 것 같다.
먼저 주요 용어들을 살펴보자
1) Aspect
- 여러 클래스에 걸친 횡단 관심사의 모듈을 말한다. 즉 클래스.
- 하나 이상의 Pointcut과 Advice의 조합으로 만들어지는 AOP의 기본 모듈이다.
- Spring framework에서는 @Aspect를 사용하거나 XML에서 설정할 수 있다.
package com.example.aspect;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class LoggingAspect {
@Before("execution(* com.example.service.*.*(..))")
public void beforeServiceMethods(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
String className = joinPoint.getTarget().getClass().getSimpleName();
System.out.println("Calling method " + methodName + " of class " + className);
}
}
얘를 들어 위의 코드와 같이 사용할 수 있다. 이 클래스는 Aspect로 사용될 것이며 AspectJ언어를 사용하여 정의된 포인트컷 표현식은 @Before 어드바이스의 매개변수로 전달된다.
excution: 메서드 실행 시점을 지정하는 포인트컷의 지시자
*: 반환 타입을 나타내는 와일드 카드로, 어떤 반환 타입이든 매치된다.
com.example.service.*: 해당 패키지 내의 모든 클래스를 대상으로 한다.
*.*(..): 모든 메서드를 대상으로 한다. 괄호 안의 ..은 모든 매개변수 타입과 개수를 포함한다.
2) Join Point
- AOP(Aspect-Oriented Programming)에서 중요한 개념으로, 프로그램 실행 중 특정한 지점을 가리킨다. (메서드 실행 시점, 메서드 호출, 필드 접근, 생성자 호출, 예외 발생 등)
- Pointcut의 후보라고 생각할 수 있다.
- Spring AOP에서는 메서드 실행만 대상이다.
- AspectJ에서는 JoinPoint 객체를 사용하여 조인 포인트에서의 실행 정보를 제공한다.
JoinPoint객체가 제공하는 정보
- getSignature(): 조인 포인트의 시그니처(메서드, 필드 등)를 반환합니다.
- getArgs(): 메서드 호출 시 전달된 인자들을 반환합니다.
- getTarget(): 조인 포인트가 발생한 대상 객체를 반환합니다.
- getThis(): Advice가 실행 중인 객체를 반환합니다.
- proceed(): 다음 어드바이스나 타겟을 호출한다.
3) Advice
- 타겟에 제공할 부가 기능을 담은 모듈을 말한다.
- 적용될 메서드를 선정한다.
- 특정 Join Point에서 Aspect가 취하는 행동
- Advice 는 포인트컷과 관련하여 메소드 실행 전, 후, 전/후 를 결정하기위해 사용한다.
- 스프링에서는 각 형태마다 어노테이션을 제공한다.
4) Pointcut
- Advice를 적용할 Join Point를 선별하는 작업 또는 그 기능을 적용한 모듈
- Advice는 Pointcut 표현식과 연결되고 Pointcut이 매치한 Join Point에서 실행된다.
5) Target object
- 부가기능을 부여할 대상
- 하나 이상의 Aspect로 어드바이즈드된 객체
- advised object라고 부르기도 함
6) AOP Proxy
- 클라이언트와 타겟 사이에 투명하게 존재하면서 부가기능을 제공하는 오브젝트
- aspect 계약(어드바이스 메서드 실행 등)을 위해 AOP에 의해 생성된 객체
- spring AOP는 proxy의 메커니즘을 기반으로 AOP proxy를 제공하고 있다. 사용자의 특정 호출 시점에 IoC컨테이너에 의해 AOP를 할 수 있는Proxy Bean을 생성하며 동적으로 생성된 Proxy Bean은 타깃의 메소드가 호출되는 시점에 부가기능을 추가할 메소드를 자체적으로 판단하고 가로채어 부가기능을 주입한다.
Spring은 AOP Proxy를 생성하는 과정에서 자체 검증 로직을 통해 타깃의 인터페이스 유무를 판단한다.
1) JDK Dynamic Proxy : 인터페이스가 있을 경우 생성
2) CGLib : 인터페이스가 없을 경우 생성
7) Advisor
- Pointcut과 Advice를 하나씩 갖고 있는 객체
- 스프링 AOP에서만 사용되는 용어
8) Weaving
- pointcut에 의해서 결정된 타겟의 join point에 부가기능인 advice를 삽입하는 과정을 뜻한다.
- weaving은 aop가 핵심기능(타겟)의 코드에 영향을 주지 않으면서 필요한 부가기능을 추가할 수 있도록 해주는 핵심적인 처리과정이다.
종류로는 크게 4가지가 있다.
1. Compile Time Weaving(CTW): AspectJ에서 컴파일러를 통해 컴파일 과정에서 바이트 코드 조작을 하여 Advisor코드를 직접 삽입하여 위빙을 수행한다. 가장 빠르지만 충돌 위험이 있다.
2. Runtime Weaving(RTW): Spring AOP에서 사용하는 방식으로, Proxy를 생성해서 실제 타깃 오브젝트의 변형 없이 위빙을 수행한다. 실제 런타임시 메소드 호출 시에 위빙이 이루어지는 방식이다. point cut에 대한 advice수가 늘어날수록 성능이 떨어진다는 단점이 있다.
3. Load Time Weaving(LTW): ClassLoader를 이용해 클래스가 JVM에 로드될 때 바이트 코드 조작을 통해 위빙되는 방식이다.
4. Post-Compile Weaving: 컴파일 후 기존 클래스 파일과 JAR 파일을 위빙하는데 사용, Compile Time Weaving과 같은 방식으로 처리한다.
관련 용어들을 정리하면서 대충 AOP가 어떻게 사용되는지 정리가 된 것 같다.
이제 실제 AOP를 사용하기 위한 준비과정들을 살펴보자
AOP 적용하기
- pom.xml 파일에 spring-aspects 라이브러리 의존성을 추가한다.
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.3.15</version>
</dependency>
- AspectJ(Annotation)을 활성화한다.
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
}
- @AspectJ 스타일은 일반 java 에 annotation 을 설정하는 방식이다.
- 스프링 프레임워크는 AspectJ 5 의 anntation 을 사용하지만 AspectJ의 컴파일러나 위버(Weaver) 를 사용하지 않는다.
- 쉽게 말해서, AspectJ에서 제공하는 어노테이션을 활용하기는 하지만 위빙 방식은 스프링 AOP의 방식을 따른다는 것이다.
- 사용하고자 하는 Aspect에 빈으로 선언하고 어노테이션을 설정한다.
@Aspect
@Component
public class LoggingAspect {
...
}
- 이와 같이 사용할 것이다.
다양한 활용을 위한 포인트컷과 Advice 내용 정리..
포인트 컷 표현식
@Pointcut(
"execution(" // Pointcut Designator
+ "[접근제한자 패턴] " // public
+ "리턴타입 패턴" // long
+ "[패키지명, 클래스 경로 패턴]" // com.nhnacademy.GreetingService
+ "메소드명 패턴(파라미터 타입 패턴|..)" // .greet(User, String)
+ "[throws 예외 타입 패턴]"
+")"
)
*) Pointcut Designator
- execution: 메소드 실행 조인포인트와 매칭, 주로 사용
- within: 주어진 타입으로 조인 포인트 범위를 제한
--> 차이: execution은 메서드 지칭, within은 클래스 지칭
- this: 주어진 타입을 구현한 스프링 AOP Proxy객체에 매칭
- target: 주어진 타입을 구현한 타겟 객체에 매칭
- args: 주어진 타입의 인수들을 이용해 매칭
- @target: 주어진 타입의 애너테이션을 가진 클래스의 인스턴스 매칭
- @args: 실제 인수의 런타임 타입이 주어진 타입의 어노테이션 가질 경우 매칭
- @within: 주어진 타입의 어노테이션을 타입들로 제한하여 매칭
- @annotation: 주어진 어노테이션을 가지고 있을 경우 매
- bean: 스프링 AOP 에서 지원하는 추가적인 포인트컷 지정자, 빈의 이름에 해당하는 메서드 실행 매
Advice - JoinPoint 활용
1) advice에 파라미터 넘기기
@Before("com.xyz.myapp.CommonPointcuts.dataAccessOperation() && args(account,..)")
public void validateAccount(Account account) {
// ...
}
- args(account,..) 표현식은 두가지 의미를 내포한다.
- 1개 이상의 파라미터를 받는 메소드 실행에 매칭, 첫번째 인자는 Account 클래스의 인스턴스 이어야 한다.
- Account 객체는 Advice의 account 파라미터에 바인딩한다.
2) custom annotation 매칭
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Auditable {
AuditCode value();
}
- @annotation 포인트컷 지정자로 설정된 Annotation 을 Advice 파라미터로 참조 할 수 있다.
@Before("com.xyz.lib.Pointcuts.anyPublicMethod() && @annotation(auditable)")
public void audit(Auditable auditable) {
AuditCode code = auditable.value();
// ...
}
3) 파라미터와 제네릭
- Advice의 파라미터의 타입으로 매칭을 제한할 수 있다.
@Before("execution(* ..Sample+.sampleGenericMethod(*)) && args(param)")
public void beforeSampleMethod(MyType param) {
// Advice implementation
}
예를 들어, 아래와 같은 코드가 존재할 때
public class SampleSubclass extends Sample {
public void sampleGenericMethod(MyType myType) {
// Method implementation
}
public void sampleGenericMethod(String str) {
// Method implementation
}
}
sampleGenericMethod(MyType myType): MyType 타입의 파라미터를 가지므로 beforeSampleMethod Advice가 실행되고, sampleGenericMethod(String str): String 타입의 파라미터를 가지므로 beforeSampleMethod Advice가 실행되지 않는다.
참고
NHN-Academy 교육자료