JPA를 사용하면 N+1문제를 자주 겪게 됩니다.

이 문제를 해결하기 위해 주로 3가지방법을 사용합니다.

  1. Fetch Join (inner join)
  2. BatchSize
  3. Entity Graph (fetch Join의 left outer join)

이 셋의 차이점을 잘 이해하지 못해서, 다음과 같은 문제점이 발생했습니다.

문제: 모든 지출을 조회하는데, 1:1 관계인 카테고리 N+1 문제 발생!

BatchSize

spring:
    properties:
      hibernate:
        jdbc:
          batch_size: 1000
//...
select
        e1_0.expenditure_id,
        e1_0.cost_category_id,
        e1_0.cost,
        e1_0.is_excluded,
        e1_0.member_id,
        e1_0.memo,
        e1_0.time       
    from
        expenditure e1_0       
    where
        e1_0.member_id=1           
        and e1_0.cost>=1500       
    limit
        0,10
2023-11-16T17:05:19.705+09:00  INFO 11640 --- [nio-8080-exec-1] p6spy                                    : [statement] | 0 ms | 
    select
        count(e1_0.expenditure_id)       
    from
        expenditure e1_0       
    where
        e1_0.member_id=1           
        and e1_0.cost>=1500
2023-11-16T17:05:19.709+09:00  INFO 11640 --- [nio-8080-exec-1] p6spy                                    : [statement] | 0 ms | 
    select
        c1_0.cost_category_id,
        c1_0.name       
    from
        cost_category c1_0       
    where
        c1_0.cost_category_id=1
2023-11-16T17:05:19.711+09:00  INFO 11640 --- [nio-8080-exec-1] p6spy                                    : [statement] | 0 ms | 
    select
        c1_0.cost_category_id,
        c1_0.name       
    from
        cost_category c1_0       
    where
        c1_0.cost_category_id=2
2023-11-16T17:05:19.711+09:00  INFO 11640 --- [nio-8080-exec-1] p6spy                                    : [statement] | 0 ms | 
    select
        c1_0.cost_category_id,
        c1_0.name       
    from
        cost_category c1_0       
    where
        c1_0.cost_category_id=3
//....

그대로 N+1문제가 발생!!!

@Override
  public Page<Expenditure> searchExpenditures(
     //...
    List<Expenditure> expenditures = queryFactory
        .select(expenditure)
        .from(expenditure)
        .where(
            expenditure.member.eq(member),
            categoryEq(category),
            startDateEq(startDate),
            endDateEq(endDate),
            minCostEq(minCost),
            maxCostEq(maxCost)
        )
        .join(expenditure.category, costCategory) // 추가
        .fetchJoin()                              // 추가
        .offset(pageable.getOffset())
        .limit(pageable.getPageSize())
        .fetch();
select
        e1_0.expenditure_id,
        e1_0.cost_category_id,
        c1_0.cost_category_id,
        c1_0.name,
        e1_0.cost,
        e1_0.is_excluded,
        e1_0.member_id,
        e1_0.memo,
        e1_0.time       
    from
        expenditure e1_0       
    join
        cost_category c1_0               
            on c1_0.cost_category_id=e1_0.cost_category_id       
    where
        e1_0.member_id=1           
        and e1_0.cost>=1500       
    limit
        0,10
2023-11-16T17:07:38.219+09:00  INFO 6788 --- [nio-8080-exec-1] p6spy                                    : [statement] | 0 ms | 
    select
        count(e1_0.expenditure_id)       
    from
        expenditure e1_0       
    where
        e1_0.member_id=1           
        and e1_0.cost>=1500
2023-11-16T17:07:38.223+09:00  INFO 6788 --- [nio-8080-exec-1] p6spy                                    : [commit] | 0 ms |

N+1 문제 해결!!

둘이 무슨 차이가 있었을까요?