발단
스케줄러 작업을 통해 주기적으로 DB 요청을 하는 상황에서 로그를 살펴보니 원하지 않았던 정보까지 요청하고 있어 어떻게 해결하게 되었는지 공유하고자 합니다.
환경
- Spring Boot > 3.0
- Kotlin
Kotlin 의 final 정책
이해를 돕기 위해 간단히 Kotlin 의 정책에 대해 얘기해보고자 합니다.
Effective Java, "Item 17: Design and document for inheritance or else prohibit it"
`Effective Java` 2001 연도에 출판한 책으로 Java 의 효율적인 사용과 좋지만 흔히 사용되지 않은 개발패턴에 대해 언급한 저명한 서적입니다. 해당 책에서 `상속을 위해 디자인과 문서화 하지 않을거면 지양해라` 라는 챕터를 다룬적 있습니다.
Kotlin 은 클래스및 변수에 대해 기본적으로 final 을 적용합니다. final 을 modifier 로써 사용하게 되면 상속을 하지 못하게 할 수 있습니다. Kotlin 문서에서 default 로 final 을 적용한 부분에 대해 따로 언급하고 있지는 않습니다만 이유로 추측하길, 위에서 언급한 인용과 같이 제대로 문서화되지 않은 상속 남용으로 인해 고통을 받아 지양하는 문화가 생긴듯 싶습니다.
별개로 현재까지 final 을 default 로써 사용하는 방식에 대한 논의가 이어지고 있으며 의견을 확인하고 싶다면 Kotlin Discussions 라는 커뮤니티에서 참고하시면 될 듯 싶습니다. 전반적으로 Kotlin 의 정책에는 동의하지만 Spring 과 같은 framework 사용시 IDE 상에서 오류가 발생하는 부분에 대해 상당히 부정적인듯 싶었습니다.
- Kotlin Discussions `final 이 기본인 점에 대해`
JPA(Hibernate) 의 지연로딩은 proxy 를 사용한다.
JPA 는 Java Persistence Api 의 약자로 인터페이스를 제공합니다. 즉, 구현체는 따로 있다는 말이 되죠. 이 구현체중 하나로 우리는 흔히(Spring Boot 의 경우) Hibernate 를 사용합니다.
Hibernate 는 지연로딩을 구현하기 위해 proxy 를 생성합니다. 지연로딩 대상에 대한 proxy 를 구축하는 과정에서 상속을 이용하는데 이는 위에서 말씀드렸던 kotlin 의 기본 정책과 기본적으로 맞지 않는 방식이라고 볼 수 있습니다. 즉, 이 맞지 않는 정책이 지연로딩을 하지 못하게 하는 주체라고 볼 수 있습니다.
JPA 의 proxy 에 대한 자세한 설명은 아래 글을 보면 쉽게 이해될 거라 생각합니다.
- JPA Hibernate 프록시 설명
해결
all-open plugin 적용
Kotlin has classes and their members final by default, which makes it inconvenient to use frameworks and libraries such as Spring AOP that require classes to be open. The all-open compiler plugin adapts Kotlin to the requirements of those frameworks and makes classes annotated with a specific annotation and their members open without the explicit open keyword.
Kotlin 은 기본 정책으로 final 을 적용하는데, 이는 Spring AOP 와 같이 반드시 open 이어야 하는 프레임워크나 라이브러리를 사용함에 있어서 불편함을 제공합니다. all-open 플러그인 컴파일러는 ... open 을 명시하지 않아도 open 되게끔 해줍니다.
Kotlin 은 기본적으로 default 를 적용한다고 말하였습니다. 이에 대해 Kotlin 문서에서 설명은 하지 않았다고 했는데 그래도 신경은 쓰였나 봅니다. 그래서 그런 불편함을 덜어내고자 final 을 해제할 수 있는 플러그인을 제공하고 있습니다.
예제
plugins {
...
kotlin("plugin.allopen") version "1.7.22"
}
allOpen {
// all-open 의 적용대상이 될 annotation 정의
annotation("jakarta.persistence.Entity")
...
}