이번에는 JPA의 n+1을 해결하는 방법을 한번 살펴보자.
n+1의 예를 한번들어보자.

어떤 Account 라는 엔티티와 Order라는 엔티티가 있다고 가정하자.

@Entity
public class Account {

  @Id
  @GeneratedValue
  @Column(name = "ACCOUNT_ID")
  private Long id;

  private String name;

  private String password;

  private String email;

  @OneToMany(mappedBy = "account", fetch = FetchType.LAZY)
  private List<Order> orders;
}
//getter setter etc..

@Entity
public class Order {

  @Id
  @GeneratedValue
  @Column(name = "ORDER_ID")
  private Long id;

  @ManyToOne
  @JoinColumn(name = "ACCOUNT_ID")
  private Account account;

  @Temporal(TemporalType.TIMESTAMP)
  private Date orderDate;

//getter setter etc..
}

위와 같은 도메인이 있다고 가정할때 우리는 n+1 만날 수 있다. 예를 들어 Account를 찾고 그 안에 주문정보를 보고 싶다면 Account를 조회 후 해당하는 주문정보를 또 조회해야 한다. 예를들어 계정 정보가 10개가 있다면 주문 정보도 10번 조회를 해야 한다. 또한 주문정보에는 주문한 item들이 있기도 하기에 이게 계속 되다보면 성능에 영향을 미칠 수 도 있다.
이것을 해결 하기 위해 우리는 몇가지 방법을 알아볼 예정이다.
Account를 찾아서 그 아래에 있는 리스트를 가져오려면 아래와 같은 쿼리문이 n번 날라간다.

Hibernate: 
    select
        account0_.account_id as account_1_0_,
        account0_.email as email2_0_,
        account0_.name as name3_0_,
        account0_.password as password4_0_ 
    from
        account account0_
Hibernate: 
    select
        orders0_.account_id as account_3_3_0_,
        orders0_.order_id as order_id1_3_0_,
        orders0_.order_id as order_id1_3_1_,
        orders0_.account_id as account_3_3_1_,
        orders0_.order_date as order_da2_3_1_ 
    from
        orders orders0_ 
    where
        orders0_.account_id=?
Hibernate: 
    select
        orders0_.account_id as account_3_3_0_,
        orders0_.order_id as order_id1_3_0_,
        orders0_.order_id as order_id1_3_1_,
        orders0_.account_id as account_3_3_1_,
        orders0_.order_date as order_da2_3_1_ 
    from
        orders orders0_ 
    where
        orders0_.account_id=?
  ...
  ...
//n개

이것을 해결하려면 JPA의 패치 조인을 사용하면 해결 할 수 있다. jpql를 직접 써도 되지만 필자는 querydsl을 사용하였다.

public List<Account> findByleftJoinOrders() {
  QAccount account = QAccount.account;
  QOrder order = QOrder.order;
  return from(account)
    .leftJoin(account.orders, order).fetchJoin()
    .fetch();
}

로그를 살펴보면 쿼리가 한번 출력 되는 것을 확인 할 수 있다.

Hibernate: 
    select
        account0_.account_id as account_1_0_0_,
        orders1_.order_id as order_id1_3_1_,
        account0_.email as email2_0_0_,
        account0_.name as name3_0_0_,
        account0_.password as password4_0_0_,
        orders1_.account_id as account_3_3_1_,
        orders1_.order_date as order_da2_3_1_,
        orders1_.account_id as account_3_3_0__,
        orders1_.order_id as order_id1_3_0__ 
    from
        account account0_ 
    left outer join
        orders orders1_ 
            on account0_.account_id=orders1_.account_id

하지만 여기의 예제에서는 문제가 있다 중복으로 데이터들을 가져온다. 그걸 방지하고자 distinct를 사용해서 중복을 제거하자.

public List<Account> findByleftJoinOrders() {
  QAccount account = QAccount.account;
  QOrder order = QOrder.order;
  return from(account)
    .distinct()
    .leftJoin(account.orders, order).fetchJoin()
    .fetch();
}

다시 테스트를 해보면 중복이 제거된 상태로 출력이 된다. 근데 순서가 맞지 않게 정렬 되어 나온다. account도 그렇지만 그 안에 주문 정보도 마찬가지다.
아래와 같이 orderby를 넣어주면 끝이다.

public List<Account> findByleftJoinOrders() {
  QAccount account = QAccount.account;
  QOrder order = QOrder.order;
  return from(account)
    .distinct()
    .leftJoin(account.orders, order).fetchJoin()
    .orderBy(account.id.asc(), order.id.asc())
    .fetch();
}

이렇게 fetch 조인을 써서 n+1을 막을 수가 있다. fetch 조인 말고도 방법이 몇가지 있는데 이것은 하이버네이트에서 제공해주는 방법이 있다. 하이버네이트에 의존적이긴 하지만 거의 대부분이 하이버네이트를 쓰기에 그렇게 많은 문제는 되지 않을 듯하다.

@OneToMany(mappedBy = "account", fetch = FetchType.LAZY)
@BatchSize(size = 5)
private List<Order> orders;

하이버네이트에서 제공해주는 BatchSize 어노테이션을 넣어주면 끝이다. 저 사이즈는 한번에 가져오는 갯수를 말하는것이다. 예를들어 Account가 10개 있으면 Order도 10번의 쿼리를 날려서 가져오는데 5개씩 2번의 쿼리만 날려서 가져오게 된다.

Hibernate: 
    select
        account0_.account_id as account_1_0_,
        account0_.email as email2_0_,
        account0_.name as name3_0_,
        account0_.password as password4_0_ 
    from
        account account0_
Hibernate: 
    select
        orders0_.account_id as account_3_3_1_,
        orders0_.order_id as order_id1_3_1_,
        orders0_.order_id as order_id1_3_0_,
        orders0_.account_id as account_3_3_0_,
        orders0_.order_date as order_da2_3_0_ 
    from
        orders orders0_ 
    where
        orders0_.account_id in (
            ?, ?, ?, ?, ?
        )
Hibernate: 
    select
        orders0_.account_id as account_3_3_1_,
        orders0_.order_id as order_id1_3_1_,
        orders0_.order_id as order_id1_3_0_,
        orders0_.account_id as account_3_3_0_,
        orders0_.order_date as order_da2_3_0_ 
    from
        orders orders0_ 
    where
        orders0_.account_id in (
            ?, ?
        )

위와 같은 쿼리를 날린다. in을 사용하여 데이터들을 가져오게된다. 만약 글로벌하게 설정하고 싶다면 아래와 같이 하면 된다. 필자는 Spring boot를 자주 사용해서 boot기준이다. 만약 boot를 사용하지 않느다면 아래와 같은 설정을 xml에 해주면 될 것으로 판단된다.

spring.jpa.properties.hibernate.default_batch_fetch_size=5

n+1의 마지막 방법으로 @Fetch(FetchMode.SUBSELECT)을 사용하면 된다. 이것 또한 하이버네이트에서 제공해주는 어노테이션이다.
방법 역시 간단하다.

@OneToMany(mappedBy = "account", fetch = FetchType.LAZY)
@Fetch(FetchMode.SUBSELECT)
private List<Order> orders;

위와 같이 어노테이션만 넣어 주면 끝난다.

Hibernate: 
    select
        account0_.account_id as account_1_0_,
        account0_.email as email2_0_,
        account0_.name as name3_0_,
        account0_.password as password4_0_ 
    from
        account account0_
Hibernate: 
    select
        orders0_.account_id as account_3_3_1_,
        orders0_.order_id as order_id1_3_1_,
        orders0_.order_id as order_id1_3_0_,
        orders0_.account_id as account_3_3_0_,
        orders0_.order_date as order_da2_3_0_ 
    from
        orders orders0_ 
    where
        orders0_.account_id in (
            select
                account0_.account_id 
            from
                account account0_
        )

그럼 위와 같이 서브 쿼리를 작성해서 날라간다.

필자가 말한 위의 3가지 방법 모두 사용가능하지만 @Fetch와 @BatchSize 는 너무 정적이다. 굳이 필요 없을 때에도 쿼리를 날려야만 한다. 그게 그렇게 많은 상관이 없다면 해도 되지만 정적인것을 역시 맘에 들지 않는다. 그래서 필자는 fetch조인을 그때그때 사용하는 것이 낫다고 판단한다. (물론 필자생각)
이것저것 다 해보고 자신에게 맞는 것만 한다면 아무거나 해도 상관은 없다.
이상으로 n+1에 대한 해결 방법을 알아봤다.