[문제해결] findById() 최적화가 필요한 이유

YeonJi·2025년 3월 15일
3

문제해결

목록 보기
18/24

💭 상황

Kotlin Spring Boot Project 강의를 듣던 중 이해가 안 되는 부분이 생겨 구글링을 해봤고 테스트 코드를 작성해 비교하여 알아봤다.

🤔 이해를 못한 부분

findById()는 JPA에서 기본으로 제공해주는 메서드이며 id 값에 해당하는 단일 Entity를 조회하는 기능을 수행한다.

강의 내용 중 findById()가 성능 개선이 필요해 Fetch Join을 해야한다는 부분이 이해가 안됐다.

어떤 부분 때문에 최적화가 필요한걸까?

🧪 테스트 코드

@Test
fun testFindById() {
    val experience = experienceRepository.findById(1).get()
    logger.info { experience.details }
}

🌚 Fetch Join 전 결과

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]

🌝 Fetch Join 후 결과

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 조회
  • 두 번째 쿼리 : ExperienceDetail 조회

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에 대한 차이점을 다시 공부해야겠다.

profile
문제해결 위주로 기록

0개의 댓글