티스토리 뷰
N:1과 달리 1:N에서 fetch join을 사용하면 중복 데이터가 조회되는 문제가 발생합니다. 1:N에서만 문제가 발생하는 이유를 알기 위해서는 N:1과 1:N에서 fetch join을 수행하는 과정을 알아야 합니다.
Member Entity
@Entity
@Getter
@Setter
public class Member {
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String name;
@ManyToOne(fetch = FetchType.LAZY)
Team team;
}
Team Entity
@Entity
@Getter
@Setter
public class Team {
@Id
@GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
@OneToMany(mappedBy = "team", fetch = FetchType.LAZY)
private List<Member> members = new ArrayList<>();
}
N:1 fetch join
다음과 같이 Member와 Team이 존재합니다. Member와 Team은 N:1 관계이기 때문에 회원당 1개의 Team을 가질 수 있습니다.
현재 3명의 Member가 존재하고 각 Member는 1개의 Team을 가지고 있습니다.
JPA에서 Member를 fetch join을 통해 연관관계에 있는 Team을 같이 조회해 보겠습니다. Member 엔티티에서 Team 엔티티를 지연로딩으로 설정하였으나, fetch join이 우선시 되기 때문에 동시에 조회하게 됩니다.
@Test
@DisplayName("N:1 fetch join")
void test() {
List<Member> members = em.createQuery("select m from Member m join fetch m.team", Member.class)
.getResultList();
for (Member member : members) {
System.out.println("username = " + member.getName() + ", teamname = " + member.getTeam().getName());
}
}
지연로딩을 사용하더라도 fetch join을 우선시하기 때문에 Member를 조회할 때 연관관계에 있는 Team을 같이 조회하는 SQL이 발생합니다. DB에 존재하는 3명의 Member가 조회되었고 동시에 각 Member가 가지는 Team도 동시에 조회됩니다.
위 내용과 별개로 fetch join을 사용하지 않았다면 Team은 사용하는 시점에 DB에서 조회되기 때문에 Team의 Proxy 객체가 영속성 컨텍스트에 저장됩니다. fetch join을 사용하여 Team을 동시에 조회하였기에 Proxy가 아닌 실제 객체가 조회되고, Team을 조회할 때 DB에 추가적인 쿼리문이 발생하지 않습니다.
1:N fetch join
반대로 Team을 조회할 때 fetch join을 사용하여 연관관계에 있는 Member를 조회해 봅시다.
N:1과 동일하게 DB에는 3명의 Member와 각 Member는 1개의 Team을 갖고 있습니다.
JPQL를 사용하여 Team 조회하고 동시에 연관관계에 있는 Member를 조회해봅시다
@Test
@DisplayName("1:N fetch join")
void test() {
List<Team> teams = em.createQuery("select t from Team t join fetch t.members", Team.class)
.getResultList();
for (Team team : teams) {
System.out.println("teamname = " + team.getName());
for (Member member : team.getMembers()) {
System.out.println("-> username = " + member.getName());
}
}
}
N:1에서 fetch join 할 때와 달리 Team(teamA)이 중복으로 조회되는 문제가 발생하였습니다. 이는 객체와 RDB(관계형 데이터베이스) 간의 차이점에 의해 발생하는 문제입니다.
DB에서 ID가 1인 Team를 외래키로 참조하고 있는 Member 튜플은 2개가 존재합니다. 따라서 ID가 1인 Team과 이를 참조하는 Member를 join으로 조회하면 2개의 데이터가 조회됩니다. JPA는 DB로부터 ID가 1인 Team을 2건을 전달받게 됩니다.
사실 저도 처음엔 이게 뭔 소린가 했는데 다음과 같이 이해하면 될 것 같습니다,
- JPQL을 통해 Entity를 조회하는 쿼리문을 작성
- JPQL -> SQL로 변경됨
- DB 입장에서는 ID가 1인 Team과 이를 참조하는 Member를 join으로 조회
- ID가 1과 2인 Member가 ID가 1인 Team을 외래키로 참조
- DB는 2건이 조회되었으므로 JPA에게 반환
- JPA는 ID가 1인 Team을 2건을 전달받음
해결 방법
1:N에서 중복으로 데이터가 조회되는 문제를 해결하는 방법이 존재합니다. JPA에서 제공하는 DISTINCT 연산자를 JPQL에 추가합니다.
@Test
@DisplayName("1:N fetch join")
void test() {
// distinct 추가
List<Team> teams = em.createQuery("select distinct t from Team t join fetch t.members", Team.class)
.getResultList();
for (Team team : teams) {
System.out.println("teamname = " + team.getName());
for (Member member : team.getMembers()) {
System.out.println("-> username = " + member.getName());
}
}
}
teamA가 2건 -> 1건으로 조회되었습니다. JPQL -> SQL로 변경되는 과정에서 DISTINCT 연산자가 추가됨에 따라 중복되는 Entity를 제거하게 된 것입니다.
사실 자세히 말하자면 DB에서는 ID가 1인(teamA) 튜플이 여전히 2건이 조회됩니다.
Team과 Member를 join 한 결과를 보면 Team이 갖는 속성 값은 동일하지만 Member가 갖는 속성값은 동일하지 않습니다. DISTINCT 연산자는 튜플이 갖는 모든 속성 값이 동일해야 중복으로 인식하고 제거합니다.
그렇다면 JPA는 어째서 1건만 조회되는 것일까요? JPA는 DB로부터 중복된 데이터를 전달받지만 자체적으로 Entity 중복을 제거합니다.
JPA는 동일한 식별자를 갖는 Entity를 제거합니다. 따라서 ID가 1인 Team Entity가 1건만 조회되는 것입니다.