훈훈훈
Spring boot :: Kotlin + Hibernate 사용 시, lazy loading 이슈 정리 본문
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 양방향으로 테스트를 하려고 한다.
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 조회 시 쿼리가 두 번 나가는 것을 볼 수 있다.
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
kotlinlang.org/docs/reference/compiler-plugins.html#all-open-compiler-plugin
www.xspdf.com/resolution/52913032.html
'Spring Framework > 개념' 카테고리의 다른 글
Spring boot :: QueryDSL을 사용해서 No Offset Paging 구현하기 (0) | 2021.01.10 |
---|---|
Spring boot :: JPA에서 OneToOne 관계 N+1 문제 정리 (0) | 2020.12.28 |
Spring Security :: CSRF protection disable option 대한 생각 정리 (0) | 2020.10.20 |
Spring :: @RequestBody with Multiple Object arguments 오류 정리 (0) | 2020.10.11 |
Spring Framework :: IntelliJ 환경에서 Spring MVC 초기 설정 (0) | 2020.05.20 |