본문 바로가기

트러블 슈팅

kotlin + Jpa Lazy Loading 트러블 슈팅

상황 1

Kotlin과 JPA를 사용하면서, 다음과 같은 plugins를 적용하고 LazyLoading을 사용하였으나, 적용되지 않는 문제가 발생하였다.

kotlin("plugin.spring") version "1.8.21"
kotlin("plugin.jpa") version "1.8.21"

🔎 plugin.spring: 다음과 같은 어노테이션이 붙은 코틀린 클래스에 대해 자동으로 붙는 final 키워드를 open 해주는 allOpen을 지원한다. Spring boot 2.x 버전부터 CGLIB Proxy 방식으로 Bean을 관리하고 있어, Target Class를 상속받아 프록시를 생성하기 때문에 open으로 열기 위해 사용한다.

@Component, @Async, @Transactional, @Cacheable, @SpringBootTest, @Configuration, @Controller, @RestController, @Service, @Repository, @Component

 

🔎 plugin.jpa: argument가 없는 기본 생성자를 생성해주는 플러그인이다. Hibernate는 Reflection으로 객체를 생성하기 때문에protected이상의 생성자가 필요해 사용한다.

@Entity, @Embeddable, @MappedSuperclass

 

원인 1

📝  Lazy Loading 역시 CGLIB Proxy 방식을 사용하기 때문에, 상속이 가능한 open class여야 한다. plugin.spring은 @Entity, @Embeddable, @MappedSuperclass에 대해 allOpen을 지원하지 않아 Lazy Loading이 동작하지 않는다.

 

해결 방법 1

allOpen {
    annotation("jakarta.persistence.Entity")
    annotation("jakarta.persistence.Embeddable")
    annotation("jakarta.persistence.MappedSuperclass")
}

@Entity, @Embeddable, @MappedSuperclass에 대해 allOpen을 적용하도록 위와 같은 코드를 build.gradle.kts에 추가한다.

 

@Entity
@IdClass(FollowId::class)
class Follow(
    @Id
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "follower_id", columnDefinition = "NUMERIC(19, 0)", nullable = false, updatable = false)
    val follower: User,

    @Id
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "following_id", columnDefinition = "NUMERIC(19, 0)", nullable = false, updatable = false)
    val following: User,
) : BaseTimeEntity() {
}

 

💬 위 Follow를 조회할 때, Follow 만 조회하는 것을 확인할 수 있다.

 

❌ allOpen을 추가할 때, 패키지 경로가 Spring Boot 3.x 버전부터 javax가 jakarta로 변경되었기 때문에, 버전에 맞게 경로를 입력해야 한다. 


상황 2

// FollowService
fun getMyFollowings(followerId: Long, pageable: Pageable): PageResponse<FollowUserResponse> {
    val pageData = followRepository.findPageByFollowerId(follower, pageable)
       .map { FollowUserResponse(it.following) }
    return PageResponse(pageable, pageData)
}


data class FollowUserResponse(
    val nickname: String,
    val profileName: String
) {
    constructor(user: User) : this(
        user.nickname,
        user.profileName
    )
}

OSIV=false, Lazy Loading을 사용하면서, 위 코드를 사용하여 Entity를 Dto로 변환하는 과정에서 다음과 같은 에러를 겪었다.

 

org.hibernate.LazyInitializationException: could not initialize proxy [kr.x.x.domain.user.User#1] - no Session

 

원인 2

📝 Lazy Loading으로 데이터를 조회하면, 실제 User 객체가 아닌 프록시로 저장되어 있다.

프록시를 사용할 수 있는 Session 범위는 Transaction 범위인데, 위 코드에서는 findPageByFollowerId 쿼리 메서드가 동작할 때만 Transaction(readOnly=true)로 동작한다. 따라서, Transaction 범위 밖인 FollowUserResponse에서 User에 접근할 때 LazyInitializationException이 발생한다.

 

해결방법 2

@Transactional(readOnly = true)
fun getMyFollowings(followerId: Long, pageable: Pageable): PageResponse<FollowUserResponse> {
    val follower = userRepository.getByUserId(followerId)
    val pageData = followRepository.findPageByFollower(follower, pageable).map { FollowUserResponse(it.following) }
    return PageResponse(pageable, pageData)
}

위 코드와 같이 Transaction 범위를 getMyFollowings 메서드로 설정하면 정상적으로 수행된다.