이 글을 읽으시는 분들은 아마 Spring을 써보셨거나, 혹은 관심이 있으신 분들일겁니다. 전자의 경우에는 모두 AOP를 경험해보셨을텐데 (사실 AOP를 안써봤다고 하면 그거 공부 헛한거임 ㅋㅋ) 여러분들은 이건 아마 모르실겁니다.
예제를 하나 보여드리겠습니다.
🔨 FooService.kt
class FooService(private val fooRepository: FooRepository) {
fun readOne(fooId: String): Foo {
bar()
return fooRepository.findById(fooId)
}
@PerformanceCheck
private fun bar() {
println("나 불렀니? ㅋㅋ")
}
}
🔨 FooAdvisor.kt
@Component
@Aspect
class FooAdvisor {
@Around("@annotation(PerformanceCheck)")
fun stopWatch(joinPoint: ProceedingJoinPoint): Any {
val stopWatch = StopWatch()
try {
stopWatch.start()
return joinPoint.proceed()
} finally {
stopWatch.stop()
log.info("request spent ${stopWatch.getLastTaskTimeMills}")
}
}
}
해당 예제에서 FooService의 readOne 메소드를 호출하게 되면 Spring AOP에 의해서 bar() 메소드의 성능이 체크되어야 하지만, 실제 호출을 하게되면 stopWatch에 대한 log가 찍혀나오지 않는 현상이 일어납니다.
그 이유를 알아보면 굉장히 깊고 심오한데요, 이제부터 하나하나 설명해드리겠습니다.
우선 위의 현상을 이해하기 위해서는 Spring AOP에 대해서 더욱 심도있게 알아볼 필요가 있습니다.
우선 프록시에 대해서 이해를 해볼 필요가 있습니다. 프록시에 대해서 간략하게 설명을 하자면, A라는 클래스를 타겟 오브젝트가 의존하는 상황에서 A 대신에 A'라는 클래스가 대신 의존하도록 만들면, 이러한 A' 클래시를 프록시 클래스 라고 부릅니다.
이러한 프록시 클래스는 프록시 패턴, 혹은 데코레이터 패턴을 이용해서 생성해낼 수 있는데요, 위의 그림처럼
1. Original, Proxy 모두 AbstractOriginal 이라는 인터페이스를 realize한다
2. Proxy는 Original을 생성자 인자로 받음으로써 Original에 의존하는 관계를 맺는다
3. Client는 Original이 아닌 Proxy와 관계를 맺도록 한다
이렇게 해주면 프록시를 이용할 수 있습니다. 프록시 자체는 여기까지만 이해해도 무방합니다.
그런데 생각해봅시다. 저희는 Spring을 쓰면서 Proxy라는 것을 만든 적이 없습니다. 어떻게 된 일일까요?
여기서 Spring의 마법이 벌어집니다. Spring은 Java Reflection을 이용해서 Dynamic Proxy를 만들어내는 능력을 가지고 있습니다. 그리고 Spring은 내부적으로 프록시 팩토리 빈을 컨텍스트에 등록시켜서 AOP 대상이 되는 빈에다가 부가기능을 더하는 방식입니다.
👉 흔히 Spring에서의 AOP는 메소드만이 대상이라는 말을 들어보셨을겁니다. 그 이유가 여기에 있는건데, Spring AOP는 Java Reflection을 이용해서 내부적으로 구현이 되어있기 때문에 메소드만을 대상으로 할 수밖에 없습니다. 그 이상을 원한다면 AspectJ를 이용해서 Spring AOP가 아닌 Java AOP를 구현하셔야합니다.
바로 여기에 PerformanceCheck가 동작하지 않은 비밀이 숨겨져있습니다.
정답부터 말씀드리겠습니다. readOne 메소드는 프록시로 감싸지지 않았고, readOne에서 호출하는 bar 메소드는 프록시에 감싸지지 않은 메소드이기 때문입니다.
이걸 듣고 바로 이해가 되시지 않을겁니다. 천천히 생각해봅시다.
readOne(fooId: String) 메소드는 어떠한 포인트컷의 대상도 아닙니다. 따라서 해당 메소드는 AOP에 의해서 다이내믹 프록시 대상이 아닙니다. 그런데 이런 메소드가 bar 라는 메소드를 내부적으로 호출을 하고 있는데, readOne은 프록시로 감싸진게 아니기 때문에 역으로 거슬러 올라가서 Proxy 내부의 bar 메소드를 호출할리가 절대 없습니다. 따라서 프록시로 감싸지지 않은 bar 라는 메소드를 호출하기 때문에 이런 문제가 발생한겁니다.
저는 이게 매우 중요하다고 생각합니다. 왜냐하면 아래의 사실 때문입니다.
👉 @Transactional 어노테이션은 Spring AOP를 통해 구현되는 선언적 트랜잭션 어노테이션이다.
따라서 Transactional도 Spring AOP이기 때문에 위의 사항을 고려할 필요가 있을 것 같습니다.