[Spring][AOP] 스프링 AOP

donghyeok·2023년 3월 12일
0

Spring

목록 보기
7/9

기본 사용법

@Aspect

@Around("execution(* hello.aop.order..*(..))")
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
	//추가 로직 수행 
    return joinPoint.proceed();
}
  • @Around어노테이션의 값 execution..은 포인트컷이 된다.
  • @Around어노테이션의 메서드인 doLog는 어드바이스가 된다.

참고로 스프링 AOP는 AspectJ의 문법을 차용하고 프록시 방식의 AOP 방식을 제공한다.
AspectJ를 직접 사용하는 것이 아니다.

포인트컷 분리

@Pointcut("execution(* hello.aop.order..*(..))")
private void allOrder() {} //pointcut signature

@Around("allOrder()")
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
	//내용 동일
}

@Pointcut

  • @Pointcut에 포인트컷 표현식을 사용한다.
  • 메서드와 파라미터를 합쳐서 포인트컷 시그니처라고 한다.
  • 반환 타입은 void이며, 내용은 비워둔다.
  • 어드바이스쪽 코드에서 @Around("{함수명()}") 형태로 사용할 수 있으며 &&, ||, ! 3가지 조합으로 여러 포인트컷을 조합할 수도 있다.

어드바이스 순서 지정

위 방식으로 하나의 에스펙트에 여러 어드바이스를 지정하면 순서를 보장 받을 수 없다.
따라서 애스펙트를 별도의 클래스로 분리해야한다.

@Slf4j
public class AspectOrder {
	
    @Aspect
    @Order(2)
    public static class LogAspect {
    	@Around("allOrder()")
        public Object doLog(ProceedingJoinPoint JoinPoint) throws Throwable {
        	//로직
        }
    }
    
    @Aspect
    @Order(1)
    public static class TxAspect {
    	@Around("orderAndService()')
        public Object doTran(ProceedingJoinPoint JoinPoint) throws Throwable {
        	//로직 
        }	
    }	
}

위와 같이 @Aspect를 클래스 내부에 위치시킨 후 @Order()어노테이션으로 적용시키면 동일 에스펙트에 적용되는 어드바이스의 순서를 지정할 수 있다.

어드바이스 종류

지금까지는 @Around만 사용했지만 다음과 같은 어드바이스들이 존재한다.
복잡해 보이지만 사실 @Around 어드바이스를 제외한 나머지 어드바이스들은 @Around가 할 수 있는 일의 일부만 제공할 뿐이다.

  • @Around : 메서드 호출 전후에 수행, 가장 강력한 어드바이스
  • @Before : 조인 포인트 실행 이전에 실행
  • @AfterReturning : 조인 포인트가 정상 완료후 실행
  • @AfterThrowing : 메서드가 예외를 던지는 경우 실행
  • @After : 조인 포인트가 정상 또는 예외에 관계없이 실행(finally)

왜 @Around 외의 다른 어드바이스가 존재할까?

좋은 설계는 제약이 있는 것이다.

어드바이스 구성을 보면 @AroundProceedingJoinPoint클래스를 인수로 받고 나머지는 JoinPoint클래스를 받는데 JoinPointproceed() 메서드가 존재하지 않는다.
이는 일종의 가이드 역할을 한다. 개발자가 @Around를 호출했는데 proceed()를 호출하지 않으면 큰 장애가 발생할 수 있다. 이는 실수를 미연에 방지하게 해준다.
또한 이런 제약 덕분에 코드의 의도가 명확해진다. 메서드의 이름으로 로직 수행 시점을 파악할 수 있다.

포인트컷 사용법

애스펙트J는 포인트컷을 편리하게 사용하기 위한 특별한 표현식을 제공한다.
@Pointcut("execution(* hello.aop.order..*(..))")

포인트컷 지시자

포인트컷 표현식은 execution같은 포인트컷 지시자로 시작한다. 줄여서 PCD라고 한다.

종류는 아래와 같다.

  • execution : 메소드 실행 조인 포인트를 매칭한다. (가장 많이 사용)
  • within : 특정 타입 내의 조인 포인트를 매칭한다.
  • args : 인자가 주어진 타입의 인스턴스인 조인 포인트
  • this : 스프링 빈 개체(AOP 프록시)를 대상으로 하는 조인 포인트
  • target : Target 객체(프록시가 가르키는 실제 대상)을 대상으로 하는 조인 포인트
  • @Target : 실행 객체의 클래스에 주어진 타입의 어노테이션이 있는 조인 포인트
  • @within : 주어진 어노테이션이 있는 타입 내 조인 포인트
  • @annotation : 메서드가 주어진 어노테이션을 가지고 있는 조인 포인트를 매칭
  • @args : 전달된 실제 인수의 런타임 타입이 주어진 타입의 어노테이션을 갖는 조인 포인트
  • bean : 스프링 전용 포인트컷 지시자, 빈의 이름으로 포인트컷을 지정

execution 문법

  • execution(접근제어자? 반환타입 선언타입?메서드이름(파라미터) 예외?)
  • execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?)
    ex) execution(* hello.aop.*.*(..))

execution 문법은 위 구문으로 표현가능하다. ?는 생략 가능하며, * 같은 패턴을 지정할 수 있다.
패턴 매칭 관련 특수 문법은 아래와 같다.

  • 선언타입에 부모타입 클래스를 지정할 수 있다.
  • *는 아무 내용이나 들어올 수 있다는 뜻이다.
  • 패키지경로에서 .은 정확하게 해당 위치 패키지, ..은 해당 위치 및 하위 패키지 포함한다는 뜻이다.
  • 파라미터 매칭에서 ()은 파라미터 없음, (*)은 정확히 하나의 파라미터, (..)은 타입에 무관하게 여러 파라미터 허용, (String, ..)은 첫번째 인수는 String 타입으로 시작하고 나머지는 타입에 무관하게 여러 파라미터 허용

within 문법

ex) within(hello.aop.member.*Service*)

within은 특정 타입 내의 조인포인트로 매칭을 제한한다.
주의점은, execution과 다르게 표현식에 부모 타입을 지정할 수 없다는 점이다.

args 문법

ex) args(String,..)

args는 인자가 주어진 타입의 인스턴스인 조인 포인트로 매칭한다.
주의점은, execution은 정적으로 메서드의 시그니처를 보고 판단하지만 args는 동적으로 런타임에 전달된 인수로 판단하기 때문에 상위 타입 지정이 가능하다.
args 지시자는 단독으로 사용되기 보다는 파라미터 바인딩에서 주로 사용한다.

@target, @within

  • @Target : 실행 객체의 클래스에 주어진 타입의 어노테이션이 있는 조인 포인트
  • @within : 주어진 어노테이션이 있는 타입 내 조인 포인트

둘은 차이가 있는데, 쉽게 이야기해서 @target은 부모 클래스의(주어진 타입과 같음) 메서드까지 어드바이스를 모두 적용하고, @within은 자기 자신의 클래스에 정의된 메서드에만 어드바이스를 적용한다.

주의점은, 다음 포인트컷 지시자는 단독으로 사용하면 안된다는 점이다. args, @args, @target
해당 지시자들은 실제 객체 인스턴스가 생성되고 실행될 때 어드바이스 적용 여부를 확인할 수 있다.(런타임)
실행 시점에 일어나는 포인트컷 적용 여부도 결국 프록시가 있어야 판단할 수 있다.
그런데 스프링 컨테이너가 프록시를 생성하는 시점은 컨테이너가 만들어지는 애플리케이션 로딩 시점에 적용할 수 있다.
따라서 args, @args, @target지시자가 있으면 스프링은 모든 빈에 AOP를 적용하려고 시도한다.
문제는 이때 스프링 내부의 빈들 중 final로 지정된 빈들도 있기 때문에 오류가 발생한다.
따라서 이런 표현식들은 다음과 같이 프록시 적용 대상을 축소하는 표현식과 함께 사용해야 한다.

@Around("execution( hello.aop..(..)) && @within(hello.aop.member.annotation.ClassAop)")

@annotation, @args, bean

  • @annotation : 메서드에 주어진 어노테이션을 가지고 있는 조인 포인트를 매칭
  • @args : 전달된 실제 인수의 런타임 타입이 주어진 타입의 어노테이션을 갖는 조인 포인트
  • bean : 스프링 전용 포인트컷 지시자, 빈의 이름으로 지정한다.
    - bean(orderService) || bean(*Repository) 처럼 연산자 사용가능
    - * 같은 패턴을 사용할 수 있다.

매개변수 전달

다음 지시자들은 포인트컷 표현식을 사용해서 어드바이스에 매개변수를 전달할 수 있다.

this, target, args, @target, @within, @annotation, @args

각 예시는 아래와 같다.

//지시자 사용X 
@Around("allMember()")
public Object test(ProceedingJoinPoint joinPoint) {
	log.info("arg1={}", joinPoint.getArgs()[0]);
    return joinPoint.proceed();
}

//args 사용한 파라미터 전달 
@Around("allMember() && args(arg,..)")
public void test(String arg) {
	log.info("arg={}", arg);	//첫번째 파라미터 출력 
}

//this를 사용한 프록시 객체 전달 
@Around("allMember() && this(obj)")
public void test(MemberService obj) {
	log.info("obj={}", obj);    //AOP 프록시 객체 정보 출력 
}	

//target을 이용한 실제 target 객체 전달 
@Around("allMember() && target(obj)")
public void test(MemberService obj) {
	log.info("obj={}", obj);	//실제 객체 정보 출력
}

//@target을 이용한 클래스 어노테이션 정보 출력 
@Around("allMember() && @target(annotation)")
public void test(ClassAop annotation) {
	log.info("annotation={}", annotation); //클래스 어노테이션 정보 출력
}

//@within을 이용한 클래스 어노테이션 정보 출력 
@Around("allMember() && @within(annotation)")
public void test(ClassAop annotation) {
	log.info("annotation={}", annotation); //클래스 어노테이션 정보 출력
}

//@annotation을 이용한 메서드 어노테이션 정보 출력
@Around("allMember() && @annotation(annotation)")
public void test(MethodAop annotataion) {
	log.info("annotation={}", annotation); //메서드 어노테이션 정보 출력 
}

this, target

  • this : 스프링 빈 객체(AOP 프록시)를 대상으로 하는 조인포인트
  • target : Target 객체(AOP 프록시의 실제 대상)을 대상으로 하는 조인포인트

this, target은 다음과 같이 적용 타입 하나를 정확하게 지정해야 한다.
부모 타입을 허용하며 * 같은 패턴을 사용할 수 없다.

this(hello.aop.member.MemberService)
target(hello.aop.member.MemberService)

this vs target

this, target 특정 상황에서 차이가 있다.

프록시 생성 방식에는 위와 같이 두가지 방식이 있다.

  1. JDK 동적 프록시 : 인터페이스를 구현한 프록시 객체를 생성
  2. CGLIB 프록시 : 인터페이스가 있어도 구체 클래스를 상속 받아서 프록시 객체 생성

이 때, this, target의 대상을 인터페이스, 구체 클래스로 하는 경우 다음과 같은 차이가 발생한다.

  1. JDK 동적 프록시 방식
    - 대상을 인터페이스로 지정할 경우 : this, target 모두 AOP 적용 대상으로 인식 (JDK 동적 프록시 방식은 인터페이스를 구현하므로)
    - 대상을 구체클래스로 지정할 경우 : target만 AOP 적용 대상으로 인식 (JDK 동적 프록시에서 인터페이스 정보만 있고 구체클래스 정보는 없기 때문)
  2. CGLIB 프록시 방식
    - 대상을 인터페이스로 지정할 경우 : this, target 모두 AOP 적용 대상으로 인식 (부모 타입을 허용하므로)
    - 대상을 구체클래스로 지정할 경우 : this, target 모두 AOP 적용 대상으로 인식 (CGLIB 방식은 구체 클래스를 상속 받으므로)

0개의 댓글