본문 바로가기
스프링부트

AOP 활용 (feat. jpa 호출 로깅)

by pius712 2024. 6. 6.

AOP 에 대한 기본적인 개념을 이전 포스팅에서 알아보았다.

https://pius712.tistory.com/33

 

스프링 AOP - 톺아보기

aop 는 횡단 관심사를 분리하여 모듈화 한 것을 말한다.횡단 관심사란 비즈니스 로직이 아닌 그 외의 로깅, 트랜잭션과 같은 것들을 말한다.스프링을 사용하면 자주 만나게되는 @Transactional 이나 @

pius712.tistory.com

 

이 AOP 를 활용하여, JPA repository 메서드 호출에 대한 로깅을 간단하게 구현하고자 한다.

요구사항

  1. jpa repository 모든 메서드에 대해 로그를 남겨야함
  2. 어떤 메서드가 호출했는지, 어떤 메서드가 호출되었는지 로그를 남겨야함
  3. jpa repository 의 time elapsed 로그를 남겨야함
  4. 예외 발생시 message 를 남길 수 있어야함.

구현

1. jpa repository 모든 메서드에 대해 로그

우선 모든 jpa repository 에 대한 로그 남겨야하는데, 이를 위해서는 jpa repository 의 구조를 살펴볼 필요가 있다. 일반적으로는 JpaRepository 를 구현하기 때문에 이를 포인트 컷으로 잡아도될거 같은데, 아래 블로그와 같은 케이스도 있으니, 최상위 인터페이스를 포인트컷으로 잡으려고 한다.

https://ohzzi.io/jpa-repository-no-repository-yes

 

JpaRepository? No! Repository? Yes!

JPA를 사용하는 이상 Spring Data JPA를 사용하지 않는 사람은 거의 없을 것이고, Spring Data JPA를 사용하는데 JpaRepository 인터페이스를 사용하지 않는 사람도 거의 없을 것입니다. save, findById, findAll 같

ohzzi.io

jpa repository 상속 구조

 

@Aspect
class JpaAccessAspect {

    private val log = LoggerFactory.getLogger(javaClass)

    @Around("execution(public * org.springframework.data.repository.Repository+.*(..))")
    fun logAll(joinPoint: ProceedingJoinPoint): Any? {
		}
}

2. 어떤 메서드가 호출되었는지 로그

joinPoint 를 통해 aop 가 적용된 메서드의 시그니처를 가져올 수 있다.

이를 통해 메서드 이름을 가져올 수 있다.

private fun getMethodName(joinPoint: ProceedingJoinPoint): String {
    return joinPoint.signature.name
}

3. 어떤 메서드가 호출하였는지 로그

이를 위해서는 stack trace 를 찾아봐야한다.

우선 stack trace 중 나의 패키지에 속하는 데이터만 필터링해준다.

// 패키지 네임으로 필터링
private fun getStack(): String {
      return Thread.currentThread().stackTrace.filter {
          it.className.contains("com.pius")
    }
}

필터링 후의 스택은 아래와 같다.

따라서, 2번째의 stackTraceElement 를 문자열로 가져온다.

private fun getStack(): String {
      return Thread.currentThread().stackTrace.filter {
          it.className.contains("com.pius")
      }.toTypedArray()[2].toString()
  }

4. time elapsed 로그

이 경우, 간단하게 아래와 같이 구현한다.

val startTime = System.currentTimeMillis()
val result = joinPoint.proceed()  
val timeElapsed = System.currentTimeMillis() - startTime

5. 예외 발생 message 로그

별 다른 추가 요구사항이 없기 때문에, spring 에서 변환해준 에러의 메세지를 로그로 찍으려고 한다.

var exception: Exception? = null
// or var exceptionMessage: String = ""
try{
	// 생략
}catch (e: Exception) {
    exception = e
    throw e
} finally {
		// 아래 메시지 사용
		exception?.cause?.message		
}

완성

@Aspect
class JpaAccessAspect {

    private val log = LoggerFactory.getLogger(javaClass)

    @Around("execution(public * org.springframework.data.repository.Repository+.*(..))")
    fun logAllJpaRepository(joinPoint: ProceedingJoinPoint): Any? {
        val startTime = System.currentTimeMillis()
        var exception: Exception? = null
        try {
			return joinPoint.proceed()
        } catch (e: Exception) {
            exception = e
            throw e
        } finally {
            val timeElapsed = System.currentTimeMillis() - startTime

            write(getMethodName(joinPoint), getStack(), timeElapsed, exception?.cause?.message)
        }
    }

    private fun getMethodName(joinPoint: ProceedingJoinPoint): String {
        return joinPoint.signature.name
    }

    private fun getStack(): String {
        return Thread.currentThread().stackTrace.filter {
            it.className.contains("com.pius")
        }.toTypedArray()[2].toString()
    }

    private fun write(methodName: String, stack: String, timeElapsed: Long, message: String?) {
        log.info
            mutableMapOf(
                "methodName" to methodName,
                "stack" to stack,
                "timeElapsed" to timeElapsed,
                "message" to message
            ).toString()
        )
    }
}