✏️ [JPQL] Join(조인)

박상민·2023년 10월 29일
0

JPA

목록 보기
17/24
post-thumbnail

⭐️ Join

JPQL의 조인은 SQL 조인과 실행되는 것은 똑같다.
차이가 뭐냐면 entity 중심으로 동작한다는 것이다. 조금 객체 스타일로 조인 문법이 나간다.

내부 조인:

  • SELECT m FROM Member m [INNER] JOIN m.team t

외부 조인:

  • SELECT m FROM Member m LEFT [OUTER] JOIN m.team t

세타 조인:

  • Select count(m) from Member m, Team t where m.username = t.name

내부, 외부, 세타 조인 한줄 설명
INNER JOIN(내부 조인): 두 테이블을 조인할 때, 두 테이블에 모두 지정한 열의 데이터가 있어야 한다.
OUTER JOIN(외부 조인): 두 테이블을 조인할 때, 1개의 테이블에만 데이터가 있어도 결과가 나온다.
THETA JOIN(세타조인):: 연관관계 상관 없이 유저 명과 팀의 이름이 같은 경우 찾아라 라는 쿼리 날릴 수 있다.

📌 INNER JOIN(내부 조인)

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

Member member = new Member();
member.setUsername("member1");
member.setAge(10);

member.setTeam(team);

em.persist(member);

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

String query = "select m from Member m inner join m.team t";
List<Member> result = em.createQuery(query, Member.class)
       .getResultList();
tx.commit()

내부 조인 시 team이 존재하는 Member 정보만을 리턴한다.

실행 쿼리

Hibernate: 
    /* select
        m 
    from
        Member m 
    inner join
        m.team t */ select
            member0_.id as id1_0_,
            member0_.age as age2_0_,
            member0_.TEAM_ID as team_id4_0_,
            member0_.username as username3_0_ 
        from
            Member member0_ 
        inner join
            Team team1_ 
                on member0_.TEAM_ID=team1_.id

📌 OUTER JOIN(외부 조인)

String query = "select m from Member m left outer join m.team t";
List<Member> result = em.createQuery(query, Member.class)
       .getResultList();
tx.commit()

외부 조인 시 team이 존재하지 않는 Member 정보도 함께 리턴한다.
이때 team이 존재하지 않다면 null로 나온다.

실행 쿼리

Hibernate: 
    /* select
        m 
    from
        Member m 
    left outer join
        m.team t */ select
            member0_.id as id1_0_,
            member0_.age as age2_0_,
            member0_.TEAM_ID as team_id4_0_,
            member0_.username as username3_0_ 
        from
            Member member0_ 
        left outer join
            Team team1_ 
                on member0_.TEAM_ID=team1_.id

📌 THETA JOIN(세타조인)

String query = "select m from Member m, Team t  where m.username = t.name";
List<Member> result = em.createQuery(query, Member.class)
              .getResultList();

두 테이블을 크로스 조인 후 조건에 해당하는 값만을 조회한다.

실행 쿼리

Hibernate: 
    /* select
        m 
    from
        Member m,
        Team t  
    where
        m.username = t.name */ select
            member0_.id as id1_0_,
            member0_.age as age2_0_,
            member0_.TEAM_ID as team_id4_0_,
            member0_.username as username3_0_ 
        from
            Member member0_ cross 
        join
            Team team1_ 
        where
            member0_.username=team1_.name

참고
하이버네이트 5.1부터 세타 조인도 외부 조인이 가능!

📌 조인 - ON 절

ON절을 활용한 조인(JPA 2.1부터 지원)

  • 조인 대상 필터링
  • 연관관계 없는 엔티티 외부 조인(하이버네이트 5.1부터)

조인 대상 필터링

  • 예시) 회원과 팀을 조인하면서, 팀 이름이 A인 팀만 조인
JPQL:
SELECT m, t FROM Member m LEFT JOIN m.team t on t.name = 'A'

SQL:
SELECT m.*, t.* FROM
Member m LEFT JOIN Team t ON m.TEAM_ID=t.id and t.name='A'

연관관계 없는 엔티티 외부 조인

  • 예시) 회원의 이름과 팀의 이름이 같은 대상 외부 조인
JPQL:
SELECT m, t FROM
Member m LEFT JOIN Team t on m.username = t.name
SQL:
SELECT m.*, t.* FROM
Member m LEFT JOIN Team t ON m.username = t.name

연관관계가 전혀 없는 애를 LeftJoin 하고 싶다면 ON절에다가 명시해주면 된다.

📌 FetchJoin

페치 조인(Fetch Join)

  • SQL 조인 종류X
  • JPQL에서 성능 최적화를 위해 제공하는 기능
  • 연관된 엔티티나 컬렉션을 SQL 한 번에 함께 조회하는 기능
  • join fetch 명령어 사용

페치 조인은 현업에서 굉장히 많이 쓰인다. fetchType을 LAZY로 다 세팅 해놓고, 쿼리 튜닝할때 한번에 조회가 필요한 경우 페치 조인을 사용한다.

  • 엔티티 객체 그래프를 한번에 조회하는 방법이다.

  • 회원을 조회하면서 연관된 팀도 함께 조회(SQL 한 번에)

    • JPQL
      select m from Member m join fetch m.team

    • SQL

      SELECT M.*, T.*
      FROM MEMBER T
      INNER JOIN TEAM T ON M.TEAM_ID = T.ID
  • 최근에(jpa2.1)는 페치 조인 말고 엔티티 그래프라는 기능이 있다.

  • 페치 조인 예시)

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

List<Member> members = em.createQuery(jpql, Member.class).getResultList();

for (Member member : members) {
   //페치 조인으로 회원과 팀을 함께 조회해서 지연 로딩이 발생하지 않는다.
   System.out.println("username = " + member.getUsername() + ","
                   + "teamname = " + member.getTeam().name());
}
-----------------------------------------------------
Hibernate: 
    /* select
        m 
    from
        Member m 
    join
        fetch m.team */ select
            member0_.id as id1_0_0_,
            team1_.id as id1_3_1_,
            member0_.age as age2_0_0_,
            member0_.TEAM_ID as team_id5_0_0_,
            member0_.type as type3_0_0_,
            member0_.username as username4_0_0_,
            team1_.name as name2_3_1_ 
        from
            Member member0_ 
        inner join
            Team team1_ 
                on member0_.TEAM_ID=team1_.id
                
member = 회원 1, teamA
member = 회원 2, teamA
member = 회원 3, teamB
  • 현업에서 많이 쓰이는 이유는, 리스트를 쭉 뿌릴때. LAZY(지연로딩)로 가게 되면 리스트에서 반복문으로 정보 받아올 때마다 DB에 쿼리가 나간다. 이게 JPA N+1 문제이다. 성능상 좋지 않다.
  • 리스트가 10명이면 10명의 리스트를 가져오는 쿼리 한방 나가는데, 세부 조회를 할 때마다(10번) Lazy 로딩 되므로 쿼리가 총 11번 나가게 된다.

✔︎ 페치 조인과 일반 조인의 차이

  • 일반 조인 실행시 연관된 엔티티를 함께 조회하지 않음.

일반 조인의 경우

String query = "select t from Team t join t.members m";
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);
    }
}

----------------------------------------------
Hibernate: 
    /* select
        t 
    from
        Team t 
    join
        t.members m */ select
            team0_.id as id1_3_,
            team0_.name as name2_3_ 
        from
            Team team0_ 
        inner join
            Member members1_ 
                on team0_.id=members1_.TEAM_ID
result.size() = 3

일반 조인의 경우 Select 절에서 Team만 가지고 온다. 일반 조인은 조인문만 SQL에서 실행되고 실제 데이터를 가져오는 것은 실제 사용될 때마다 가져오게 된다.(Lazy로 설정했기 때문에)
때문에 데이터가 뻥튀기 돼서 result.size() = 3처럼 result의 개수가 3개로 나온 것을 알 수 있다.
문제는 뭐냐면 루프를 돌릴 때 켈렉션인 members가 아직 초기화가 안됐다는 것이다.

Hibernate: 
    select
        members0_.TEAM_ID as team_id5_0_0_,
        members0_.id as id1_0_0_,
        members0_.id as id1_0_1_,
        members0_.age as age2_0_1_,
        members0_.TEAM_ID as team_id5_0_1_,
        members0_.type as type3_0_1_,
        members0_.username as username4_0_1_ 
    from
        Member members0_ 
    where
        members0_.TEAM_ID=?
team = teamA|members=2
-> member = Member{id=3, username='회원 1', age=0}
-> member = Member{id=4, username='회원 2', age=0}
team = teamA|members=2
-> member = Member{id=3, username='회원 1', age=0}
-> member = Member{id=4, username='회원 2', age=0}
Hibernate: 
    select
        members0_.TEAM_ID as team_id5_0_0_,
        members0_.id as id1_0_0_,
        members0_.id as id1_0_1_,
        members0_.age as age2_0_1_,
        members0_.TEAM_ID as team_id5_0_1_,
        members0_.type as type3_0_1_,
        members0_.username as username4_0_1_ 
    from
        Member members0_ 
    where
        members0_.TEAM_ID=?
team = teamB|members=1
-> member = Member{id=5, username='회원 3', age=0}

일반 조인은 Select 절에서 Team만 가져오고 members는 초기화를 안했기 때문에 members가 사용되는 시점에 쿼리가 계속 나간다.

페치 조인

String query = "select t from Team t join fecth 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);
    }
}
------------------------------------------------------
Hibernate: 
    /* select
        t 
    from
        Team t 
    join
        fetch t.members m */ select
            team0_.id as id1_3_0_,
            members1_.id as id1_0_1_,
            team0_.name as name2_3_0_,
            members1_.age as age2_0_1_,
            members1_.TEAM_ID as team_id5_0_1_,
            members1_.type as type3_0_1_,
            members1_.username as username4_0_1_,
            members1_.TEAM_ID as team_id5_0_0__,
            members1_.id as id1_0_0__ 
        from
            Team team0_ 
        inner join
            Member members1_ 
                on team0_.id=members1_.TEAM_ID
result.size() = 3
team = teamA|members=2
-> member = Member{id=3, username='회원 1', age=0}
-> member = Member{id=4, username='회원 2', age=0}
team = teamA|members=2
-> member = Member{id=3, username='회원 1', age=0}
-> member = Member{id=4, username='회원 2', age=0}
team = teamB|members=1
-> member = Member{id=5, username='회원 3', age=0}

페치 조인은 Selcet 절에서 데이터를 다 불러온다. 때문에 그 밑에서 지연 로딩이 발생하지 않는다.

페치 조인과 일반 조인의 차이

  • JPQL은 결과를 반환할 때 연관관계 고려X
  • 단지 SELECT 절에 지정한 엔티티만 조회할 뿐
  • 여기서는 팀 엔티티만 조회하고, 회원 엔티티는 조회X
  • 페치 조인을 사용할 때만 연관된 엔티티도 함께 조회(즉시 로딩)
  • 페치 조인은 객체 그래프를 SQL 한번에 조회하는 개념

✔︎ 페치 조인의 특징과 한계

한계

  • 페치 조인 대상에는 별칭을 줄 수 없다.
    • String query = "select t from Team t join fetch t.members as m";
    • 하이버네이트는 가능, 가급적 사용X
  • 둘 이상의 컬렉션은 페치 조인 할 수 없다.
  • 컬렉션을 페치 조인하면 페이징 API(setFirstResult, setMaxResults)를 사용할 수 없다.
    • 일대일, 다대일 같은 단일 값 연관 필드들은 페치 조인해도 페이징 가능
    • 하이버네이트는 경고 로그를 남기고 메모리에서 페이징(매우 위험)

특징

  • 연관된 엔티티들을 SQL 한 번으로 조회 - 성능 최적화
  • 엔티티에 직접 적용하는 글로벙 로딩 전략보다 우선함
    • @OneToMany(fetch = FetchType.Lazy) //글로벌 로딩 전략
  • 실무에서 글로벌 로딩 전략은 모두 지연 로딩
  • 최적화가 필요한 곳은 페치 조인 적용(N+1 문제가 발생하는 곳)

정리
모든 것은 페치 조인으로 해결할 수는 없다.
페치 조인은 객체 그래프를 유지할 때 사용하면 효과적이다.
여러 테이블으 ㄹ조인해서 엔티티가 가진 모양이 아닌 전혀 다른 결과를 내야 하면, 페치 조인 보다는 일반 조인을 사용하고 필요한 데이터들만 조회해서 DTO로 반환하는 것이 효과적이다.


출처
자바 ORM 표준 JPA 프로그래밍 강의
게시글 속 자료는 모두 위 강의 속 자료를 사용했습니다.
JPA N+1 이슈는 무엇이고, 해결책은 무엇인가요?

profile
스프링 백엔드를 공부중인 대학생입니다!

0개의 댓글