스프링 AOP 개념정리

ParkIsComing·2024년 1월 8일

Spring

목록 보기
18/21


AOP 키워드


관점(Aspect)

  • 여러 클래스에 걸쳐 적용되는 관심사(횡단 관심사)의 모듈화

어드바이스

  • 특정 조인 포인트에서 관점이 취하는 조치 (부가 기능)
  • "around"/"before"/"after" 어드바이스를 포함

포인트컷

  • 조인포인트 중 어드바이스가 적용될 위치를 선별하는 기능
  • 주로 AspectJ 표현식을 사용해서 지정
  • 프록시를 사용하는 스프링 AOP는 메서드 실행 지점만 포인트컷을 선별 가능

어드바이저

  • 하나의 어드바이스와 하나의 포인트컷으로 구성
  • 스프링 AOP에서만 사용되는 개념

조인포인트

  • 어드바이스(즉 AOP)가 적용될 수 있는 위치
  • 프로그램이 실행될 때 지나는 지점은 조인포인트가 될 수 있다.
    • 메서드 실행시
    • 생성자 호출시
    • 필드값 접근시
    • static 메서드 접근시
  • 다만 스프링 AOP는 프록시 방식을 사용하기 때문에 조인 포인트는 메서드 실행 지점으로 제한되니 유의

타겟

  • 하나 이상의 관점에 어드바이스를 받는 객체
  • 포인트컷으로 결정함
  • 스프링 AOP런타임 프록시를 사용하여 구현되므로 이 객체는 항상 프록시 객체

위빙

  • 포인트컷으로 결정한 타겟의 조인포인트에 어드바이스를 적용하는 것
  • AOP 적용을 위해 관점(aspect)를 객체에 연결한 상태

Spring AOP


1. 포인트컷

  • @Point() <- 안에 포인트컷 표현식을 사용한다.
  • 메서드 이름과 파라미터를 합쳐서 포인트컷 시그니처라고 한다.
  • 메서드 반환 타입은 void, 바디는 비워둠
  • 포인트컷을 따로 분리해 설정함으로써 메서드명에 의미를 담을 수 있다.
  • 접근 제어자를 public으로 하면 다른 Aspect에서도 참조 가능하다.

다음과 같은 Pointcut이 있다고 하자.
allOrder()는 패키지 위치를 기준으로 포인트컷 시그니처를 정의했다.
allService()는 클래스 이름 패턴을 기준으로 포인트컷 시그니처를 정의했다.

@Aspect
@Slf4j
public class AspectV3 {
	@Pointcut("execution(* hello.aop.order..*(..))") // order 패키지에 포함된 모든 클래스의 메서드는 aop 적용 대상
	public void allOrder() {

	}

	//클래스 이름 패턴이 *Service
	@Pointcut("execution(* *..*Service.*(..))")
	private void allService() {

	}
}

사용할 때는 아래처럼 어드바이스에 포인트컷 시그니처를 사용하면 된다.

@Aspect
public class AspectV3 {
	@Pointcut("execution(* hello.aop.order..*(..))") // order 패키지에 포함된 모든 클래스의 메서드는 aop 적용 대상
	public void allOrder() {

	}

	//클래스 이름 패턴이 *Service
	@Pointcut("execution(* *..*Service.*(..))")
	private void allService() {

	}

	//hello.aop.order 패키지와 하위패키지 & 클래스 이름 패턴이 *Service
	@Around("allOrder() && allService()")
	public Object doTransaction(ProceedingJoinPoint joinPoint) throws  Throwable {
		// 어드바이스를 통해 처리할 로직 작성
	}
}

포인트컷 지시자(Pointcut designator)

  • execution
  • within
  • args
  • this
  • target
  • @target
  • @within
  • @annotation
  • @args
  • bean



2. 어드바이스

어드바이스에 대해서도 자세히 알아보자.

어드바이스 종류

  • @Around : 메서드 호출 전후에 수행 <- 가장 강력하다!
    • ✅ 조인포인트 실행 여부 선택
      • 실행시키기 위해 proceed()를 사용해야 함 (여러 번 가능)
      • 따라서 proceed()를 제공하는 ProceedingJoinPoint를 사용해야 함
    • ✅ 반환 값 변환
    • ✅ 예외 변환 가능
  • @Before : 조인 포인트 실행 이전에 실행 (proceed() 호출 이전)
  • @AfterReturning : 조인 포인트가 정상 완료되면 실행
    • 속성 : value, returning
    • returning절에 지정된 타입값을 반환하는 메서드에 대해서만 실행되니 주의
    • returning 속성에 사용된 이름은 어드바이스 메서드의 매개변수 이름과 같아야 함
  • @AfterThrowing : 메서드가 예외를 던지는 경우 실행
    • 속성 : value, throwing
    • throwing 속성에 사용된 이름은 어드바이스 메서드의 매개변수 이름과 같아야 함
  • @After : 조인 포인트가 정상 또는 예외에 관계없이 실행(try-catch의 finally 같은 역할)
    • 주로 사용이 끝난 리소스를 릴리즈하기 위해 사용함

종류별 실행 순서

@Around -> @Before -> @After -> @AfterReturning -> @AfterThrowing

결국 조인포인트 전후로 언제 실행하는지에 따라 나뉜다.


아래 코드에 주석을 달아놓은 위치가 각 애노테이션이 동작하는 시점이라고 이해하면 된다.
메서드 실행 전후에 작업을 수행할 수 있는 @Around를 기준으로 봤을 때, 다른 어드바이스들은 어느 시점을 정의하는지 나타낸다.

@Around("allOrder() && allService()")
	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());
		}
	}

종류별로 예를 들어보자면 다음과 같이 어드바이스를 작성할 수 있다.
@Before("hello.aop.order.aop.Pointcuts.orderAndService()")
public void doBefore(JoinPoint joinPoint) {
	log.info("[before] {}", joinPoint.getSignature());
}

@AfterReturning(value = "hello.aop.order.aop.Pointcuts.orderAndService()", returning = "result")
public void doReturn(JoinPoint joinPoint, Object result) {
	log.info("[return] {} return = {}", joinPoint.getSignature(), result);
}

@AfterThrowing(value = "hello.aop.order.aop.Pointcuts.orderAndService()", throwing = "ex")
public void doThrowing(JoinPoint joinPoint, Exception ex) {
	log.info("[ex] {} message = {}", joinPoint.getSignature(), ex.getMessage());
}

@After(value = "hello.aop.order.aop.Pointcuts.orderAndService()")
public void doAfter(JoinPoint joinPoint) {
	log.info("[after] {}", joinPoint.getSignature());
}

[참고] JoinPoint와 ProceedingJoinPoint

  • JoinPoint : 메소드 매개변수, 반환 값 또는 발생한 예외와 같이 특정 조인 포인트에서 사용 가능한 상태에 대한 reflective access를 제공하는 AspectJ 인터페이스
  • ProceedingJoinPoint: JoinPoint의 extension으로 proceed() 메서드를 제공하는 것이 차이점.(따라서 @Around에서만 사용)이를 호출하면 코드 실행이 다음 어드바이스 또는 대상 메서드로 이동한다.


어드바이스 순서

스프링은 어드바이스의 순서를 보장하지 않는다.
따라서 @Aspect(@Around 아님) 단위로 순서를 지정하는 @Order 애노테이션을 적용해야 한다.
-> 어드바이스 단위가 아니라 클래스 단위로 적용해야 한다.
-> 하나의 @Aspect에 여러 개의 어드바이스를 정의하면 안 되고, 클래스 단위로 분리해야 한다.

아래 코드를 보면 클래스마다 하나의 어드바이스가 정의되어 있고, 클래스에 @Order를 적용하여 어드바이스의 순서를 설정한 것이다.

public class AspectV5Order {

	@Aspect
	@Order(1)
	public static class LogAspect {
		@Around("hello.aop.order.aop.Pointcuts.allOrder()")
		public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
			log.info("[log] {}", joinPoint.getSignature()); // joinPoint 시그니처
			return joinPoint.proceed();
		}
	}

	@Aspect
	@Order(2)
	public static class TraxAspect {
		//hello.aop.order 패키지와 하위패키지 & 클래스 이름 패턴이 *Service
		@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());
			}
		}
	}
}

0개의 댓글