JPA - 지연로딩(LAZY)와 즉시로딩(EAGER)


  • 지연로딩(LAZY)과 즉시로딩(EAGER)

지연로딩(LAZY)

지연 로딩(LAZY)을 사용해서 프록시로 조회

Member
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Entity
class Member (
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
var id: Long? = null,

@Column(name = "USERNAME")
var username: String? = null,
) : BaseEntity() {
@ManyToOne(fetch = FetchType.LAZY) // 지연로딩 추가 부분
@JoinColumn(name = "TEAM_ID")
var team: Team? = null

fun changeTeam(team: Team) {
this.team = team;
team.members.add(this)
}
}
Team
1
2
3
4
5
6
7
8
9
10
11
12
13
@Entity
class Team (
@Id
@GeneratedValue
@Column(name = "TEAM_ID")
var id: Long? = null,

@Column(name = "NAME")
var name: String? = null,
){
@OneToMany(mappedBy = "team")
var members: MutableList<Member> = arrayListOf()
}
jpaMain
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
fun main() {
val emf = Persistence.createEntityManagerFactory("hello")
val em = emf.createEntityManager()
val tx = em.transaction

tx.begin()

try {
val team = Team(
name = "team1"
)
em.persist(team)

val member = Member(
username = "member1",
)
member.team = team
em.persist(member)

em.flush()
em.clear()

val findMember = em.find(Member::class.java, member.id)
println("findMember = ${findMember.team!!.javaClass}")

tx.commit()
} catch (e: Exception) {
tx.rollback()
} finally {
em.close();
emf.close();
}
}
result
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Hibernate: 
select
member0_.MEMBER_ID as MEMBER_I1_3_0_,
member0_.createdAt as createdA2_3_0_,
member0_.deletedAt as deletedA3_3_0_,
member0_.updatedAt as updatedA4_3_0_,
member0_.TEAM_ID as TEAM_ID6_3_0_,
member0_.USERNAME as USERNAME5_3_0_
from
Member member0_
where
member0_.MEMBER_ID=?
Hibernate:
select
team0_.TEAM_ID as TEAM_ID1_5_0_,
team0_.NAME as NAME2_5_0_
from
Team team0_
where
team0_.TEAM_ID=?
findMember = class entity.Team

여기서 지연 로딩(LAZY)로 Member 객체를 조회 하였는데, Team 정보 가지 조회 되는 현상이 나타난다.
원래는 지연 로딩시 Team 정보는 Proxy로 가져오고, Team을 조회 할때 정보를 가지고 와야 지연로딩이 된다고 할 수 있다.
지연 로딩을 하려면 프록시 객체를 만들어야 하는데, Kotlin의 모든 클래스는 final이라 상속을 받을 수 없고, 일반 클래스는 open할 수 있지만 데이터 클래스는 불가능하다.
JPA 표준에서는 엔티티 클래스가 final이면 안되는데 JPA 구현체로써 사용하기 때문에 작동을 한다.
gradle 에 아래 와 같이 allOpen 플러그인 추가 해주면된다.

build.gradle.kts
1
2
3
4
5
6
7
8
9
10
plugins {
kotlin("jvm") version "1.5.31"
id("org.jetbrains.kotlin.plugin.allopen") version "1.5.31"
}

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

}
result
1
2
3
4
5
6
7
8
9
10
11
12
13
Hibernate: 
select
member0_.MEMBER_ID as MEMBER_I1_3_0_,
member0_.createdAt as createdA2_3_0_,
member0_.deletedAt as deletedA3_3_0_,
member0_.updatedAt as updatedA4_3_0_,
member0_.TEAM_ID as TEAM_ID6_3_0_,
member0_.USERNAME as USERNAME5_3_0_
from
Member member0_
where
member0_.MEMBER_ID=?
findMember = class entity.Team$HibernateProxy$pD1cOOED

즉시로딩(EAGER)

JPA 구현체는 가능하면 조인을 사용해서 SQL 한번에 조회

Member
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Entity
class Member (
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
var id: Long? = null,

@Column(name = "USERNAME")
var username: String? = null,
) : BaseEntity() {
@ManyToOne(fetch = FetchType.EAGER) // 즉시로딩 추가 부분
@JoinColumn(name = "TEAM_ID")
var team: Team? = null

fun changeTeam(team: Team) {
this.team = team;
team.members.add(this)
}
}
result
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Hibernate: 
select
member0_.MEMBER_ID as MEMBER_I1_3_0_,
member0_.createdAt as createdA2_3_0_,
member0_.deletedAt as deletedA3_3_0_,
member0_.updatedAt as updatedA4_3_0_,
member0_.TEAM_ID as TEAM_ID6_3_0_,
member0_.USERNAME as USERNAME5_3_0_,
team1_.TEAM_ID as TEAM_ID1_5_1_,
team1_.NAME as NAME2_5_1_
from
Member member0_
left outer join
Team team1_
on member0_.TEAM_ID=team1_.TEAM_ID
where
member0_.MEMBER_ID=?
findMember = class entity.Team

프록시와 즉시로딩 주의

  • 가급적 지연 로딩(LAZY)만 사용
  • 즉시 로딩(EAGER)을 적용하면 예상하지 못한 SQL이 발생
  • 즉시 로딩(EAGER)은 JPQL에서 N+1 문제를 일으킨다.
  • @ManyToOne, @OneToOne는 기본이 지연 로딩(LAZY)
  • @OneToMany, @ManyToMany는 기본이 즉시 로딩(EAGER)

소스코드

참조