티스토리 뷰

Spring

[Spring] EmptyResultDataAccessException 예외가 발생한 SQL 쿼리와 파라미터 로깅하기

망나니개발자 2023. 12. 5. 10:00
반응형

 

 

 

 

1. EmptyResultDataAccessException 예외가 발생한 SQL 쿼리와 파라미터 로깅하기


[ 요구 사항 ]

서비스를 개발하다 보면 존재하지 않는 리소스에 접근하여 EmptyResultDataAccessException 에러가 발생하는 경우가 있다. 하지만 EmptyResultDataAccessException를 통해서는 어떤 쿼리의 어떤 파라미터로 인해 문제가 발생했는지 정확한 파악이 어렵다. 만약 여러 개의 레포지토리에 접근하는 중에 리소스를 접근하고 있다면 더욱 파악이 어렵다. 따라서 에러가 발생한 쿼리를 로깅하여 에러 로그를 보완하도록 하자.

 

 

[ 기능 추가 ]

  1. 쿼리와 파라미터를 저장하기 위한 컨텍스트 추가
  2. 파라미터를 컨텍스트에 저장하기 위한 AOP 추가
  3. 실행 쿼리를 컨텍스트에 저장하고, 컨텍스트를 초기화하는 인터셉터 개발 및 등록
  4. 빈 등록 및 에러 로깅 추가

 

 

쿼리와 파라미터를 저장하기 위한 컨텍스트 추가

먼저 쿼리와 파라미터를 저장하기 위한 컨텍스트가 필요하다. 이를 위한 스레드 로컬(Thread Local) 기반의 컨텍스트를 먼저 추가하도록 하자. 아래의 로직에서 getOriginQuery()는 기존의 ?를 파라미터로 대체한 쿼리를 반환한다.

internal class EmptyResultDataAccessExceptionContext {

    companion object {

        private val query = ThreadLocal<String>()
        private val params = ThreadLocal<List<Any>>()
        private val log = KotlinLogging.logger { }

        fun getOriginQuery(): String {
            val result = StringBuilder(query.get()!!)
            params.get().forEach {
                val index = result.indexOf("?")
                if (index != -1) {
                    result.replace(index, index + 1, it.toString())
                }
            }
            return result.toString()
        }

        fun saveQuery(sql: String?) {
            log.debug { "EmptyResultDataAccessExceptionContext saveQuery called" }
            if (sql != null) {
                this.query.set(sql)
            }
        }

        fun saveParams(args: List<Any>) {
            log.debug { "EmptyResultDataAccessExceptionContext paramsSaved called" }
            params.set(args)
        }

        fun clear() {
            log.debug { "EmptyResultDataAccessExceptionContext clear called" }
            query.remove()
            params.remove()
        }
    }
}

 

 

getOriginQuery()에 대한 테스트를 작성하여 정상 동작을 확인하도록 하자.

class EmptyResultDataAccessExceptionContextTest {

    @Test
    fun `origin 쿼리 조회`() {
        EmptyResultDataAccessExceptionContext.saveQuery("This is a template with placeholders: ?, ?, ?")
        EmptyResultDataAccessExceptionContext.saveParams(listOf("1", "2", "3"))

        assertThat(EmptyResultDataAccessExceptionContext.getOriginQuery()).isEqualTo("This is a template with placeholders: 1, 2, 3")
    }

}

 

 

 

파라미터를 컨텍스트에 저장하기 위한 AOP 추가

파라미터를 저장하기 위해서는 AOP를 활용한다. JPA 레포지토리를 구현한 인터페이스들 중에서 findBy로 시작하는 메서드를 호출한 경우에 컨텍스트를 넣어주도록 하였다. findAll로 시작하여 목록을 반환하는 경우에는 에러가 발생하는 것이 아니라 빈 리스트가 반환되기 때문에 처리할 필요가 없다.

@Aspect
class EmptyResultDataAccessExceptionLoggingAspect {

    @Before("execution(* org.springframework.data.repository.Repository+.*(..)) &&" +
        " execution(public * *..*Repository+.find*(..)) ||" +
        " execution(public * *..*Repository+.delete*(..))")
    fun beforeFindByMethod(joinPoint: JoinPoint) {
        EmptyResultDataAccessExceptionContext.saveParams(joinPoint.args.toList())
    }

}

 

 

 

실행 쿼리를 컨텍스트에 저장하고, 컨텍스트를 초기화하는 인터셉터 개발 및 등록

아래의 인터셉터는 실행 쿼리를 컨텍스트에 조정하고, 요청이 끝나면 컨텍스트를 초기화하는 기능을 담당한다. 실행 쿼리를 저장하기 위해 하이버네이트의 StatementInspector를, 컨텍스트 초기화를 위해 서블릿의 Filter를 사용하고 있다.

class EmptyResultDataAccessExceptionLoggingInterceptor : Filter, StatementInspector {

    override fun doFilter(request: ServletRequest?, response: ServletResponse?, chain: FilterChain?) {
        chain!!.doFilter(request, response)
        EmptyResultDataAccessExceptionContext.clear()
    }

    override fun inspect(sql: String?): String {
        EmptyResultDataAccessExceptionContext.saveQuery(sql)
        return sql!!
    }
}

 

 

하이버네이트의 StatementInspector를 등록하기 위해서는 아래의 값을 프로퍼티에 넣어주어야 한다.

spring.jpa.properties.hibernate.session_factory.statement_inspector=com.mangkyu.config.logging.EmptyResultDataAccessExceptionLoggingInterceptor

 

 

 

빈 등록 및 에러 로깅 추가

이제 생성한 객체들을 스프링의 빈으로 등록해주면 된다.

@Configuration(proxyBeanMethods = false)
class EmptyResultDataAccessExceptionLoggingConfiguration {

    @Bean
    fun emptyResultDataAccessExceptionLoggingInterceptor(): EmptyResultDataAccessExceptionLoggingInterceptor {
        return EmptyResultDataAccessExceptionLoggingInterceptor()
    }

    @Bean
    fun emptyResultDataAccessExceptionLoggingAspect(): EmptyResultDataAccessExceptionLoggingAspect {
        return EmptyResultDataAccessExceptionLoggingAspect()
    }

    @Bean
    fun hibernateCustomizer(interceptor: EmptyResultDataAccessExceptionLoggingInterceptor): HibernatePropertiesCustomizer {
        return HibernatePropertiesCustomizer { properties: MutableMap<String?, Any?> ->
            properties[AvailableSettings.STATEMENT_INSPECTOR] = interceptor
        }
    }

}

 

 

그리고 해당 에러가 발생한 경우에 다음과 같이 로그를 남기도록 에러 핸들링 부분을 추가해주면 완료된다.

@RestControllerAdvice
class ExceptionController {

    @ResponseStatus(HttpStatus.NOT_FOUND)
    @ExceptionHandler
    fun emptyResultDataAccessException(
        request: HttpServletRequest,
        e: EmptyResultDataAccessException,
    ): ResponseEntity<Any> {
        log.error("error query = ${EmptyResultDataAccessExceptionContext.getOriginQuery()}")

        return ...
    }
}

 

 

그리면 이제 해당 에러가 발생한 경우에 다음과 같이 어떤 쿼리로 실행되었는지 확인할 수 있다.

 

 

 

 

 

해당 문제를 해결하기 위해 AOP와 필터(Filter), 스레드 로컬(ThreadLocal), StatementInspector 등의 기술이 활용되었다. 그리고 각각은 다음의 역할을 담당하고 있다.

  • AOP: Repository가 findXXX로 실행되는 경우, 스레드 로컬에 파라미터를 저장한다.
  • StatementInspector: 스레드 로컬에 실행되는 SQL 쿼리를 저장한다.
  • Filter: 예외 처리 후에 요청이 완료되는 순간에 스레드 로컬을 초기화한다.

 

 

 

 

 

반응형
댓글
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG more
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함