JPA의 훌륭한 점 중 하나인 Object Graph를 보장한다는 점은, 주의를 기울여 사용하지 않으면 성능에 치명적인 부하를 줍니다. JPA를 사용하면서 가장 주의를 기울여야 할 성능 이슈는 N+1 문제입니다. N+1 문제가 뭔지는 유명하니 따로 언급을 하지 않고...
N+1 문제를 해결하는 방법은 여러가지가 있습니다.
여기서 언급하고 싶은 점은 글로벌 페치 전략이 N+1 문제를 제대로 해결해 주지 않는다는 점입니다.
글로벌 페치 전략이란? 연관관계 매핑된 엔티티에 대한 로딩 전략을 글로벌하게 설정하는 것입니다.
@Entity
public class Member {
...
@OneToMany(mappedBy = "member", fetch = FetchType.EAGER)
private List<Order> orders = new ArrayList<>();
...
}
이렇게 하면 Member를 조회하는 시점에 Order 리스트도 같이 로딩되어 N+1 문제가 발생하지 않....지 않습니다.
Member 엔티티를 ID로 조회할 때는 N+1 문제가 발생하지 않지요.
EntityManager em = getEntityManager();
em.find(Member.class, memberId);
이에 대한 위 코드 실행 시 발생하는 쿼리는 다음과 같습니다.
Hibernate:
select
m1_0.id,
m1_0.member_name,
o1_0.member_id,
o1_0.id
from
members m1_0
left join
orders o1_0
on m1_0.id=o1_0.member_id
where
m1_0.id=?
한 번의 쿼리로 Member와 Order 엔티티 둘 다 가져오므로 썩 괜찮다고 볼 수 있습니다.
다음 코드가 있습니다.
EntityManager em = getEntityManager();
Member findMember = em.createQuery("SELECT m FROM Member m WHERE m.id = :id", Member.class)
.setParameter("id", member.getId())
.getSingleResult();
System.out.println(findMember.getMemberName());
findMember.getOrders()
.forEach((o) -> System.out.println(o.getId()));
그러면 쿼리는 다음과 같이 나타납니다.
Hibernate:
select
m1_0.id,
m1_0.member_name
from
members m1_0
where
m1_0.id=?
Hibernate:
select
o1_0.member_id,
o1_0.id
from
orders o1_0
where
o1_0.member_id=?
쿼리가 두 번 발생하는 것을 볼 수 있습니다. 글로벌 페치 전략을 Eager Fetch로 설정한다고 하더라도 N+1 문제가 해결되지 않습니다.
JPA가 JPQL을 분석해서 SQL을 생성할 때 글로벌 페치 전략을 참고하지 않고 오직 JPQL 자체만 사용합니다.
이 짧은 글의 결론은 이렇습니다. JPA는 개발자에게 주어진 축복임은 틀림없지만 편리한 만큼 고려해야 할 사항이 많습니다. JPA를 제대로 활용하지 못한다면 JPA는 재앙에 지나지 않습니다. 특히 JPA를 3-layer 아키텍처에서 활용한다든지, 헥사고날 아키텍처에서 활용한다든지 할 때 JPA를 비즈니스 로직으로부터 분리시켜야 한다는 아키텍처 철학이 JPA 활용 난이도를 더욱 가중시킬 수 있습니다. 그래서 JPA를 수박 겉 핥기식이 아닌 제대로 알고, 멋지게 활용할 수 있는 방법에 대해서 끊임없이 고민해야 합니다.