스프링 AOP는 AspectJ의 문법을 차용하고, AspectJ가 제공하는 기능의 일부만 제공한다.
AspectJ 프레임워크는 스스로를 다음과 같이 설명한다.
자바 프로그래밍 언어에 대한 완벽한 관점 지향 확장
횡단 관심사의 깔끔한 모듈화
오류 검사 및 처리
동기화
성능 최적화(캐싱)
모니터링 및 로깅
AOP를 적용하는 3가지 방식(시점)
부가기능을 핵심 기능에 적용하는 방식 3가지
1. 컴파일 시점
컴파일 시점에 실제로 부가기능 코드를 실제 코드에 붙여버린다.
따로 관리되고있지만 컴파일때 합쳐지면서 부가기능이 추가되는 구조
특별한 컴파일러가 필요하는 등 여러가지로 복잡하다
2. 클래스 로딩 시점
.class 파일을 JVM 내부의 클래스 로더에 보관할때 중간에서 .class 파일을 조작해 부가기능을 추가해서 JVM에 올릴 수 있다.
클래스 로더 조작기를 지정해야 하는데, 이 부분이 번거롭고 운영하기 어렵다.
3. 런타임 시점(프록시) - 스프링 AOP
런타임 시점이란?
컴파일도 다 끝나고, 클래스 로더에 클래스도 다 올라가서 이미 자바가 실행되고 난 다음을 말한다.
스프링과 같은 컨테이너의 도움을 받고 프록시와 DI, 빈 포스트 프로세서 같은 개념들을 총 동원해야 한다
그러나 특별한 컴파일러나, 자바를 실행할 때 복잡한 옵션과 클래스 로더 조작기를 설정하지 않아도 된다.
스프링만 있으면 얼마든지 AOP를 적용할 수있다!
부가 기능이 정의된 프록시를 동적으로 생성하고 빈 후처리기를 통해 프록시 객체를 빈으로 바꿔 등록함으로써 구현된다.
[핵심!]스프링은 이 방법을 이용해서 AOP를 구현한다.
스프링 AOP
원래 AOP는 지금까지 학습한 메서드 실행 위치 뿐만 아니라 다음과 같은 다양한 위치에 적용할 수 있다.
스프링 AOP는 런타임 시점에 프록시를 사용했기 때문에 메서드실행 시점에만 적용 할 수 있는 것이다.
컴파일 시점이나 클래스 로딩 시점을 이용하면 생성자,필드값 등 다양한 위치에 부가기능(Advice)을 적용할수있다.
그럼 기능에 제한이 있는 스프링 AOP를 사용할게 아니라 어디든 적용가능한 AspectJ 프레임웤을 사용해서 AOP를 구현하면 안될까?
AspectJ를 사용하려면 공부할 내용도 많고, 자바 관련 설정(특별한 컴파일러, AspectJ 전용 문법, 자바 실행 옵션)도 복잡하다.
반면에 스프링 AOP는 프록시를 사용하기 때문에 메서드 실행시점에만 적용가능하지만 별도의 추가 자바 설정 없이 스프링만 있으면 편리하게 AOP 를 사용할 수 있다.
실무에서는 스프링이 제공하는 AOP 기능만 사용해도 대부문의 문제를 해결할 수 있다.
또한, 스프링 AOP는 편의 기능을 제공하기 때문에 @Aspect를 이용해 Advisor만 정의해주면 쉽게 AOP를 적용할 수 있다.
AOP라는건 스프링만의 고유한 개념이 아니다.
스프링에서는 AspectJ가 제공하는 애노테이션이나 관련 인터페이스만 사용하는 것이고, 실제 AspectJ가 제공하는 컴파일, 로드타임 위버 등을 사용하는 것은 아니다.
스프링은 지금까지 우리가 학습한 것 처럼 프록시 방식의 AOP만을 사용한다.
AOP 용어 정리
스프링 AOP를 알아보기 전에 AOP에서 쓰이는 용어들의 개념을 정리해야한다.
조인 포인트(Join point)
어드바이스가 적용될 수 있는 위치
조인 포인트는 추상적인 개념이다. AOP를 적용할 수 있는 모든 지점이라 생각하면 된다.
스프링 AOP는 프록시 방식을 사용하므로 조인 포인트는 항상 메소드 실행 지점으로 제한된다
포인트컷(Pointcut)
조인 포인트 중에서 어드바이스가 적용될 위치를 선별하는 기능
주로 AspectJ 표현식을 사용해서 지정
프록시를 사용하는 스프링 AOP는 메서드 실행 지점만 포인트컷으로 선별 가능
타켓(Target)
어드바이스를 받는 객체, 포인트컷으로 결정
어드바이스(Advice)
적용할 부가 기능
애스펙트(Aspect)
어드바이스 + 포인트컷을 모듈화 한 것
@Aspect 를 생각하면 됨
여러 어드바이스와 포인트 컷이 함께 존재
어드바이저(Advisor)
하나의 어드바이스와 하나의 포인트 컷으로 구성
스프링 AOP에서만 사용되는 특별한 용어
위빙(Weaving)
포인트컷으로 결정한 타켓의 조인 포인트에 어드바이스를 적용하는 행위를 의미
AOP 프록시
AOP 기능을 구현하기 위해 만든 프록시 객체, 스프링에서 AOP 프록시는 JDK 동적 프록시 또는
CGLIB 프록시이다.
이는 프록시 팩토리로 보다 쉽게 만들수있다.
AOP 구현
스프링은 @Aspect를 이용해서 쉽게 AOP를 적용 할 수 있다.
스프링 AOP를 구현하는 일반적인 방법은 @Aspect 를 사용하는 방법이다.
기본버전
@Slf4j
@Component //Aspect를 스프링 빈에 등록해야한다.
@Aspect //@Aspect는 이 객체가 애스펙트라는 표식
public class AspectV1 {
@Around("execution(* hello.aop.order..*(..))")
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[log] {}", joinPoint.getSignature());
return joinPoint.proceed(); //자바 리플랙션 기술
}
}
@Around 애노테이션의 값인 execution(* hello.aop.order..*(..)) 는 포인트컷이 된다.
@Around 애노테이션의 메서드인 doLog 는 어드바이스( Advice )가 된다.
execution(* hello.aop.order..*(..)) 는 AspectJ 포인트컷 표현식이다.
스프링은 프록시 방식의 AOP를 사용하므로 프록시를 통하는 메서드만 적용 대상이 된다.
포인트컷 분리 버전
@Slf4j
@Aspect
public class AspectV2 {
//hello.aop.order 패키지와 하위 패키지
@Pointcut("execution(* hello.aop.order..*(..))") //pointcut expression
private void allOrder(){} //void 빈 메서드로 정의해야한다. //메서드 이름을 통해 포인트컷 유추가 가능하도록 설계한다.
@Around("allOrder()")
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[log] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
}
@Around 에 포인트컷 표현식을 직접 넣을 수 도 있지만, @Pointcut 애노테이션을 사용해서 별도로 분리할 수 도 있다.
@Pointcut("execution(* hello.aop.order..*(..))") //pointcut expression
public void allOrder(){}
포인트컷 메서드를 public으로 설정하면 다른 애스펙트에서도 사용이 가능하다.
여러개의 어드바이스 관리 버전
@Slf4j
@Aspect
public class AspectV3 {
//hello.aop.order 패키지와 하위 패키지
@Pointcut("execution(* hello.aop.order..*(..))")
public void allOrder() {
}
//클래스 이름 패턴이 *Service
@Pointcut("execution(* *..*Service.*(..))")
private void allService() {
}
@Around("allOrder()")
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[log] {}", joinPoint.getSignature());
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());
}
}
}
doLog 와 doTransaction 두개의 Advice를 관리하고있다.
참고로,보통 allOrder와 같은 포인트컷 분리 코드들은 별도의 객체로 구분하고 @Aspect에서는 어드바이스만 관리하도록 설계한다.
public class Pointcuts {
//hello.springaop.app 패키지와 하위 패키지
@Pointcut("execution(* hello.aop.order..*(..))")
public void allOrder(){}
//타입 패턴이 *Service
@Pointcut("execution(* *..*Service.*(..))")
public void allService(){}
//allOrder && allService
@Pointcut("allOrder() && allService()")
public void orderAndService(){}
}
사용하는 방법은 아래와 같이은 패키지명을 포함한 클래스 이름과 포인트컷 시그니처를 모두 지정하면 된다.
....
@Around("hello.aop.order.aop.Pointcuts.allOrder()") //패키지명을 포함한 클래스 이름과 포인트컷 시그니처 모두 지정
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[log] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
....
여러개의 어드바이스 관리 버전을 보면 doLog -> doTransaction 순으로 어드바이스가 적용될텐데, 이 순서를 바꿀수도있다.
어드바이스 순서 바꾸기 버전
@Slf4j
public class AspectV5Order {
@Aspect
@Order(2) //순서지정
public static class LogAspect { //별도의 @Aspect 클래스로 만듬
@Around("hello.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 { //별도의 @Aspect 클래스로 만듬
@Around("hello.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 애노테이션을 적용하여 순서를 지정 할 수 있다.
단,@Order 애노테이션은 어드바이스 단위가 아니라 클래스 단위로 적용할 수 있다
따라서 애스펙트를 별도의 클래스로 분리해야 한다.
위처럼 분리하면 @Order 애노테이션을 적용하여 순서를 지정 할 수 있다.
이제 지정한 것 처럼 doTransaction -> doLog 순으로 어드바이스가 적용된다.
어드바이스 종류
@Around : 메서드 호출 전후에 수행, 가장 강력한 어드바이스, 조인 포인트 실행 여부 선택, 반환 값 변환, 예외 변환 등이 가능
@Before : 조인 포인트 실행 이전에 실행
@AfterReturning : 조인 포인트가 정상 완료후 실행
@AfterThrowing : 메서드가 예외를 던지는 경우 실행
@After : 조인 포인트가 정상 또는 예외에 관계없이 실행(finally)
@Around 가 붙은 메서드가 어드바이스라고 했는데, 다양한 에너테이션으로 @Around를 쪼개서 어드바이스를 적용할수있다.
why?
@Around 가 가장 넓은 기능을 제공하는 것은 맞지만, 전체 어드바이스 동작을 한번에 정의해 두기 때문에 실수할 가능성이 있다.
나머지 에터네이션들을 사용하면 역할을 소분해서 정의하기때문에 실수를 줄이고 가독성 또한 올릴 수 있다.
@Around 어드바이스만 사용해도 필요한 기능을 모두 수행할 수 있다. 알아두고 필요한 시점이 오면 공부하자
포인트컷 표현식(AspectJ 표현식)
AspectJ 표현식
프로젝트 규모가 커지고 AOP를 적용하다보면 원치 않는 곳까지 매칭되어 AOP가 동작하는 경우가 발생한다.
따라서 포인트컷을 디테일하고 편리하게 정의해서 지정해줄 필요가있다.
AspectJ는 포인트컷을 ''디테일''하고 ''편리''하게 표현하기 위한 특별한 표현식을 제공한다.
포인트컷 표현식은 execution 같은 포인트컷 지시자(Pointcut Designator)로 시작한다.
줄여서 PCD라 한다.
execution : 메소드 실행 조인 포인트를 매칭한다.
사실, within , args , this , target .. 등등 다양한 포인트컷 지시자가 존재한다.
within : 특정 타입 내의 조인 포인트를 매칭한다.
args : 인자가 주어진 타입의 인스턴스인 조인 포인트
this : 스프링 빈 객체(스프링 AOP 프록시)를 대상으로 하는 조인 포인트
target : Target 객체(스프링 AOP 프록시가 가르키는 실제 대상)를 대상으로 하는 조인 포인트
@target : 실행 객체의 클래스에 주어진 타입의 애노테이션이 있는 조인 포인트
@within : 주어진 애노테이션이 있는 타입 내 조인 포인트
@annotation : 메서드가 주어진 애노테이션을 가지고 있는 조인 포인트를 매칭
@args : 전달된 실제 인수의 런타임 타입이 주어진 타입의 애노테이션을 갖는 조인 포인트
bean : 스프링 전용 포인트컷 지시자, 빈의 이름으로 포인트컷을 지정한다.
execution 을 가장 많이 사용하고, 나머지는 자주 사용하지 않는다. 필요할때 공부해서 사용하자.
스프링 AOP는 프록시를 사용하기 때문에 대부분 메서드 실행 시점에만 조인포인트를 걸어 해결 할 수 있기때문
pointcut.setExpression("execution(* hello.aop.member.MemberServiceImpl.hello(..))");
pointcut.setExpression("execution(* hello.aop.member.*.*(..))");
pointcut.setExpression("execution(* hello.aop.*.*(..))"); // . 은 정확히 그 패키지만 포함
pointcut.setExpression("execution(* hello.aop.member..*.*(..))"); // .. 은 하위 패키지도 포함한다는 뜻
파라미터 매칭
pointcut.setExpression("execution(* *(String))"); // 정확히 하나의 String 파라미터만 허용
pointcut.setExpression("execution(* *(*))"); //정확히 하나의 파라미터 허용, 모든 타입 허용
pointcut.setExpression("execution(* *(..))"); //숫자와 무관하게 모든 파라미터, 모든 타입 허용
pointcut.setExpression("execution(* *(String, ..))"); //String 타입으로 시작, 숫자와 무관하게 모든 파라미터, 모든 타입 허용