자바 ORM 표준 JPA 프로그래밍 - 기본편 - 섹션 11. 객객체지향 쿼리 언어2 - 중급 문법 (JPQL)
CS/김영한 스프링 강의

자바 ORM 표준 JPA 프로그래밍 - 기본편 - 섹션 11. 객객체지향 쿼리 언어2 - 중급 문법 (JPQL)

 

말이 경로표현식이지 어렵게 용어를 정의해서 설명하는데 그냥 점(.) 찍고 원하는거 불러오는거다. 근데 그 불러오는게 단순 필드값인지, 엔티티인지, 컬렉션인지에 따라 내부 동작 방식이 다르기 때문에 각각 구분해서 알아야 해서 이름 붙이고 구분 하는 거다.

 

여기서 경로라고 표현한 이유가 나오는데 자바 객체이기 때문에 불러오는 걸 타고타고 쭉 갈수 있다는 것. db는 이걸 구현하기 위해 join을 하고 안에서 난리가 난다. 그래서 최적화 할 때 알아두어야된다.

 

 

 

난 단순히 자바 객체 안에 있는걸 불러오려고 했을 뿐인데 안에선 나도 모르게 join이 실행된다. 그래서 묵시적 join인거고.. 그래서 잘 알고 써야 한다. 사실 묵시적 join이 나오도록 코드 짜지 말자. 진짜 나중에 sql문 튜닝할 때 힘들어진다...

 

members로 하면 저게 컬렉션이라 List에 대한 함수 및 속성들만 쓸 수 있음.

그래서 명시적으로 join하면 내가 더 정의할 수 있기 때문에 엔티티로 더 탐색 가능하다.

 

 

실전에선 무조건 명시적 써라. 안그럼 너무 헷갈린다.

 

 

페치 조인. 실무에서 엄청엄청엄청 중요하다고 함. sql에 있는건 아니고 jpql에서 성능 최적화를 위해 제공하는 기능이다.

일단 fetch를 사용하지 않는 상태에서 엔티티 안에 속성으로 정의된 다른 엔티티를 불러오는걸 해보자. 지금까지 해봤던거임.

            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("회원1");
            member1.setTeam(teamA);
            em.persist(member1);

            Member member2 = new Member();
            member2.setUsername("회원2");
            member2.setTeam(teamA);
            em.persist(member2);

            Member member3 = new Member();
            member3.setUsername("회원3");
            member3.setTeam(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.getUsername() + ", " + member.getTeam().getName());
                // 회원1, 팀A(SQL)
                // 회원2, 팀A(1차 캐시)
                // 회원3, 팀B(SQL)

                // 회원 100명 -> N + 1 문제
            }

보면 teamA, teamB, member1, member2, member3가 있는데 fetchtype이 lazy이기 때문에 처음에 member를 불러와도 안에 있는 속성 엔티티도 같이 join해서 불러오는게 아니라 일단 member만 불러온다. 그 뒤 안에 있는 속성 엔티티 team을 부르면 그제서야 where로 불러온다. 이걸로 처음 teamA를 불러오면 영속성 컨텍스트에 저장되어서 한번 더 db에 가지 않는 것이며, 그 다음 처음 불러오는 team이 보이면 또 그제서야 불러와서 반환하고 저장하는 것. 

 

사실 이게 의도한 건 맞지만 lazy로 설정하면서도 한번에 불러오는게 필요할 때가 있다. 그래서 fetch를 사용하는 것.

            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("회원1");
            member1.setTeam(teamA);
            em.persist(member1);

            Member member2 = new Member();
            member2.setUsername("회원2");
            member2.setTeam(teamA);
            em.persist(member2);

            Member member3 = new Member();
            member3.setUsername("회원3");
            member3.setTeam(teamB);
            em.persist(member3);


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

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

            List<Member> result = em.createQuery(query, Member.class)
                    .getResultList();

            for (Member member : result) {
                System.out.println("member = " + member.getUsername() + ", " + member.getTeam().getName());
                // 회원1, 팀A(SQL)
                // 회원2, 팀A(1차 캐시)
                // 회원3, 팀B(SQL)

                // 회원 100명 -> N + 1 문제
            }

fetch를 사용하면 처음부터 join을 통해 모든 걸 불러들여 다시 db에 갈 일 없이 잘 된다.

 

그 다음은 엔티티 컬렉션일때. 데이터가 늘어나는건 나중에 설명하고 일단 보자.

            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("회원1");
            member1.setTeam(teamA);
            em.persist(member1);

            Member member2 = new Member();
            member2.setUsername("회원2");
            member2.setTeam(teamA);
            em.persist(member2);

            Member member3 = new Member();
            member3.setUsername("회원3");
            member3.setTeam(teamB);
            em.persist(member3);


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

            String query = "select t from Team t join fetch t.members";

            List<Team> result = em.createQuery(query, Team.class)
                    .getResultList();
                          
            System.out.println("result.size() = " + result.size());

            for (Team team : result) {
                System.out.println("team = " + team.getName() + "|members=" + team.getMembers().size());
                for (Member member : team.getMembers()) {
                    System.out.println("-> member = " + member);
                }
            }

jpa 엔티티 자바 객체를 다루고 있는 우리가 db 안의 내용이 어떤지 상관 없이 자바 객체처럼 다루는게 목적이기 때문에 jpa가 그에 맞춰 sql문을 만들어 보낸다. 이를 수행하기 위해 각 member에 따라 분명 select team 해서 존재하고 있는 team은 2개여야 하는데 3개로 늘어나서 나오는 걸 볼 수 있다. 그래서 distinct를 써서 team이 원래 나와야 하는 갯수만큼 중복을 줄여야 한다.

근데 문제는 사실 db 자체는 중복이 없어 잘못이 없다는 것. db에서 distinct로 없애는 조건은 모든 column의 값들이 같아야 하는데 실제로 join해서 나열해보면 member id가 다르거든. 그래서 jpa 자체에서 같은 식별자를 가진 엔티티에 distint를 한번 더 해준다.

 

일대다이면 데이터가 늘어날 수 있다. 다대일이면 뭐 상관없고.

 

결국 fetch join은 즉시로딩으로 한번에 쫙 가져오는 것.

 

 

페치 조인의 특징과 한계점들

별칭은 가능하긴 한데 쓰지말자. 이유는 이게 결과가 체인처럼 쭉쭉 나가는데 앞에서 나온 결과 가지고 그대로 뒤에서 하고 하다보면 내가 db를 하는건지 뭘 하는건지 모르게 된다.

 

컬렉션을 패치 조인하면 페이징 할 수 없다는 말은 일대다에서 못한다는 말임. 가능은 한데 아까 결과가 뻥튀기 된 걸 봤을거다. 그래서 db 결과가 어떻게 될지 몰라 뻥튀기가 되든 말든 모든 결과를 불러온 뒤 사용자가 정의한 페이지를 가져오는 것. 그래서 가능은 한데 메모리에 매우 안좋다고 에러가 뜬다.

 

해결방법은 일대다를 다대일로 바꾸거나 batch size를 쓰거나 애플리케이션에서 처리하기가 있다.

특히 batch size같은 경우는 알아두면 나중에 성능 향상에 진짜 도움 많이 된다. 실무에서 유용하게 쓰일 수 있으니 알아보자.

일반적으로 불러올 때 과정을 보자.

team 안의 member 컬렉션을 불러올 때 lazy로 설정했으므로 일단 team만 불러오고 요청하면 그제서야 where를 사용해서 불러와 보여주고 영속성 컨텍스트에 저장한다. 그래서 재활용해서 한번 쓰고 그 다음은 영속성 컨텍스트에 없는거라 다시 where로 불러온다.

근데 batch size를 쓰면 불러오는 녀석의 뒤까지 미리 한번에 불러온다.

보면 where = 로 불러오는게 아니라 in으로 한번에 불러오는 걸 볼 수 있다. 그래서 한번에 영속성 컨텍스트에 저장해서 이걸 재활용하여 sql문 횟수를 줄인것. 여기서야 양이 별로 안되서 체감이 안되지 막 수백개씩 있어서 일일이 보내다가 한번에 보내는거 보면 몇 십배로 체감 확 된다.

batch size 설정은 저렇게 속성에다가 정의해도 되지만 글로벌하게 하는걸 추천. 그냥 기본 옵션처럼 한다고 함.

 

 

싱글 테이블에 사용하는 특이 문법. 뽑아내길 원하는 타입을 직접 지정한다.

 

 

엔티티 자체를 직접 사용할 수도 있는데 무슨말이냐면 그냥 sql문에는 해당 모델 자체를 사용하는건 없고 식별자를 통해 count 같은걸 계산하지만 jpa는 자바 객체기 때문에 걍 사용한다. 어차피 식별자로써 변환되서 최종 결과는 똑같음.

 

named query로써 사전에 미리 정의하고 가져다 쓸 수 있는데, 이걸 하면 서버 애플리케이션이 실행되기 전에 미리 한번 파싱 해봐서 문법 문제가 없는지 사전 점건 해주는 것이 가장 큰 이점이다. 여기서 오류가 떠야 실제 돌아갈 때 오류가 안뜬다.

jpa에서도 권장한다.

https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.query-methods.at-query

 

벌크 연산은 update나 delete를 한줄 씩 하는걸 제외한 것들을 말하는 것. 그냥 모든애들 전부 set으로 싹 다 바꾸고 하는것들.

이런 벌크 연산을 쓸 땐 얘를 맨 처음 먼저 실행하거나, 실행 했다면 전에 무언가를 해놔서 영속성 컨텍스트에 들어있는 것들을 초기화 해야 한다. 왜냐하면 sql문을 이미 불러오는 거라 flush는 벌크 연산 실행 전에 알아서 하는데 flush만 하지 clear는 하지 않아서 트랜잭션이 끝날 때 영속성 컨텍스트에 있는게 자동 실행되어서 결국 벌크 연산 한건 사라지기 때문.

사실 벌크 연산은 모든걸 한번에 바꾸기 때문에 이 연산 자체를 조심해야 한다.