maeng0830
뇌 채우기 운동
maeng0830
전체 방문자
오늘
어제
  • maeng0830-note (85)
    • 자바 (3)
    • 스프링 (39)
      • Core (21)
      • DB (16)
      • Security (2)
      • Test (0)
    • 자료구조 & 알고리즘 (19)
      • 자료구조 (12)
      • 알고리즘 (7)
    • 다른 개발 도구들 (4)
      • Git&Github (1)
      • Redis (3)
    • 프로젝트 (9)
      • Album (9)
    • CS (10)
      • 운영체제 (5)
      • 데이터베이스 (5)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • JPQL
  • spring security
  • JPA
  • 트랜잭션
  • 자료구조

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
maeng0830

뇌 채우기 운동

데이터 접근 기술_JPA_JPQL join, fetch join
스프링/DB

데이터 접근 기술_JPA_JPQL join, fetch join

2023. 3. 20. 23:42

이번 글에서는 JPQL의 join과 fetch join에 대해 알아보자.

 

아래는 이번 글에서 사용할 엔티티들이다.

  • Member(N)와 TeamForEager(1)는 다대일 단방향 연관관계를 가진다.
  • Member(N)와 TeamForLazy(1)는 다대일 및 일대다 양방향 연관관계를 가진다.
  • TeamForEager와 TeamForLazy는 FetchType.EAGER와 FetchType.Lazy에 따른 동작 차이를 확인하기 위해 구분된 엔티티이며, 도메인 측면의 의미는 동일하다.
@Getter
@NoArgsConstructor
@Entity
public class Member {

	@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;

	private String username;

	private Integer age;

	@ManyToOne(fetch = FetchType.EAGER)
	private TeamForEager teamForEager;

	@ManyToOne(fetch = FetchType.LAZY)
	private TeamForLazy teamForLazy;

	@Builder
	public Member(Long id, String username, Integer age, TeamForEager teamForEager,
				  TeamForLazy teamForLazy) {
		this.id = id;
		this.username = username;
		this.age = age;
		this.teamForEager = teamForEager;
		this.teamForLazy = teamForLazy;
	}
}

///////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////
@Getter
@NoArgsConstructor
@Entity
public class TeamForEager {

	@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;

	private String name;

	@Builder
	public TeamForEager(Long id, String name) {
		this.id = id;
		this.name = name;
	}
}

///////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////
@Getter
@NoArgsConstructor
@Entity
public class TeamForLazy {

	@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;

	private String name;

	@OneToMany(mappedBy = "teamForLazy")
	private List<Member> members = new ArrayList<>();

	@Builder
	public TeamForLazy(Long id, String name, List<Member> members) {
		this.id = id;
		this.name = name;
		this.members = members;
	}
}

 

JOIN

JPQL에서 내부 조인과 외부 조인은 다음과 같이 작성할 수 있다.

[...]는 생략 가능한 키워드이다.

  • 내부 조인: select m from Member m [inner] join m.teamXX t
  • 외부 조인: select m from m left [outer] join m.teamXX t

 

1. 글로벌 로딩 전략 EAGER

  • 사용된 JPQL은 "select m from Member m join m.teamForEager"이다.
  • Member와 Team에 대한 select ~ join 쿼리가 실행된다. 아직 Member만 실제 객체로 조회된 상황이다. Member 객체의  TeamForEager 참조 객체는 프록시 객체이다.
  • 즉시 모든 Member 객체의 TeamForEager 참조 객체에 대한 select 쿼리가 각각 실행된다.

2. 글로벌 로딩 전략 LAZY

  • 사용된 JPQL은 "select m from Member m join m.teamForLazy"이다.
  • Member와 Team에 대한 select ~ join이 실행된다. 아직 Member만 실제 객체로 조회된 상황이다. Member 객체의  TeamForLazy 참조 객체는 프록시 객체이다.
  • 각 Member 객체의 TeamForLazy 참조 객체의 데이터에 접근 할 때마다, 각각 select 쿼리가 실행된다.
@Transactional
@SpringBootTest
public class Join_FetchTypeTest {

	@Autowired
	private MemberRepository memberRepository;
	@Autowired
	private TeamRepository teamRepository;

	@PersistenceContext
	private EntityManager em;

	@DisplayName("NO join + FetchType.EAGER")
	@Test
	void membersWithEager() {
		TeamForEager teamA = TeamForEager.builder()
				.name("teamA")
				.build();
		TeamForEager teamB = TeamForEager.builder()
				.name("teamB")
				.build();
		teamRepository.saveTE(teamA);
		teamRepository.saveTE(teamB);

		Member memberA = Member.builder()
				.username("userA")
				.age(10)
				.teamForEager(teamA)
				.build();
		Member memberB = Member.builder()
				.username("userB")
				.age(20)
				.teamForEager(teamB)
				.build();
		memberRepository.save(memberA);
		memberRepository.save(memberB);

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

		List<Member> members = memberRepository.membersWithEager();
		/*
		 * join절 없이, member 엔티티들을 조회한다.
		 *   SELECT m1_0.id ... FROM member m1_0
		 * 즉시 각 member 엔티티들의 teamForEager 엔티티를 각각 조회한다.
		 *   SELECT t1_0.id ... FROM team_for_eager t1_0 WHERE t1_0.id=?
		 *   SELECT t1_0.id ... FROM team_for_eager t1_0 WHERE t1_0.id=?
		 */
	}

	@DisplayName("YES join + FetchType.EAGER")
	@Test
	void membersWithEagerJoin() {
		TeamForEager teamA = TeamForEager.builder()
				.name("teamA")
				.build();
		TeamForEager teamB = TeamForEager.builder()
				.name("teamB")
				.build();
		teamRepository.saveTE(teamA);
		teamRepository.saveTE(teamB);

		Member memberA = Member.builder()
				.username("userA")
				.age(10)
				.teamForEager(teamA)
				.build();
		Member memberB = Member.builder()
				.username("userB")
				.age(20)
				.teamForEager(teamB)
				.build();
		memberRepository.save(memberA);
		memberRepository.save(memberB);

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

		List<Member> members = memberRepository.membersWithEagerJoin();
		/*
		 * join 절을 적용하여, member 엔티티들을 조회한다.
		 *   SELECT m1_0.id ... FROM member m1_0 JOIN team_for_eager t1_0 ON t1_0.id=m1_0.team_for_eager_id
		 * 즉시 각 member 엔티티들의 teamForEager 엔티티를 각각 조회한다.
		 *   SELECT t1_0.id ... FROM team_for_eager t1_0 WHERE t1_0.id=?
		 *   SELECT t1_0.id ... FROM team_for_eager t1_0 WHERE t1_0.id=?
		 */
	}

	@DisplayName("NO join + FetchType.LAZY")
	@Test
	void membersWithLazy() {
		TeamForLazy teamA = TeamForLazy.builder()
				.name("teamA")
				.build();
		TeamForLazy teamB = TeamForLazy.builder()
				.name("teamB")
				.build();
		teamRepository.saveTL(teamA);
		teamRepository.saveTL(teamB);

		Member memberA = Member.builder()
				.username("userA")
				.age(10)
				.teamForLazy(teamA)
				.build();
		Member memberB = Member.builder()
				.username("userB")
				.age(20)
				.teamForLazy(teamB)
				.build();
		memberRepository.save(memberA);
		memberRepository.save(memberB);

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

		List<Member> members = memberRepository.membersWithLazy();
		/*
		 * join절 없이, member 엔티티들을 조회한다.
		 *   SELECT m1_0.id ... FROM member m1_0
		 */


		for (Member member : members) {
			System.out.println("member.getTeamForLazy().getName() = " + member.getTeamForLazy().getName());
		}
		/*
		 * 각 Member 엔티티의 TeamForLazy 엔티티에 접근할 때마다 각각 조회한다.
		 *   member.getTeamForLazy().getName() = teamA
		 *   SELECT t1_0.id ... FROM team_for_lazy t1_0 WHERE t1_0.id=?
		 *   member.getTeamForLazy().getName() = teamB
		 *   SELECT t1_0.id ... FROM team_for_lazy t1_0 WHERE t1_0.id=?
		 */
	}

	@DisplayName("YES join + FetchType.LAZY")
	@Test
	void membersWithLazyJoin() {
		TeamForLazy teamA = TeamForLazy.builder()
				.name("teamA")
				.build();
		TeamForLazy teamB = TeamForLazy.builder()
				.name("teamB")
				.build();
		teamRepository.saveTL(teamA);
		teamRepository.saveTL(teamB);

		Member memberA = Member.builder()
				.username("userA")
				.age(10)
				.teamForLazy(teamA)
				.build();
		Member memberB = Member.builder()
				.username("userB")
				.age(20)
				.teamForLazy(teamB)
				.build();
		memberRepository.save(memberA);
		memberRepository.save(memberB);

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

		List<Member> members = memberRepository.membersWithLazyJoin();
		/*
		 * join절을 적용하여, member 엔티티들을 조회한다.
		 *   SELECT m1_0.id ... FROM member m1_0 JOIN team_for_lazy t1_0 ON t1_0.id=m1_0.team_for_lazy_id
		 */


		for (Member member : members) {
			System.out.println("member.getTeamForLazy().getName() = " + member.getTeamForLazy().getName());
		}
		/*
		 * 각 Member 엔티티의 TeamForLazy 엔티티에 접근할 때마다 각각 조회한다.
		 *   member.getTeamForLazy().getName() = teamA
		 *   SELECT t1_0.id ... FROM team_for_lazy t1_0 WHERE t1_0.id=?
		 *   member.getTeamForLazy().getName() = teamB
		 *   SELECT t1_0.id ... FROM team_for_lazy t1_0 WHERE t1_0.id=?
		 */
	}

}

 

FETCH JOIN

  • fetch join은 SQL에서 제공하는 join의 종류가 아니며, JPQL에서 데이터 조회 성능 최적화를 위해 제공하는 기능이다.
  • fetch join은 연관된 엔티티 또는 컬렉션을 한 번의 쿼리로 함께 조회하는 기능이다.
  • 그리고 JPQL을 사용할 때 발생할 수 있는 n + 1 문제의 해결 방법이기도 하다.
  • ... left [outer] / [inner] join fetch ...으로 사용 가능하다.
  • fetch join을 사용할 경우, 지연 로딩이 불가능하며 글로벌 로딩 전략이 설정되어있다 하더라도 즉시 로딩으로 실행된다.

 

엔티티(xToOne) 페치 조인

  • 사용된 JPQL은 "select m from Member m join fetch m.teamForLazy t"이다.
  • 해당 JPQL은 "select m, t from Member m join m.teamForLazy t"라는 JPQL과 동일한 의미를 가진다.   
  • 위의 두 JPQL들은 "select m.*, t.* from Member m inner join TeamForLazy t on m.TeamForLazy_id=t.id"라는 SQL로 변환되어 실행된다.

 

컬렉션(xToMany) 페치 조인

  • 사용된 JPQL은 "select t from TeamForLazy t join fetch t.members m"이다.
  • 해당 JPQL은 "select t, m from TeamForLazy t join t.members m"이라는 JPQL과 동일한 의미를 가진다.
  • 위의 두 JPQL들은 "select t.*, m.* from TeamForLazy t inner join Member m on t.ID=m.TeamForLazy_id"라는 SQL로 변환되어 실행된다.

 

  • 컬렉션 페치 조인 시에는 결과가 부풀려진다는 것을 주의하자.
    • 하나의 team은 여러 개의 member를 갖고 있을 수 있다.
    • 때문에 데이터베이스 조회 결과에서는 teamA - member1, teamA - member2처럼 1개의 team에 대해 다수의 row가 생성될 수 있다.
    • 데이터베이스는 이러한 조회 결과를 그대로 어플리케이션에게 전달하기 때문에, 각 team에 대해 1개의 객체만 필요하더라도 다수의 객체가 존재하는 문제가 발생한다.

 

  • 컬렉션 페치 조인 시 결과가 부풀려지는 문제를 해결할 때 사용하는 것이 distinct이다.
    • SQL 쿼리의 distinct는 row와 row간에 모든 컬럼 값이 동일해야 중복으로 판단하여 중복 제거를 실시하기 때문에 이 문제를 해결할 수 없다(member에 대한 데이터는 다를 것이기 때문이다).
    • 그러나 JPQL의 distinct는 그것과 더불어 어플리케이션 차원에서 데이터 조회 결과 컬렉션 내부에서 같은 식별자를 가진 엔티티 객체를 중복으로 판별하여 제거한다.
    • 스프링부트 3.0 이상은 hibernate 6.1을 사용하며, hibernate 6.0 이상은 distinct가 자동으로 적용된다.
@Transactional
@SpringBootTest
public class FetchJoinTest {

	@Autowired
	private MemberRepository memberRepository;
	@Autowired
	private TeamRepository teamRepository;

	@PersistenceContext
	private EntityManager em;

	@DisplayName("엔티티 페치 조인")
	@Test
	void membersWithFetchJoin() {
		TeamForLazy teamA = TeamForLazy.builder()
				.name("teamA")
				.build();
		TeamForLazy teamB = TeamForLazy.builder()
				.name("teamB")
				.build();
		teamRepository.saveTL(teamA);
		teamRepository.saveTL(teamB);

		Member memberA = Member.builder()
				.username("userA")
				.age(10)
				.teamForLazy(teamA)
				.build();
		Member memberB = Member.builder()
				.username("userB")
				.age(20)
				.teamForLazy(teamA)
				.build();
		Member memberC = Member.builder()
				.username("userC")
				.age(10)
				.teamForLazy(teamB)
				.build();
		Member memberD = Member.builder()
				.username("userD")
				.age(20)
				.teamForLazy(teamB)
				.build();
		memberRepository.save(memberA);
		memberRepository.save(memberB);
		memberRepository.save(memberC);
		memberRepository.save(memberD);

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

		List<Member> members = memberRepository.membersWithFetchJoin();
		for (Member member : members) {
			System.out.println("member.getTeamForLazy().getName() = " + member.getTeamForLazy().getName());
		}
		/*
		 * join 절이 적용되고, fetch join이 적용된 엔티티를 select 절에 포함하여 한번에 함께 조회한다.
		 *
		 *   SELECT m1_0.id, ... , t1_0.id, ... FROM member m1_0 JOIN team_for_lazy t1_0 ON t1_0.id=m1_0.team_for_lazy_id
		 *   member.getTeamForLazy().getName() = teamA
		 *   member.getTeamForLazy().getName() = teamA
		 *   member.getTeamForLazy().getName() = teamB
		 *   member.getTeamForLazy().getName() = teamB
		 */
	}

	@DisplayName("컬렉션 페치 조인 + NO distinct")
	@Test
	void teamsWithFetchJoin() {
		TeamForLazy teamA = TeamForLazy.builder()
				.name("teamA")
				.build();
		TeamForLazy teamB = TeamForLazy.builder()
				.name("teamB")
				.build();
		teamRepository.saveTL(teamA);
		teamRepository.saveTL(teamB);

		Member memberA = Member.builder()
				.username("userA")
				.age(10)
				.teamForLazy(teamA)
				.build();
		Member memberB = Member.builder()
				.username("userB")
				.age(20)
				.teamForLazy(teamA)
				.build();
		Member memberC = Member.builder()
				.username("userC")
				.age(10)
				.teamForLazy(teamB)
				.build();
		Member memberD = Member.builder()
				.username("userD")
				.age(20)
				.teamForLazy(teamB)
				.build();
		memberRepository.save(memberA);
		memberRepository.save(memberB);
		memberRepository.save(memberC);
		memberRepository.save(memberD);

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

		List<TeamForLazy> teams = teamRepository.teamsWithFetchJoin();
		for (TeamForLazy team : teams) {
			System.out.println("team.getName() = " + team.getName() + ":" + team.getMembers().size() + "명");
			for (Member m: team.getMembers()) {
				System.out.println(" -m.getUsername() = " + m.getUsername());
			}
		}
		/*
		 * join 절이 적용되고, fetch join이 적용된 엔티티를 select 절에 포함하여 한번에 함께 조회한다.
		 * 하지만 컬렉션(일대다) fetch join 시에는 조회 결과가 부풀려진다는 것을 주의하자.
		 *
		 *   SELECT t1_0.id, ... , m1_0.id, ... FROM team_for_lazy t1_0 JOIN member m1_0 ON t1_0.id=m1_0.team_for_lazy_id
		 *   team.getName() = teamA:2명
		 *     -m.getUsername() = userA
		 *     -m.getUsername() = userB
		 *   team.getName() = teamA:2명
		 *     -m.getUsername() = userA
		 *     -m.getUsername() = userB
		 *   team.getName() = teamB:2명
		 *     -m.getUsername() = userC
		 *     -m.getUsername() = userD
		 *   team.getName() = teamB:2명
		 *     -m.getUsername() = userC
		 *     -m.getUsername() = userD
		 */
	}

	@DisplayName("컬렉션 페치 조인 + YES distinct")
	@Test
	void teamsWithFetchJoinDistinct() {
		TeamForLazy teamA = TeamForLazy.builder()
				.name("teamA")
				.build();
		TeamForLazy teamB = TeamForLazy.builder()
				.name("teamB")
				.build();
		teamRepository.saveTL(teamA);
		teamRepository.saveTL(teamB);

		Member memberA = Member.builder()
				.username("userA")
				.age(10)
				.teamForLazy(teamA)
				.build();
		Member memberB = Member.builder()
				.username("userB")
				.age(20)
				.teamForLazy(teamA)
				.build();
		Member memberC = Member.builder()
				.username("userC")
				.age(10)
				.teamForLazy(teamB)
				.build();
		Member memberD = Member.builder()
				.username("userD")
				.age(20)
				.teamForLazy(teamB)
				.build();
		memberRepository.save(memberA);
		memberRepository.save(memberB);
		memberRepository.save(memberC);
		memberRepository.save(memberD);

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

		List<TeamForLazy> teams = teamRepository.teamsWithFetchJoinDistinct();
		for (TeamForLazy team : teams) {
			System.out.println("team.getName() = " + team.getName() + ":" + team.getMembers().size() + "명");
			for (Member m: team.getMembers()) {
				System.out.println(" -m.getUsername() = " + m.getUsername());
			}
		}
		/*
		 * join 절이 적용되고, fetch join이 적용된 엔티티를 select 절에 포함하여 한번에 함께 조회한다.
		 * JPQL의 distinct 키워드를 사용하였다.
		 *   SQL의 distinct는 로우의 모든 데이터가 동일할 때 중복으로 판별하여 중복 제거한다.
		 *   JPQL의 distinct는 그것과 더불어 어플리케이션 차원에서 결과 컬렉션 내부의 같은 식별자를 가진 엔티티를 중복으로 판별하여 제거한다!
		 *
		 *   SELECT t1_0.id, ... , m1_0.id, ... FROM team_for_lazy t1_0 JOIN member m1_0 ON t1_0.id=m1_0.team_for_lazy_id
		 *   team.getName() = teamA:2명
		 *     -m.getUsername() = userA
		 *     -m.getUsername() = userB
		 *   team.getName() = teamB:2명
		 *     -m.getUsername() = userC
		 *     -m.getUsername() = userD
		 */
	}
}
    '스프링/DB' 카테고리의 다른 글
    • 데이터 접근 기술_JPA_Cascade, orphanRemoval
    • 데이터 접근 기술_JPA_Spring Data JPA
    • 데이터 접근 기술_JPA_JPQL 기본 문법
    • 데이터 접근 기술_JPA_임베디드 타입
    maeng0830
    maeng0830

    티스토리툴바