Kotlin Spring Boot Project 강의를 듣던 중 이해가 안 되는 부분이 생겨 구글링을 해봤고 테스트 코드를 작성해 비교하여 알아봤다.
findById()는 JPA에서 기본으로 제공해주는 메서드이며 id 값에 해당하는 단일 Entity를 조회하는 기능을 수행한다.
강의 내용 중 findById()가 성능 개선이 필요해 Fetch Join을 해야한다는 부분이 이해가 안됐다.
어떤 부분 때문에 최적화가 필요한걸까?
@Test
fun testFindById() {
val experience = experienceRepository.findById(1).get()
logger.info { experience.details }
}
Hibernate:
select
e1_0.id,
e1_0.created_date_time,
e1_0.description,
e1_0.end_month,
e1_0.end_year,
e1_0.is_active,
e1_0.start_month,
e1_0.start_year,
e1_0.title,
e1_0.updated_date_time
from
experience e1_0
where
e1_0.id=?
Hibernate:
select
d1_0.experience_id,
d1_0.id,
d1_0.content,
d1_0.created_date_time,
d1_0.is_active,
d1_0.updated_date_time
from
experience_detail d1_0
where
d1_0.experience_id=?
2025-03-16T05:49:49.040+09:00 INFO 10648 --- [ main] c.j.p.d.r.ExperienceRepositoryTest : [com.jiyeon.portfolio.domain.entity.ExperienceDetail@66b65360]
interface ExperienceRepository : JpaRepository<Experience, Long> {
@Query("select e from Experience e left join fetch e.details where e.id = :id")
override fun findById(id: Long): Optional<Experience>
}
Hibernate:
select
e1_0.id,
e1_0.created_date_time,
e1_0.description,
d1_0.experience_id,
d1_0.id,
d1_0.content,
d1_0.created_date_time,
d1_0.is_active,
d1_0.updated_date_time,
e1_0.end_month,
e1_0.end_year,
e1_0.is_active,
e1_0.start_month,
e1_0.start_year,
e1_0.title,
e1_0.updated_date_time
from
experience e1_0
left join
experience_detail d1_0
on e1_0.id=d1_0.experience_id
where
e1_0.id=?
2025-03-16T05:56:15.601+09:00 INFO 14600 --- [ main] c.j.p.d.r.ExperienceRepositoryTest : [com.jiyeon.portfolio.domain.entity.ExperienceDetail@61b83fef]
테스트 코드를 작성해 전과 후를 비교해보니 차이점을 알 수 있었다.
@OneToMany(fetch = FetchType.LAZY) 부분으로 인해 성능 개선이 필요했던 것이다.
Experience를 조회할 때 연관된 ExperienceDetail Entity는 즉시 로딩되지 않고, details에 접근할 때마다 추가적인 쿼리가 실행된다.
Experience 객체에 대한 조회는 한 번만 수행되지만, details에 접근할 때마다 별도의 쿼리가 실행되므로 성능에 영향을 미칠 수 있다는 부분이 문제가 되었다.
@Entity
class Experience(
title: String,
description: String,
startYear: Int,
startMonth: Int,
endYear: Int?,
endMonth: Int?,
isActive: Boolean) : BaseEntity() {
...
@OneToMany(
targetEntity = ExperienceDetail::class,
fetch = FetchType.LAZY,
cascade = [CascadeType.ALL])
@JoinColumn(name = "experience_id")
var details: MutableList<ExperienceDetail> = mutableListOf()
...
}
Fetch Join을 사용하면 Experience와 ExperienceDetail을 하나의 쿼리로 데이터를 가져올 수 있기 때문에 성능 개선이 된다.
Fetch Join을 사용하면 한 번에 부모와 자식 데이터를 조회할 수 있다는 것은 알고 있었다.
그렇기 때문에 N+1 문제를 해결하는 방법 중 하나가 Fetch Join이라고 알고있다.
N+1 문제를 해결할 수 있다는 것에 초점이 맞춰져 있어서 Lazy Loading에 대해 놓치고 있었다.
"N+1 문제가 생겼네 → Fetch Join을 사용해 해결해야지" 라는 생각이 원인이었다.
"왜?" 라는 생각을 갖고 알아가는 게 중요하다는 것을 깨달았다.
Eager Loading과 Lazy Loading에 대한 차이점을 다시 공부해야겠다.