개요
Spring AOP 핵심 원리에 대한 정리가 끝났고 이제 스프링 AOP 적용 방식, 스프링 AOP 어드바이스에 대해서
딥하게 정리해 보겠다.
이전 Spring AOP 핵심 원리 정리에는 스프링 AOP에 바탕이 되는 개념들인 프록시 패턴, 동적 프록시(JDK Dynamic Proxy, CGLIB), ProxyFactory, Advice, Advisor, Pointcut, 빈 후처리기(BeanPostProcessor), 스프링이 제공하는 프록시 자동 생성기(빈 후처리기)에 대해서 정리해놨다.
모르는 키워드가 보인다면 참고하자!!
https://20240228.tistory.com/429
참고 자료
AOP 여러가지 적용 방식
AOP를 사용하면 핵심 기능과 부가 기능(횡단 관심사 Cross-Cutting-Concerns)이 코드상 완전히 분리되어서 관리된다.
AOP를 사용할 때 어떤 방식으로 실제 로직에 추가될 수 있을까?
→ 컴파일 시점, 클래스 로딩 시점, 런타임 시점(프록시)
컴파일 시점 - 컴파일 타임 위빙
.java 소스 코드를 컴파일러 사용해서 .class를 만드를 시점에 부가 기능을 로직을 추가할 수 있다.
이때 AspectJ가 제공하는 특별한 컴파일러(AspectJ 컴파일러)를 사용해야 한다.
컴파일 된 .class를 디컴파일 해보면 에스펙트 관련 호출 코드가 들어간다.
핵심 기능이 있는 컴파일된 코드 주변에 실제 부가 코드가 들어간다.
이렇게 원본 로직에 부가 기능 로직이 추가되는 것을 위빙(Weaving)이라 한다.
컴파일 시점 - 단점
컴파일 시점에 부가 기능을 적용하려면 특별한 컴파일러도 필요하고 복잡하다.
클래스 로딩 시점 - 로드 타임 위빙
자바를 실행하면 자바 언어는 .class 파일을 JVM 내부의 클래스 로더에 보관한다.
이때 중간에서 .class 파일을 조작한 다음 JVM에 올릴 수 있다.
이 시점에 애스펙트를 적용하는 것을 로드 타임 위빙이라 한다.
클래스 로딩 시점 - 단점
로드 타임 위빙은 자바를 실행할 때 특별한 옵션( java -java agent )을 통해 클래스 로더 조작기를 지정해야 하는
데, 이 부분이 번거롭고 운영하기 어렵다.
런타임 시점 - 런타임 위빙
프록시 방식의 AOP
런타임: 컴파일 이후 클래스 로더에 클래스도 다 올라간 상태 (자바의 main 메소드가 실행된 다음)
스프링 컨테이너의 도움과 프록시, DI, 빈 포스트 프로세서 사용한다.
정리
컴파일 시점, 클래스 로딩 시점에는 실제 대상 코드에 에스팩트를 통한 부가 로직이 추가된다.
그래서 AspectJ를 직접 사용해야 한다.
하지만 런타임 시점에 부가 기능을 적용하는 방식은 실제 대상 코드가 그대로 유지되고 프록시를 통해 부가 기능이 적용된다. 스프링 AOP는 런타임 시점을 사용한다.
AOP를 적용할 수 있는 지점을 조인 포인트(Join Point)라 한다.
AspectJ를 사용해서 컴파일 시점과 클래스 로딩 시점에 적용하는 AOP는 생성자, 필드 값, static 메소드 등...
모든 지점에 다 적용할 수 있지만 프록시 방식을 사용하는 스프링 AOP는 메소드 실행 지점에만 AOP를 적용할 수 있다.
또한 등록되는 빈을 빈 후처리기를 통해 바꿔치기하는 방식으로 적용하기 떄문에 스프링 컨테이너가 관리할 수 있는
스프링 빈에만 AOP를 적용할 수 있다.
but, 직접 프록시 클래스를 만들거나 동적 프록시 기술을 사용해서 프록시를 생성하면 꼭 빈이 아니더라도 부가 기능을 적용이 가능하긴 하다. 하지만 이는 매우 불편하고 유지보수가 불편하다.
스프링 AOP는 AspectJ 문법을 차용하고 프록시 방식의 AOP를 적용한다. AspectJ를 직접 사용하는 것이 아니다.
AOP 용어 정리
조인 포인트(Join point)
어드바이스가 적용될 수 있는 위치, 메소드 실행, 생성자 호출, static 메소드 접근, 필드 값 접근 같은 프로그램 실행 중 지점
스프링 AOP는 프록시 방식을 사용해서 조인 포인트는 항상 메소드 실행 시점으로 제한된다.
포인트컷(Pointcut)
조인 포인트 중에서 어드바이스가 적용될 위치를 선별하는 기능 (조인 포인트 >= 포인트컷)
주로 AspectJ 표현식을 사용해서 지정한다.
타켓(Target)
어드바이스를 받는 객체, 포인트컷으로 결정한다.
어드바이스(Advice)
부가 기능
특정 조인 포인트에서 AspectJ에 의해 취해지는 조치
Around, Before, After 같은 다양한 종류의 어드바이스가 있다.
어드바이저(Advisor)
스프링 AOP에서만 사용되는 용어
포인트컷 + 어드바이스
애스펙트(Aspect)
어드바이스 + 포인트컷의 모듈화
@Aspect
여러 어드바이스와 포인트 컷이 함께 존재
위빙(Weaving)
포인트컷으로 결정한 타겟의 조인 포인트에 어드바이스를 적용하는 것
위빙을 통해서 핵심 기능 코드에 영향을 주지 않고 부가 기능을 추가할 수 있다.
AOP 프록시
AOP 가능을 구현하기 위해 만든 프록시 객체 스프링에서는 ProxyFactory를 통해서 프록시 객체를 만들고 인터페이스 기반은 JDK 동적 프록시 / 클래스 상속은 CGLIB 프록시가 있다.
AOP 예제 (포인트컷 분리, 조합, 참조, 순서, 어드바이스 종류)
간단한 예제 - @Aspect 등록 과정, 프록시 생성 과정
@Slf4j
@Aspect
public class AspectV1 {
@Around("execution(* spring.aop.order..*(..))")
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable{
log.info("[log] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
}
AspectV1을 스프링 빈으로 등록하면 자동 프록시 생성기(AnotationAwareAspectAutoProxyCreator - 빈 후처리기)는 어드바이저 빌더(BeanFactoryAspectAdvisorsBuilder)를 통해서 어드바이저(포인트컷 + 어드바이스)를 생성하고 어드바이저 빌더 내부에 저장한다.
스프링 컨테이너에 저장하기전에 자동 프록시 생성기에 객체가 들어오는데 먼저 스프링 컨테이너에서 Advisor 빈(MethodInterceptor 구현체)을 모두 조회, 어드바이저 빌더 내부에 저장된 Advisor를 모두 조회해서 해당 객체가 프록시 적용 대상인지 아닌지 판단(포인트컷 기반)하고 적용 대상이면 프록시를 생성해서 컨테이너에 저장 아니면 원본 객체를 저장한다.
@Around 애노테이션의 값인 execution(* hello.aop.order..*(..)) → 포인트컷
@Aroung 애노테이션의 메소드인 doLog() → 어드바이스
포인트컷 분리 - @Pointcut
@Around에 포인트컷 표현식을 직접 넣을 수 있지만 @Pointcut 애노테이션을 통해서 분리가 가능하다.
→ 포인트컷 재사용이 가능
@Slf4j
@Aspect
public class AspectV2 {
@Pointcut("execution(* spring.aop.order..*(..))")
private void allOrder(){}
@Around("allOrder()")
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable{
log.info("[log] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
}
@Pointcut
메소드 이름 + 파라미터를 합쳐서 포인트컷 시그니처라 한다.
메소드 반환 타입은 void
코드 내용은 비워둔다.
내부에서만 사용하면 private을 사용해도 되지만 다른 애스팩트에서 참고하려면 public 접근 제어자를 사용해야 한다.
포인트컷 조합 1
@Slf4j
@Aspect
public class AspectV3 {
// spring.aop.order 패키지와 하위 패키지 전부 포함
@Pointcut("execution(* spring.aop.order..*(..))")
private void allOrder(){} // pointcut signature
// 클래스 이름 패턴이 *Service
@Pointcut("execution(* *..*Service.*(..))")
private void allService(){}
@Around("allOrder()")
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable{
log.info("[log] {}", joinPoint.getSignature()); // join point 시그니처
return joinPoint.proceed();
}
// hello.aop.order 패키지와 하위 패키지 이면서 클래스 이름 패턴이 *Service
@Around("allOrder() && allService()")
public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable{
try {
log.info("[트랜잭션 시작] {}", joinPoint.getSignature());
Object result = joinPoint.proceed();
log.info("[트랜잭션 커밋] {}", joinPoint.getSignature());
return result;
}
catch (Exception e){
log.info("[트랜잭션 롤백] {}", joinPoint.getSignature());
throw e;
}
finally {
log.info("[리소스 릴리즈] {}", joinPoint.getSignature());
}
}
}
doTransaction를 보면 @Around("allOrder() && allService()")이렇게 2개의 포인트컷이 조합되어 있다.
포인트컷은 && (AND), || (OR), ! (NOT) 3가지 조합이 가능하다.
포인트컷 조합 2
Pointcuts - 포인트 컷만 가지는 클래스
public class Pointcuts {
// spring.aop.order 패키지와 하위 패키지 전부 포함
@Pointcut("execution(* spring.aop.order..*(..))")
public void allOrder(){} // pointcut signature
// 클래스 이름 패턴이 *Service
@Pointcut("execution(* *..*Service.*(..))")
public void allService(){}
// allOrder && allService
@Pointcut("allOrder() && allService()")
public void orderAndService() {}
}
@Pointcut("allOrder() && allService()") 이렇게 포인트컷 + 포인트컷 조합도 가능하다.
@Slf4j
@Aspect
public class AspectV4 {
@Around("spring.aop.order.aop.Pointcuts.allOrder()")
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable{
log.info("[log] {}", joinPoint.getSignature()); // join point 시그니처
return joinPoint.proceed();
}
// hello.aop.order 패키지와 하위 패키지 이면서 클래스 이름 패턴이 *Service
@Around("spring.aop.order.aop.Pointcuts.orderAndService()")
public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable{
try {
log.info("[트랜잭션 시작] {}", joinPoint.getSignature());
Object result = joinPoint.proceed();
log.info("[트랜잭션 커밋] {}", joinPoint.getSignature());
return result;
}
catch (Exception e){
log.info("[트랜잭션 롤백] {}", joinPoint.getSignature());
throw e;
}
finally {
log.info("[리소스 릴리즈] {}", joinPoint.getSignature());
}
}
}
외부 포인트컷을 참조하려면 해당 포인트컷의 접근제어자가 public이고 FQPN(Fully Qualified Package Name)이 필요하다. 같은 패키지에 있는 경우 클래스명.메소드명도 가능하다.
어드바이스 순서
어드바이스는 기본적으로 순서를 보장하지 않는다.
순서를 지정하고 싶으면 @Aspect 적용 단위로 org.springframework.core.annotation.@Order 애노테이션을 적용해야 한다.
어드바이스 단위가 아니라 클래스 단위로 적용할 수 있다.
즉, 하나의 애스펙트(클래스)에 여러 어드바이스가 있다면 순서를 보장 받을 수 없다.
따라서 애스펙트를 별도의 클래스로 분리해야 한다.
@Slf4j
public class AspectV5Order {
@Aspect
@Order(2)
public static class LogAspect{
@Around("spring.aop.order.aop.Pointcuts.allOrder()")
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable{
log.info("[log] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
}
@Aspect
@Order(1)
public static class TxAspect{
@Around("spring.aop.order.aop.Pointcuts.orderAndService()")
public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable{
try {
log.info("[트랜잭션 시작] {}", joinPoint.getSignature());
Object result = joinPoint.proceed();
log.info("[트랜잭션 커밋] {}", joinPoint.getSignature());
return result;
}
catch (Exception e){
log.info("[트랜잭션 롤백] {}", joinPoint.getSignature());
throw e;
}
finally {
log.info("[리소스 릴리즈] {}", joinPoint.getSignature());
}
}
}
}
@Order: 숫자가 낮은 순서대로 실행된다.
TxAspect → LogAspect
어드바이스 종류1
@Slf4j
@Aspect
public class AspectStudy {
@Around("Pointcuts.orderAndService()")
public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable{
try {
// @Before
log.info("[트랜잭션 시작] {}", joinPoint.getSignature());
Object result = joinPoint.proceed();
// @AfterReturning
log.info("[트랜잭션 커밋] {}", joinPoint.getSignature());
return result;
}
catch (Exception e){
// @AfterThrowing
log.info("[트랜잭션 롤백] {}", joinPoint.getSignature());
throw e;
}
finally {
// @After
log.info("[리소스 릴리즈] {}", joinPoint.getSignature());
}
}
}
@Around
메서드 호출 전후에 수행, 가장 강력한 어드바이스, 조인 포인트 실행 여부 선택, 반환 값 변환, 예외 변환 등이 가능
@Before
조인 포인트 실행 이전에 실행
@AfterReturning
조인 포인트가 정상 완료후 실행
@AfterThrowing
메서드가 예외를 던지는 경우 실행
@After
조인 포인트가 정상 또는 예외에 관계없이 실행(finally)
@Around 및 나머지 어드바이스
@Around를 제외한 나머지 어드바이스들은 @Around가 할 수 있는 일의 일부만 제공할 뿐이다.
따라서 @Around 어드바이스만 사용해도 필요한 기능을 모두 수행할 수 있다.
정보 획득
모든 어드바이스는 org.aspectj.lang.JoinPoint 를 첫번째 파라미터에 사용할 수 있다 (생략 가능)
단 @Around는 ProceedingJoinPoint를 사용해야 한다.
ProceedingJoinPoint는 JoinPoint의 하위 타입
JoinPoint 인터페이스의 주요 기능
getArgs() : 메소드 인수를 반환
getThis() : 프록시 객체를 반환
getTarget() : 대상 객체를 반환
getSignature() : 조언되는 메소드에 대한 설명을 반환
ProceedingJoinPoint 인터페이스의 주요 기능
proceed() : 다음 어드바이스나 타켓을 호출
어드바이스 종류2
@Before
@Before("Pointcuts.orderAndService()")
public void doBefore(JoinPoint joinPoint){
log.info("[before] {}", joinPoint.getSignature());
}
@Around는 ProceedingJoinPoint.proceed()를 호출해야 다음 대상이 호출된다.
호출하지 않으면 다음 대상이 호출되지 않는다.
반면 @Before는 ProceedingJoinPoint.proceed() 자체를 사용하지 않는다 메소드 종료시 자동으로 다음 타겟이 호출된다.
@AfterReturning
@AfterReturning(value = "Pointcuts.orderAndService()", returning = "result")
public void doReturn(JoinPoint joinPoint, Object result){
log.info("[return] {} result= {}", joinPoint.getSignature(), result);
}
메소드 실행이 정상적으로 반환될 때 실행된다.
returning 속성에 사용된 이름은 어드바이스 메소드의 매개변수 이름과 일치해야 한다.
returning절에 지정된 타입의 값을 반환하는 메소드만 대상으로 실행된다. (부모 타입 허용)
@Around와 다르게 반환되는 객체를 변경할 수 없다.
반환 객체를 반환 객체에 있는 setter 같은 메소드를 사용해서 조작은 가능하다.
@AfterThrowing
@AfterThrowing(value = "Pointcuts.orderAndService()", throwing = "ex")
public void doThrowing(JoinPoint joinPoint, Exception ex){
log.info("[exception] {} message= {}", joinPoint.getSignature(), ex.getMessage());
}
메서드 실행이 예외를 던져서 종료될 때 실행
throwing 속성에 사용된 이름은 어드바이스 메서드의 매개변수 이름과 일치해야 한다
throwing 절에 지정된 타입과 맞는 예외를 대상으로 실행한다. (부모 타입 허용)
@After
@After(value = "Pointcuts.orderAndService()")
public void doAfter(JoinPoint joinPoint){
log.info("[after] {}" , joinPoint.getSignature());
}
메서드 실행이 종료되면 실행된다. (finally)
정상 및 예외 반환 조건을 모두 처리한다.
일반적으로 리소스를 해제하는 데 사용한다
@Around
메서드의 실행의 주변에서 실행된다. 메서드 실행 전후에 작업을 수행한다. (가장 강력한 어드바이스)
조인 포인트 실행 여부 선택 joinPoint.proceed() 호출 여부 선택
전달 값 변환: joinPoint.proceed(args[])
반환 값 변환
예외 변환
트랜잭션 처럼 try ~ catch~ finally 모두 들어가는 구문 처리 가능
어드바이스의 첫 번째 파라미터는 ProceedingJoinPoint를 사용해야 한다.
proceed()를 여러번 실행할 수도 있다(재시도)
순서 (동일한 @Aspect 안에 여러 종류의 어드바이스)
실행 순서: @Around , @Before , @After , @AfterReturning , @AfterThrowing
'Spring > Spring AOP' 카테고리의 다른 글
[Spring] AOP 핵심 원리 정리 (3) | 2024.12.15 |
---|