훈훈훈

Spring boot :: Kotlin + Hibernate 사용 시, lazy loading 이슈 정리 본문

Spring Framework/개념

Spring boot :: Kotlin + Hibernate 사용 시, lazy loading 이슈 정리

훈훈훈 2020. 12. 19. 03:39

이번에는 스프링 부트와 Kotlin 그리고 JPA(Hibernate) 사용하면서 겪었던 N+1 이슈를 정리해보려고 한다.

 

여러 연관관계가 매핑되어 있는 테이블의 전체 데이터를 조회하는 API를 호출하였을 때,

한방 쿼리가 발생할 것으로 예상을 했지만.... 결과는 몇천만 건 이상 쿼리가 발생하는 이슈가 발견되었다.

 

해당 글에서는 문제를 해결하면서 알게된 사용한 기술들에서 발생한 문제점과 해결방안을 공유하려고 한다.

Hibernate 

하이버네이트는 지연로딩(Lazy Loading)을 할 때 Entity를 프록시 객체로 조회한다고 한다.

그리고 공식 문서를 확인해보면 Entity Class를 final 속성으로 선언하면 프록시 객체를 생성할 수 없기 때문에 지연로딩을 사용할 수 없다고 명시되어 있다.

 

코틀린 언어는 클래스, 프로퍼티, 함수는 기본적으로 final 속성이기 때문에, 상속이 불가능하다. 

따라서 open 키워드를 붙여줘야 하지만, 필자가 사용한 데이터 클래스는 open이 허용되지 않기 때문에 All-open 플러그인을 사용해줘야 한다.

All-open plugin

 

코틀린 공식 문서에 따르면 All-open 플러그인을 사용하면 명시적으로 open 키워드를 붙이지 않아도 된다고 소개하고 있다.

이제 아래 그림과 같이 Gradle에 의존성을 추가하면 클래스에 하나하나 open 키워드를 붙이지 않아도 된다.

 

buildscript {
    dependencies {
        classpath("org.jetbrains.kotlin:kotlin-allopen:${kotlinVersion}")
    }
}

apply plugin: "kotlin-allopen"

 

이제 지연로딩이 정상적으로 되는지 테스트 해보자.

간단하게 Team 과 Member 테이블을 만들었으며, ManyToOne 과 OneToMany 양방향으로 테스트를 하려고 한다.

 

 

ERD

 

Member Class

@Entity
data class Member(
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "Member_ID")
    var id: Long,

    @Column(name = "USERNAME")
    val userName: String,

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "TEAM_ID")
    val team: Team
)

Team Class

@Entity
data class Team(
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "TEAM_ID")
    var id: Long,

    val name: String,

    @OneToMany(fetch = FetchType.LAZY, mappedBy = "team")
    val member: List<Member> = arrayListOf()
)

 

이제 아래 코드로 테스트 해보자.

Team 과 Member 테이블에 등록된 데이터 중 PK=1 인 것들만 조회하려고 한다.

@Component
class StartTest {

    @Autowired
    private lateinit var memberRepository: MemberRepository;

    @Autowired
    private lateinit var teamRepository: TeamRepository

    @EventListener
    fun onApplicationEvent(event: ApplicationStartedEvent) {
        println("======================")
        teamRepository.findByIdOrNull(1) ?: throw Exception("team is null")
        println("======================")

        memberRepository.findByIdOrNull(1) ?: throw Exception("member is null")
        println("======================")

    }
}

 

Team과 Member 테이블에 등록된 데이터는 아래와 같다.

 

 

Team 테이블

 

 

Member 테이블

 

 

프로젝트 실행 시, 발생한 쿼리는 아래와 같다.

 

 

Team 조회 쿼리

 

Member 조회 쿼리

 

뭔가 이상하다고 느껴지지 않나요??

Team 조회 시 쿼리가 한 번이 나갔지만 Member 조회 시 쿼리가 두 번 나가는 것을 볼 수 있다.

 

All-open 플러그인이 제대로 적용이 되었는지 확인하기 위해 컴파일된 Entity 코드를 확인해보자. 

 

 

위 Member 클래스를 확인해보면 여전히 final 속성인 것을 알 수 있다.

무언가 놓친게 있나 다시 코틀린 공식 문서를 살펴보았다.

 

 

문서 하단으로 내려가 보면 All-open 플러그인 의존성 추가 시, 자동으로 적용되는 어노테이션 종류가 많지만 @Entity는 자동으로 적용되지 않는 것을 확인하였다. 

 

따라서 Gradle에 아래와 같은 코드를 추가하였다. 

allOpen {
    annotation("javax.persistence.Entity")
}

 

위와 같이 의존성을 추가하고 프로젝트를 빌드를 하면 아래와 같이 데이터 클래스에 open 키워드가 붙어있는 것을 확인할 수 있다.

이제 정상적으로 지연로딩이 되는지 확인해보자.

 

 

이제는 정상적으로 지연로딩이 되는 것을 확인할 수 있다. 

 

 

마지막으로 타입 캐스팅 문제가 남아있다.

All-open 플러그인을 프로젝트에 적용하면 아래와 같은 메시지를 확인할 수 있다.

 

 

open 속성을 갖고 있는 val 프로퍼티에 대해서는 스마트 캐스트를 제공해주지 않는 문제이다.

따라서 오류가 발생한 프로퍼티에 모두 !! 를 붙여주면 해결이 된다. 

 

관련 내용은 아래 코틀린 공식 문서에서 확인할 수 있다.

 

 

 

 

출저

woowabros.github.io/experience/2020/05/11/kotlin-hibernate.html

 

코틀린에서 하이버네이트를 사용할 수 있을까? - 우아한형제들 기술 블로그

신규 시스템을 개발하면서 코틀린과 하이버네이트를 함께 사용한 경험을 나누기 위해 작성해봅니다.

woowabros.github.io

kotlinlang.org/docs/reference/compiler-plugins.html#all-open-compiler-plugin

 

Compiler Plugins - Kotlin Programming Language

 

kotlinlang.org

blog.junu.dev/37

 

Spring Boot + Kotlin + JPA 적용하기 Entity 생성시 생각해볼 점들

2020-05-12 우아한 형제들 기술 블로그 - 코틀린에서 하이버네이트를 사용할 수 있을까?에 나온 내용 추가합니다. 4. data class 사용에 대해 본글에서 적은 순환참조 이슈 외에도 다른 이슈가 나와있

blog.junu.dev

www.xspdf.com/resolution/52913032.html

 

Smart cast to 'Type' is impossible, because 'variable' is a mutable property that could have been changed by this time

Smart cast to button is impossible Smart cast to 'Type' is impossible, because 'variable , Between execution of left != null and queue.add(left) another thread could have changed the value of left to null . To work around this you have  Smart cast to 'Nod

www.xspdf.com

 

Comments