본문 바로가기
Java/ORM

JAVA ORM - JPA(Fetch Join)

by 티코딩 2024. 8. 12.

페치조인(Fetch Join)

연관된 엔티티나 컬렉션을 함께 로드하는데 사용되는 기술로, 기본적으로 JPA에서 연관된 엔티티는 지연로딩으로 설정된경우에, 해당 엔티티에 접근할 때마다 별도의 쿼리가 발생하는데 이를 N+1문제라고 함. 페치 조인을 사용해 메인 엔티티와 연관된 엔티티를 한 번의 쿼리로 함께 로드해 성능을 최적화 할 수 있다.

 

@Entity
public class Product {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @OneToMany(mappedBy = "product", fetch = FetchType.LAZY)
    private List<Order> orders;
	...
}

@Entity
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne
    @JoinColumn(name = "product_id")
    private Product product;
	...
}

위와같은 Product 엔티티와 Order엔티티를 페치조인 하려면,

EntityManager em = entityManagerFactory.createEntityManager();

String jpql = "SELECT p FROM Product p JOIN FETCH p.orders WHERE p.id = :productId";
TypedQuery<Product> query = em.createQuery(jpql, Product.class);
query.setParameter("productId", productId);

Product product = query.getSingleResult();

em.close();

Product 엔티티와 연관된 Order 엔티티를 한번에 로드한다. 

위의 JOIN FETCH 구문을 사용하면 연관된 엔티티를 즉시로딩(EAGER)으로 처리해서 별도의 쿼리가 발생하지 않고 한번에 데이터를 가져오게 된다.

 

SELECT p FROM Product p JOIN FETCH p.orders WHERE p.id = :productId
이 JPQL구문은
SELECT p.id AS p_id, p.name AS p_name, o.id AS o_id, o.product_id AS o_product_id FROM Product p JOIN Order o ON p.id = o.product_id WHERE p.id = ?

이렇게 SQL쿼리로 날아간다.

 

장점

N+1문제를 해결해 성능향상, 필요한 모든 데이터를 한번의 쿼리로 가져올 수 있다.

단점

Product가 여러개의 Order과 연관된 경우, Product의 데이터가 중복 될 수 있다.(하지만 Jpa는 중복을 제거해서 반환한다.)

조인의 수가 많아지면, 쿼리가 복잡해져 성능저하를 일으킬 수 있다.

JPQL에선 한 번의 쿼리에서 여러개의 컬렉션에 대해 페치조인을 쓸 수 없다.(단일 컬렉션에 대해서만 페치조인을 사용.)

 

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.getName()
                        + ", " + member.getTeam().getName());
            }

위와같은 코드에서 쿼리가 몇번날까?

Hibernate: 
    /* select
        m 
    From
        Member m */ select
            m1_0.MEMBER_ID,
            m1_0.createdBy,
            m1_0.createdDate,
            m1_0.city,
            m1_0.street,
            m1_0.zipcode,
            m1_0.lastModifiedBy,
            m1_0.lastModifiedDate,
            m1_0.name,
            m1_0.TEAM_ID 
        from
            Member m1_0
Hibernate: 
    select
        t1_0.id,
        t1_0.name 
    from
        Team t1_0 
    where
        t1_0.id=?
member = 회원1, 팀A
member = 회원2, 팀A
Hibernate: 
    select
        t1_0.id,
        t1_0.name 
    from
        Team t1_0 
    where
        t1_0.id=?
member = 회원3, 팀B

이렇게 세번나간다. 만약 팀이 더 다양하고 회원이 많으면?

끔찍하다.

 

이럴때 사용해야하는게 바로 페치조인이다.

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

JPQL을 이렇게 바꾸면,

Hibernate: 
    /* select
        m 
    From
        Member m 
    join
        
    fetch
        m.team */ select
            m1_0.MEMBER_ID,
            m1_0.createdBy,
            m1_0.createdDate,
            m1_0.city,
            m1_0.street,
            m1_0.zipcode,
            m1_0.lastModifiedBy,
            m1_0.lastModifiedDate,
            m1_0.name,
            t1_0.id,
            t1_0.name 
        from
            Member m1_0 
        join
            Team t1_0 
                on t1_0.id=m1_0.TEAM_ID
member = 회원1, 팀A
member = 회원2, 팀A
member = 회원3, 팀B

위와같이 쿼리가 한개만 나가게된다.

 

컬렉션 페치 조인

일대다 관계에서.

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

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

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

이렇게 바꿔줘도

Hibernate: 
    /* select
        t 
    From
        Team t 
    join
        
    fetch
        t.members */ select
            t1_0.id,
            m1_0.TEAM_ID,
            m1_0.MEMBER_ID,
            m1_0.createdBy,
            m1_0.createdDate,
            m1_0.city,
            m1_0.street,
            m1_0.zipcode,
            m1_0.lastModifiedBy,
            m1_0.lastModifiedDate,
            m1_0.name,
            t1_0.name 
        from
            Team t1_0 
        join
            Member m1_0 
                on t1_0.id=m1_0.TEAM_ID
member = 팀A|2명
member = 팀B|1명

쿼리는 한번만 나가게 된다.