본문 바로가기
스프링부트

스프링 AOP - 톺아보기

by pius712 2024. 6. 6.

aop 는 횡단 관심사를 분리하여 모듈화 한 것을 말한다.

횡단 관심사란 비즈니스 로직이 아닌 그 외의 로깅, 트랜잭션과 같은 것들을 말한다.

스프링을 사용하면 자주 만나게되는 @Transactional 이나 @Cacheable 과 같은 어노테이션들이 이런 스프링 AOP 를 통해 구현된 것이라고 볼 수 있다.

이 스프링 AOP의 기반이 되는 proxy 와 bean post processor 에 대해 알아보고, 스프링 AOP 에 대해 알아보고자 한다.

Spring-Aop 의 기반

Proxy

프록시는 디자인 패턴의 일종이다.

아래와 같이 client 가 target 객체를 호출할 때, target 을 객체로 감싸서 중간에서 다른 작업을 하게 된다.

간단하게 코드를 살펴보면 아래와 같다.

// 인터페이스 기반
class FooProxy(private val fooImpl:FooImpl) : Foo {
		
	fun foo():Bar {
		// 로직
		val result = fooImpl.foo()
		// 로직
		return result;
	}
}

// 상속 기반
class FooProxy : Foo() {
		
	override fun foo():Bar {
		// 로직
		val result = super.foo()
		// 로직
		return result;
	}
}

위의 코드에서 살펴본 것처럼, 프록시는 인터페이스를 기반으로 composition 을 할 수도 있고, 상속을 통해 구현할 수도 있다.

자바에서 프록시를 만들어 주는 라이브러리가 있는데, 크게 아래의 2가지가 있다.

  • JDK 동적 프록시: 인터페이스를 기반으로 proxy 를 구현하는 방법을 제공
  • CGLib : 상속을 기반으로 proxy 구현하는 방법 제공

해당 라이브러리 사용법에 대해서는 생략하겠다.

ProxyFactory

동적 proxy 를 생성할 때는 JDK 동적 프록시와 CGLib 가 사용된다.

JDK 동적 프록시: 인터페이스를 구현한 객체의 프록시 생성

CGLib: 구체 클래스 기반 객체의 프록시 생성

이 두가지 방식을 추상화한 클래스가 ProxyFactory

하나의 Proxy 에는 여러 Advisor 등록이 가능하다.

즉, 하나의 target에 여러 advice가 적용되어도, Proxy 는 하나만 생성된다.

BeanPostProcessor

Bean 을 registry 에 등록하기 전, Bean 을 조작하는 후킹 포인트.

컴포넌트 스캔을 사용하는 경우, Proxy 등록을 수동으로 할 수 없지만, BeanPostProcessor 를 통해서 Proxy 객체를 생성할 수 있다.

스프링에서는 AnnotationAwareAspectJAutoProxyCreator 를 통해서 advisor 를 인식하여 Proxy 객체를 만든다.

AutoProxyCreator 는 아래와 같은 절차를 따른다.

  1. 빈으로 등록된 advisor 를 찾는다.
  2. Aspect 어노테이션 클래스를 찾아서 advisor 를 찾는다.
  3. 이 advisor 를 기반으로 proxy 를 만든다.
    1. pointcut 을 통해서 advisor 적용 여부를 판단한다.
    2. proxy 는 ProxyFactory 를 통해 생성한다.

@Aspect

어노테이션 기반으로 proxy를 생성할 수 있게 해준다.

내부적으로, 해당 어노테이션이 붙은 빈들을 조회해서 Advisor 를 만든다.

그리고, AutoProxyCreator 가 Proxy 를 생성할 때, 등록된 Advisor와 Aspect 어노테이션으로 생성되는 Advisor 를 조회해서 적용한다.

@Aspect
class AspectCls {
	@Around(포인트컷)
	fun advice() {
		로직
	}
}

Spring-AOP

동작 방식

spring aop 는 런타임 시점에 proxy 객체를 생성하여, Bean registry에 등록하는 방식으로 동작한다.

위에서 알아본 내용처럼, Spring application 이 실행되면서 Bean이 등록될 때 BeanPostProcessor(AutoProxyCreator) 가 ProxyFactory 를 통해 프록시를 만들어서 등록하는 것이다.

aop 적용 방법

  • 포인트 컷과 advice 합쳐진 형태
    • Around 내의 표현식을 통해 pointcut 적용
    • 함수 내의 로직이 advice
  • 포인트 컷이 분리된 형태
    • 포인트 컷 어노테이션을 통해 분리하여 적용
@Aspect
class FooClass {

	// 포인트 컷과 advice 가 합쳐진 형태
	@Around("표현식")
	fun a(joinPoint: ProceedingJoinPoint) { }

	// 포인트 컷 분리
	@PointCut()
	fun whenSave(){}
	
	@Around("whenSave()")
	fun doSave(joinPoint: ProceedingJoinPoint) { }
}

advice의 종류

  • @Around: 메서드 실행 전후를 제어할 수 있다.
    • 인자로 ProceedingJoinPoint 를 사용해야함.
    • ProceedingJoinPoint 만이 proceed() 메서드 호출을 할 수 있음.

아래의 advice 는 JoinPoint 를 인자로 가질 수 있으나, proceed 메서드는 없고

메서드 시그니처, 인자, 타겟 객체 반환 등 만 할 수 있다.

  • @Before: 메서드 실행 전을 제어할 수 있음.
  • @After : 메서드 실행 후에 실행되며, 예외 발생 여부와 관계 없이 실행됨
  • @AfterReturning : 메서드 실행 후. 정상처리 후에만 실행
  • @AfterThrowing : 메서드 실행 후 예외 발생시 실행
@Aspect
class FooClass {

	@Around()
	fun a(joinPoint: ProceedingJoinPoint) { }
	
	@Before()
	fun b(joinPoint: JoinPoint) { }
	
	@After()
	fun c(joinPoint: JoinPoint) { }
	
	@AfterReturing()
	fun d(joinPoint: JoinPoint, result:Object) { }
	
	@AfterThrowing()
	fun e(joinPoint: JoinPoint, ex:Exception) { }
}

advice 순서

advice 의 순서는 @Aspect 클래스 단위로 지정할 수 있다. 하나의 Aspect 내에서는 순서를 보장할 수 없다.

@Order(숫자) 의 낮은 숫자가 먼저 적용된다.

@Aspect
@Order(1)
class FooAspect {

}

@Aspect
@Order(2)
Class BarAspect {

}

그리고 Advice 종류에 따른 순서도 존재한다. 아래 순서로 호출되며,

Around → Before → After → AfterReturning → AfterThrowing

메서드 호출 후에는 아래와 같은 형태로 진행된다.

Around ← Before ← After ← AfterReturning ← AfterThrowing

PointCut

포인트 컷의 경우, 다룰 내용이 조금 많아서 별도의 포스팅에서 다뤄보려고 한다.

주의사항

  • self-invocation

객체 내부에서 자신의 메서드를 호출하는 경우, 프록시를 거치지 않는다.

class SomeCls {
	@Transactional
	fun foo() { }
	
	fun bar() { foo() } // 적용안된다. 
}
  • 기본적으로 cglib 가 사용된다.

JDK 동적 프록시를 사용하면, 구체 클래스 타입으로 주입이 안된다.

하지만 cglib 는 상속을 기반으로 동작해서 주의해야한다.

  1. final 클래스, 메서드에 적용 불가능

상속과 오버라이드를 기반으로 동작하기 때문에, 해당 케이스에서 사용이 불가능하다.

코틀린에서 문제가 되는데, 플러그인을 통해서 해결이 가능하다.

과거에는 기본 생성자 필수, 생성자 두번 호출 등의 문제가 있었으나 현재는 이러한 문제들은 해결되었음.