Spring proxy 객체와 NPE

신연우·2024년 12월 25일

WIL

목록 보기
17/21

문제 상황

// 상황 설명을 위해 예시로 작성된 코드입니다.
open class ExampleClient {
    private val client: OkHttpClient = /* 생략 */
    private val retrofit: Retrofit = /* 생략 */
    private val exampleApi = retrofit.create(ExampleApi::class.java)
    
    @Cacheable(cacheNames = ["example"], key = "#id")
    fun findExampleById(id: Long): ExampleListDto? {
        return runCatching {
            exampleApi.findExampleById(id).blockingGet()
        }.getOrNull()
    }
}

@Configuration
class ClientConfiguration {
    @Bean
    fun exampleClient(): ExampleClient {
        return ExampleClient()
    }
}

DB에 존재하는 id 값을 매개변수로 넘겨서 ExampleClient.findExampleById를 호출하였으나 항상 값이 null이 반환되는 문제가 있었습니다.

분석

가장 먼저 서버 지표를 확인해보니 ExampleClient.findExampleById API 호출 자체가 일어나지 않은 것을 확인했습니다.

그래서 runCatching.onFailure 구문을 추가해 어떤 예외가 발생했는지 확인해보기로 했습니다. 확인해보니 exampleApi 변수가 null 값으로 채워져 있는 상황이었고, 이로 인해 findExampleById 메서드를 호출할 때 NPE가 발생하였습니다.

원인

NPE가 발생하는 원인을 찾아보니 아래와 같았습니다.

호출한 메서드의 경우 @Cacheable 어노테이션이 붙어있습니다. Spring AOP는 Proxy 객체를 만들 때 CGLIB을 사용합니다. CGLIB은 상속을 이용하여 Proxy 객체를 만드는데요. Kotlin의 클래스는 기본적으로 final 클래스이므로 상속을 할 수 없는 클래스입니다.

이번에 호출한 메서드는 open 메서드가 아니다보니 CGLIB이 오버라이드 하지 못한 메서드가 되어 해당 메서드를 호출할 때 내부 멤버 변수 값이 모두 NULL로 채워진 상태에서 호출을 하게 된 것입니다.

해결

open class ExampleClient {
    private val client: OkHttpClient = /* 생략 */
    private val retrofit: Retrofit = /* 생략 */
    private val exampleApi = retrofit.create(ExampleApi::class.java)
    
    @Cacheable(cacheNames = ["example"], key = "#id")
    open fun findExampleById(id: Long): ExampleListDto? {
        return runCatching {
            exampleApi.findExampleById(id).blockingGet()
        }.getOrNull()
    }
}

위와 같이 @Cacheable 어노테이션이 달린 메서드를 open 메서드로 만들어서 해결했습니다.

부록 1. kotlin all-open 플러그인

kotlin-allopen 플러그인을 사용하면 특정 어노테이션이 붙어있는 클래스는 모두 open 클래스로 만들며 메서드 또한 open 메서드가 되도록 설정해줍니다. 아래 어노테이션이 붙어있으면 kotlin-allopen 플러그인이 적용됩니다.

  • @Component
  • @Async
  • @Transactional
  • @Cacheable
  • @Configuration
  • @Controller
  • @RestController
  • @Service
  • @Repository

부록 2. all-open 적용하고 싶은 어노테이션을 직접 지정하기

위에 나온 어노테이션 이외에도 특정 어노테이션이 붙은 클래스는 모두 kotlin-allopen 플러그인이 적용되면 하는 경우 다음 설정을 통해 적용할 수 있습니다.

// build.gradle.kts
allOpen {
    annotation("javax.persistence.Entity")
    annotation("javax.persistence.MappedSuperclass")
    annotation("javax.persistence.Embeddable")
}

참고한 글

profile
남들과 함께하기 위해서는 혼자 나아갈 수 있는 힘이 있어야 한다.

0개의 댓글