본문 바로가기
JPA/ORM 표준 JPA

Fetch Join 별칭 이슈

by clearinging 2022. 1. 22.
반응형

Fetch Join 의 한계

1. Fetch Join에 선언된 Entity에 대해서 별칭 불가

개요

  • JPA 표준 스펙에서는 Fetch Join 대상에 별칭이 없습니다
  • 하지만 Hibernate에서 제공

Fetch Join에 별칭 선언시 이슈

  • 이유: DB와 Entity 데이터의 일관성이 깨지는 이슈가 발생
  • code
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString(of = {"id", "name"})
public class Team {
    @Id
    @GeneratedValue
    @Column(name = "team_id")
    private Long id;
    private String name;

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

    public Team(String name) {
        this.name = name;
    }
}


@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString(of = {"id", "username", "age"})
public class Member {
    @Id
    @GeneratedValue
    @Column(name = "member_id")
    private Long id;
    private String username;
    private int age;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "team_id")
    private Team team;

    public Member(String username, int age) {
        this(username, age, null);
    }

    public Member(String username) {
        this(username, 0);
    }

    public Member(String username, int age, Team team) {
        this.username = username;
        this.age = age;
        if (team != null) {
            changeTeam(team);
        }
    }

    public void changeTeam(Team team) {
        this.team = team;
        team.getMembers().add(this);
    }
}


@SpringBootTest
@Transactional
public class FetchJoinAliasTest {
    @Autowired
    EntityManager em;

    @Test
    public void contextLoads() {

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

        Member member1 = new Member("m1");
        member1.changeTeam(team);
        em.persist(member1);

        Member member2 = new Member("m2");
        member2.changeTeam(team);
        em.persist(member2);

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

        List<Team> testResult1 = em.createQuery("SELECT t FROM Team t JOIN FETCH t.members m WHERE m.username = 'm1'", Team.class)
                .getResultList();

        for (Team team1 : testResult1) {
            System.out.println("team1 = " + team1.getName());
            List<Member> members = team1.getMembers();
            for (Member member : members) {
                System.out.println("member = " + member.getUsername());
            }
        }
    }
}
  • Entity 구조
    • Member 와 Team은 ManyToOne으로 연관관계 설정
    • N:1관계를 맺고 있습니다
  • 테스트 데이터 설명
    • teamA는 m1, m2이름을 가진 Member를 2개 가지고 DB에서도 m1, m2 username을 가지고있는 Member가 teamA을 reference key를 가지고 있습니다
    • teamA를 조회하면 무조건 List Collection에 2개의 데이터가 존재 해야 합니다
    • 만약 m1을 가지는 member가 존재하는 Team을 출력할 경우 teamA와 member1, member2 가 같이 호출이 되게 됩니다
    • m1을 가지고 있는 team을 재외할 경우 아무것도 조회가 되면 아됩니다.
  • result1 조회 결과
    • where 절에 m1이라는 member를 조회 하는 쿼리 입니다
    • 조회 결과 teamA는 정상적으로 조회, member는 2개가 아니라 1개만(m1만 조회) 조회
      • teamA는 2개의 Member를 가지지만 조회 결과 1개만 나옴 -> DB불일치 발생
      • 영속성 컨택스트에서는 한 요청에 대해서만 처리가 되지만 만약 2차 캐시를 사용할 경우 문제가 됩니다
      • 2차 캐시에서는 teamA를 저장할 경우 연관관계인 m1 member Id나 Entity가 같이 저장하고 조회시 member 를 조회하는 형태로 진해이 됩니다
      • 이럴경우 다시 다른 요청에서 teamA를 조회할 경우 m1만 조회가 되는 이슈가 발생해서 데이터 불일치가 발생합니다.

2. 둘 이상 Collection Fetch Join 불가

  • Team Member
  • Team Sponser
  • 위와 같은 구조를 가지고
  • Team과 Member, Sponser 모두 fetch join 할경우 MultipleBagFetchException이 발생합니다
  • 이 경우 카타시안 곱 N * M이라는 큰 성능 이슈를 초래하는 버그가 발생하기 때문에 JPA에서 에러를 뛰우느 것입니다.
  • 이 경우 Batch Size를 사용해서 문제를 해결할 수 있습니다.

3. Page API 성능, 메모리 이슈

  • Team에 대해서 pagination 한다 하지만 member가 1개 초과할 경우 동일한 Team이 join된 member수 만큼 반복해서 표시가 될것이다.
  • 결론적으로 pagination을 하기 위해서 모든 Team데이터 조회하고 동일하지 않은 Team을 page 개수만큼 뽑아서 반환을 하는 방식을 취하게 됩니다
  • 만약 Team이 1000만개면 1000만개를 조회하고 메모리에 올라가게 됩니다 -> 성는이슈, 메모리 에러 발생

4. 결론

  • OneToMany일 경우 fetch join 말고 default batch size나 in query를 사용해서 따로 조회 하는것이 더 효과적입니다
반응형

'JPA > ORM 표준 JPA' 카테고리의 다른 글

Atomic Query 작성  (0) 2022.04.28
06 연관관계 Mapping 종류  (2) 2021.08.21
05 연관관계  (0) 2021.07.28
04 Entity  (3) 2021.07.08
03. 영속성  (0) 2021.06.30