🚀   70세 이전에 한 모든 일은 신경 쓸 가치가 없다. - 호쿠사이

JPA EntityGraph 활용하기

2024.08.28
8분

JPA EntityGraph는 복잡한 엔티티 간의 관계를 효율적으로 조회하기 위한 강력한 도구입니다. EntityGraphJPQL 또는 Criteria API를 사용하지 않고도 관련 엔티티를 쉽게 fetch하는 방법을 제공합니다. 특히 복잡한 관계를 가진 엔티티를 조회할 때 N+1 문제를 예방하는 데 유용합니다.

1. EntityGraph란?

EntityGraphJPA 2.1에서 도입된 기능으로, 특정 엔티티를 조회할 때 관련 엔티티들을 한 번에 함께 조회할 수 있도록 도와줍니다. 이를 통해 쿼리의 성능을 최적화할 수 있으며, 특히 엔티티 간의 연관 관계가 많을 경우 유용합니다.

보통 JPA에서 엔티티를 조회할 때, 연관된 엔티티들은 LAZY 로딩에 의해 필요할 때 조회됩니다. 하지만 이 경우, 연관된 엔티티들이 여러 번의 추가 쿼리로 인해 불필요하게 많은 데이터베이스 접근이 발생할 수 있습니다. 이를 방지하기 위해 EntityGraph를 사용하여 필요한 연관된 엔티티들을 즉시 로딩(EAGER Fetching)으로 한 번에 가져올 수 있습니다.

2. EntityGraph 사용 방법

EntityGraph는 크게 두 가지 방식으로 사용됩니다: 어노테이션을 사용하여 정의하는 방식과, JPQL 쿼리에 동적으로 추가하는 방식입니다.

2.1. 어노테이션 사용하여 EntityGraph 정의

어노테이션을 사용하여 EntityGraph를 사전 정의할 수 있습니다. 예를 들어, User 엔티티와 Address 엔티티가 연관되어 있는 상황에서 User를 조회할 때 Address도 함께 조회하고 싶다면, 아래와 같이 EntityGraph를 정의할 수 있습니다.

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

    private String name;

    @OneToOne(fetch = FetchType.LAZY)
    private Address address;

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

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

    private String city;

    private String street;
}

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

    private String item;

    @ManyToOne(fetch = FetchType.LAZY)
    private User user;
}

이제 User 엔티티에서 EntityGraph를 정의해보겠습니다. 우선 조금 복잡하지만 재사용이 가능한 방식에 대해 살펴보겠습니다.

@Entity
@NamedEntityGraph(
    name = "User.withAddress",
    attributeNodes = @NamedAttributeNode("address")
)
@NamedEntityGraph(
    name = "User.withAddressAndOrders",
    attributeNodes = {
        @NamedAttributeNode("address"),
        @NamedAttributeNode("orders")
    }
)
public class User {
    // 기존 필드 및 메소드
}

위와 같이 @NamedEntityGraph를 사용하여 다수의 EntityGraph를 정의하는 것이 가능합니다. 필요에 따라 선택적으로 사용하거나 더 복잡한 정의를 적용할 때 사용할 수 있습니다.
User.withAddress라는 이름의 그래프는 User 엔티티를 조회할 때 address 필드를 즉시 로딩하도록 설정합니다.
User.withAddressAndOrders라는 이름의 그래프는 User 엔티티를 조회할 때 address 필드 및 orders 필드를 즉시 로딩하도록 설정합니다.

2.2. EntityGraph를 사용한 데이터 조회

이제 정의된 EntityGraph를 활용하여 데이터를 조회해보겠습니다. 이때 EntityGraphEntityManager 또는 Spring Data JPARepository에서 사용할 수 있습니다.

1. EntityManager 사용 시

public User findUserWithAddress(Long id) {
    EntityGraph<?> entityGraph = entityManager.getEntityGraph("User.withAddress");
    return entityManager.find(User.class, id, Collections.singletonMap("javax.persistence.fetchgraph", entityGraph));
}

2. Spring Data JPA의 Repository 사용 시

Spring Data JPA의 Repository에서는 메서드에 @EntityGraph 어노테이션 붙여서 간편하게 사용할 수 있습니다.

public interface UserRepository extends JpaRepository<User, Long> {

    @EntityGraph(value = "User.withAddress", type = EntityGraph.EntityGraphType.FETCH)
    Optional<User> findById(Long id);

}

3. attributePaths 로 빠르게 적용

@NamedEntityGraph를 사용하지 않고 Repository에서 attributePaths를 이용해 다음과 같이 사용하는 것도 가능합니다. 복잡한 설정이 필요하지 않다면 이 방법이 가장 사용하기 쉽고 단순하여 개인적으로 선호하는 방식입니다.

public interface UserRepository extends JpaRepository<User, Long> {

    @EntityGraph(attributePaths = {"address", "orders"})
    Optional<User> findById(Long id);

}

위 예제에서는 findById 메서드를 호출할 때, UserAddress, Order가 함께 페치됩니다.

실제 SQL 쿼리는 다음과 같이 실행됩니다.

SELECT
    u.id AS id1_0_,
    u.name AS name2_0_,
    a.id AS id1_1_,
    a.city AS city2_1_,
    a.street AS street3_1_,
    o.id AS id1_2_,
    o.item AS item2_2_,
    o.user_id AS user_id3_2_
FROM
    user u
LEFT JOIN
    address a
ON
    u.address_id = a.id
LEFT JOIN
    orders o
ON
    u.id = o.user_id
WHERE
    u.id = 1;

만약 EntityGraph를 사용하지 않고 기존의 LAZY 방식이나 EAGER 방식으로 조회한 경우라면 다음과 유사하게 3번의 SQL문이 실행되었을 것입니다.

SELECT
    u.id AS id1_0_,
    u.address_id AS address_id3_0_,
    u.name AS name2_0_
FROM
    user u
WHERE
    u.id = 1;

SELECT
    a.id AS id1_1_,
    a.city AS city2_1_,
    a.street AS street3_1_
FROM
    address a
WHERE
    a.id = ?;

SELECT
    o.id AS id1_1_,
    o.item AS item2_1_,
    o.user AS user3_1_
FROM
    order o
WHERE
    o.user.id = ?;

3. Dynamic EntityGraph 사용

정적인 EntityGraph 정의 외에도, 동적으로 엔티티 그래프를 생성하여 사용할 수도 있습니다. 이는 상황에 따라 다른 엔티티 관계를 가져와야 할 때 유용합니다.

public List<User> findUsersWithDynamicGraph() {
    EntityGraph<User> entityGraph = entityManager.createEntityGraph(User.class);
    entityGraph.addAttributeNodes("address");
    return entityManager.createQuery("SELECT u FROM User u", User.class)
                        .setHint("javax.persistence.fetchgraph", entityGraph)
                        .getResultList();
}

위 예제에서는 EntityManager를 통해 동적으로 EntityGraph를 생성하고 이를 사용하여 쿼리를 수행합니다.

4. 요약

EntityGraph는 복잡한 엔티티 간의 연관 관계를 효율적으로 처리하고, 불필요한 쿼리 호출을 줄이며, 성능을 최적화할 수 있는 강력한 도구입니다. 특히, 어노테이션 기반의 정적 정의와 동적 생성 방식을 통해 유연하게 사용할 수 있습니다. JPA를 사용할 때 발생할 수 있는 N+1 문제를 해결하기 위한 좋은 방법 중 하나이므로, 상황에 맞게 활용하는 것이 중요합니다.


프로필 사진
Nanutbae
A small boat sailing freely on the sea of curiosity ⊹ ࣪ ﹏𓊝﹏𓂁﹏⊹ ࣪ ˖