백엔드/JPA

[JPA] N+1 문제를 해결하기 위한 fetch

happy_life 2023. 3. 6. 20:50

오늘 포스팅에서는 fetch = Lazy에서 발생할 수 있는 N+1 문제를 해결하기 위해 등장한 fetch join의 장점과 한계를 정리해보는 시간을 가지겠습니다.

 

 

1. JPA에서 N+1 문제의 발생

테이블

 

코드

        Team teamA = new Team();
            teamA.setName("teamA");
            em.persist(teamA);

            Team teamB = new Team();
            teamB.setName("teamB");
            em.persist(teamB);

            Member member1 = new Member();
            member1.setUsername("member1");
            member1.changeTeam(teamA);

            em.persist(member1);

            Member member2 = new Member();
            member2.setUsername("member2");
            member2.changeTeam(teamB);

            em.persist(member2);

            Member member3 = new Member();
            member3.setUsername("member3");
            member3.changeTeam(teamB);

            em.persist(member3);

            em.flush();
            em.clear();


            String query = "select m from Member m";

            List<Member> result = em.createQuery(query, Member.class).getResultList();
            for (Member member : result) {
                System.out.println("member = " + member + ", " + member.getTeam().getName());
            }

위와 같은 select m from Member m을 하고 team의 Name을 찾기 위해서는 몇번의 쿼리가 나갈까요? 정답은 총 3개입니다. (2 + 1)

 

쿼리 총 3개

 

 

fetch 전략이 Lazy로 되어있기 때문에 getName()을 호출하는 시점에 다시 DB에서 값을 가져와야합니다.  

 

1. member1의 team 이름을 모르므로 select를 통해 teamA를 가져옵니다.

2. member2의 team 이름을 모르므로 select를 통해 teamB를 가져옵니다.

3. member3의 team 이름을 모르지만 teamB가 1차캐시(영속성 컨텍스트)에 있기 때문에 select 쿼리문이 나가지 않습니다.

 

 

 

2. JPA에서 N+1 문제 해결 by fetch join

이런 문제를 fetch join을 통해 해결할 수 있습니다.

String query = "select m from Member m join fetch m.team";

 

fetch join

 

명시적으로 team의 정보를 즉시 가져온다는 것을 의미하므로 fetch join은 마치 eager 전략처럼 사용되나, 개발자가 명시적으로 원하는 부분만 그때그때 즉시 가져온다는 특징이 있습니다.

 

 

 

3. JPA에서 fetch join의 한계

 

1. fetch 조인 대상에는 별칭을 줄 수 없습니다. 하이버네이트는 가능하지만, 가급적 사용해서는 안됩니다. 별칭을 사용해 where문으로 부분적으로만 데이터를 가져올 우려가 있기 때문입니다.

별칭

왜냐하면 JPA에서 설계 사상자체가 객체 그래프를 탐색한다는 것은 모든 데이터를 가져온다는 것을 가정하고 설계가 되어있습니다. 만약에 부분적으로만 가져오게 된다면 cascade 같이 이상한 옵션들이 막 껴있는 경우 등에서 문제가 발생할 수 있습니다. (정합성 이슈)

 

2. 둘 이상의 컬렉션은 페치 조인 할 수 없다.

 

3. 컬렉션을 페치 조인하면 페이징 API(setFirstResult, setMaxResult)를 사용할 수 없다.

- 일대다 fetch join하면 데이터가 뻥튀기가 되어서 매우 위험하다.

- 일대다, 다대일 같은 단일 값 연관 필드들은 fetch join 해도 페이징 가능