ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • N + 1 문제
    Spring/JPA 2023. 8. 6. 16:55
    728x90

    JPA는 테이블 간의 연관관계를 Mapping 해주는 ORM기술의 하나이다.

     

    JPA는 Java의 객체지향적 관점과 DB의 테이블 참조관계 사이의 불일치하는 부분을 해소하면서 원하는 데이터를 가지고 올 수 있게 해준다.

     

    Spring data jpa를 사용하면서 맞닥드릴 수 있는 N + 1 문제에 대해서 설명하고 해결 방법을 알아보자.

     

     

    Spring Data JPA

    JPA는 성능 최적화를 위해 지연로딩이라는 기술을 활용한다.

    지연로딩이란, 관련이 있는 엔티티를 필요한 시점에 로딩하는 것을 말한다.

     

    예를 들어 Member entity와 Team entity가 있다고 가정해보자.

    Team 에는 여러명의 Member가 소속될 수 있다면, Member와 Team Entity는 N : 1 관계가 된다.

     

    Member entity의 Team entity mapping

    기본적으로 @ManyToOne의 로딩전략은 Member를 조회할 때, Member 뿐만 아니라 관련있는 팀을 조회하게 된다.

    만약 Member에 대한 속성만 필요한 상황이라면, 불필요한 Team에 대한 정보를 가지고 올 필요가 없게 된다.

     

    JPA는 이를 지연로딩을 통해 해결한다.

     

    지연 로딩의 장단점

    • 장점: 초기 로딩 시점에서 필요하지 않은 데이터를 로드하지 않으므로 성능이 향상될 수 있습니다.
    • 단점: 연관된 엔터티에 처음 접근할 때 추가 쿼리가 발생하므로, 예상하지 못한 성능 문제가 발생할 수도 있습니다

     

     

    지연로딩을 사용하면, 기본적으로 연관된 Team Entity는 가짜 객체 (Proxy)로 만들어진다.

     

    @Test
        public void findMemberLazy() throws Exception {
            // given
            //member1 -> team1
            //member2 -> team2
            Team teamA = new Team("teamA");
            Team teamB = new Team("teamB");
            teamRepository.save(teamA);
            teamRepository.save(teamB);
            memberRepository.save(new Member("member1", 10, teamA));
            memberRepository.save(new Member("member2", 10, teamB));
    
            em.flush();
            em.clear();
    
            // when
            List<Member> members = memberRepository.findAll();
    
            for (Member member : members) {
                System.out.println("member.getUsername() = " + member.getUsername());
                System.out.println("member.getTeam().getClass() = " + member.getTeam().getClass()); // class study.datajpa.entity.Team$HibernateProxy$eGwQ8bQC
                /* 지연로딩을 위해 가짜 객체(proxy)를 집어넣는다. 그리고 필요한 때에 다시 select 를 날린다.(프록시 초기화라고 말함.) */
                System.out.println("member.getTeam().getName() = " + member.getTeam().getName());
                /*
                 그래서 team 을 이제야 찾으니, team query 가 2번 날라간 것이다.
                    이런 문제를 (N + 1)문제(select member 1, select team N 개...)
    
                 다시 돌렸을 때는 왜 join을 날리는가? > findAll()을 오버라이딩해서 @EntityGraph를 넣었기 때문
                 <MemberRepository 참고>
                */
            }
    
            // then
        }

     System.out.println("member.getTeam().getClass() = " + member.getTeam().getClass());

     // class study.datajpa.entity.Team$HibernateProxy$eGwQ8bQC

     

     

     

    여기서 만약 Member만 조회하는 게 아니라 Team을 조회하게 된다면?

    위의 테스트 코드에 주석으로 써놓기도 했지만, 1번의 Member 조회 쿼리와 각각의 멤버와 연관된 Team에 대한 쿼리(N개)의 쿼리가 수행되게 된다.

     

     

     

    해결방법

    1. fetch join

    2. EntityGraph

     

     

    1.fetch join

    member를 조회하는 쿼리를 발생시킬 때, fetch라는 키워드를 통해 team에 대한 정보까지 한번에 select 하는 방법이다.

    @Test
        public void findMemberFetchJoin() throws Exception {
            // given
            //member1 -> team1
            //member2 -> team2
            Team teamA = new Team("teamA");
            Team teamB = new Team("teamB");
            teamRepository.save(teamA);
            teamRepository.save(teamB);
            memberRepository.save(new Member("member1", 10, teamA));
            memberRepository.save(new Member("member2", 10, teamB));
    
            em.flush();
            em.clear();
    
            // when
            List<Member> members = memberRepository.findMemberFetchJoin();
            /*
            * N + 1 문제 해결 방법!
            *       fetch 조인 사용!
            *       여기선 가짜 Team 객체(proxy)를 사용하지 않고 처음 한번 join문을 날린다.
            *       fetch join이란? > 연관관계에 있는 것을 fetch join한 객체를 한방에 select 한다.
            * */
            for (Member member : members) {
                System.out.println("member.getUsername() = " + member.getUsername());
                System.out.println("member.getTeam().getClass() = " + member.getTeam().getClass());
                System.out.println("member.getTeam().getName() = " + member.getTeam().getName());
            }
    
            // then
        }

     

     

    2. EntityGraph
    연관된 엔티티들을 SQL 한번에 조회하는 방법

    @Test
        public void findEntityGraph() throws Exception {
            // given
            //member1 -> team1
            //member2 -> team2
            Team teamA = new Team("teamA");
            Team teamB = new Team("teamB");
            teamRepository.save(teamA);
            teamRepository.save(teamB);
            memberRepository.save(new Member("member1", 10, teamA));
            memberRepository.save(new Member("member1", 10, teamB));
    
            em.flush();
            em.clear();
    
            // when
            List<Member> members = memberRepository.findMemberEntityGraph();
    
            for (Member member : members) {
                System.out.println("member.getUsername() = " + member.getUsername());
                System.out.println("member.getTeam().getClass() = " + member.getTeam().getClass());
                System.out.println("member.getTeam().getName() = " + member.getTeam().getName());
    
            }
    
            // then
        }

     

     

    여기서 내가 아직 정복하지 못한 개념은 Proxy 의 적용 방법과 구조이다.

    이부분은 나중에 찾아보기로...ㅋ

Designed by Tistory.