본문 바로가기

Spring Boot/Spring Data JPA

JPA) Entity Graph 로 N+1 이슈 해결하기

오직 Spring-Data-JPA 만 사용하며 N+1을 최소화하고 연관된 데이터들을 한번에 조회하고 싶을때가 있다

 

여러가지 해결방법 중 Entity Graph를 사용하는 방법을 다뤄본다.

Entity Graph를 사용하여 FetchType이 Lazy 인 연관관계 엔티티들을 한번에 (Left Join 으로) 조회할 수 있다.

(fetch join을 사용하고 싶으면 JPQL을 사용하여 fetch join 을 직접 사용하자.)

 

 

 @EntityGraph 사용

간단한 연관관계는 다음과 같이 Repository 에서 @EntityGraph 를 사용하여 사용이 가능하다.

// 부모 Entity
@Entity
...
public class Parent {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "P_ID", nullable = false)
    private long id;

    @Size(max = 100)
    @NotNull
    @Column(name = "P_NM", nullable = false, length = 100)
    private String parentName;
    
    @OneToMany(mappedBy = "parent", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    private List<Child> childEntity = new ArrayList<>();
}
// 자식 Entity
@Entity
...
public class Child {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "C_ID", nullable = false)
    private long cId;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "P_ID")
    private Parent parent;
}
// 부모 Repository
@Repository
public interface ParentRepository extends JpaRepository<Parent, Long> {

    @EntityGraph(attributePaths = {"child"}, type = EntityGraph.EntityGraphType.LOAD)
    Optional<Parent> findById(Long parentId);
}

attributePaths  : 연관관계의 변수 명을 배열로 지정

type - FETCH :  entity graph attribute는 EAGER 로 패치하고, 나머지는 LAZY로 패치

type  - LOAD : entity graph attribute는 EAGER로 패치하고, 나머지는 entity에 명시한 FetchType 이나 default FetchType으로

 

 

@NamedEntityGraph 정의

부모와 자식의 관계(1depth) 뿐만 아니라 부모-자식-자식 형태의 (2depth) 의 연관관계도 심심치 않게 볼 수 있다.

이럴때는 엔티티 그래프를 직접 정의하여 조회가 가능하다.

@NamedEntityGraph(
        name = "ParentGraph",
        attributeNodes = {
                @NamedAttributeNode(value = "childEntity", subgraph = "child-toy"),
                @NamedAttributeNode(value = "homeEntity")
        },
        subgraphs = {@NamedSubgraph(
                name = "child-toy",
                attributeNodes = {
                        @NamedAttributeNode("toyEntity")
                }
        )}
)

위 엔티티는 부모와 집이 일대일 단방향 관계로

그리고 부모와 자식이 일대다 양방향 관계, 자식과 장난감이 일대일 단방향 관계로 있는 엔티티이다.

@NamedAttributeNode 의 subgraph 속성에 @NameSubgraph 에서 사용할 이름을 지정하고

@namedSubgraph의 attributeNodes 속성에서 자식의 연관관계 엔티티 명을 작성한다

// 부모 Entity
@Entity
...
public class Parent {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "P_ID", nullable = false)
    private long id;

    // 단방향
    @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    @JoinColumn(name = "HOME")
    private Home homeEntity;
    
    // 양방향
    @OneToMany(mappedBy = "parent", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    private List<Child> childEntity = new ArrayList<>();
}
// 자식 Entity
@Entity
...
public class Child {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "C_ID", nullable = false)
    private long cId;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "P_ID")
    private Parent parent;
    
    //단방향
    @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    @JoinColumn(name = "toy")
    private TOY toyEntity;
}
// 부모 Repository
@Repository
public interface ParentRepository extends JpaRepository<Parent, Long> {

    @EntityGraph(value = {"ParentGraph"}, type = EntityGraph.EntityGraphType.LOAD)
    Optional<Parent> findById(Long parentId);
}

@NamedEntityGraph 로 정의한 Entity Graph의 명을 value 속성에 정의하여 사용한다

 

 

엔티티에 일대다 연관관계와 다대일 연관관계가 같이 있을경우

 

엔티티에 연관관계 (~ to many) 두 개 이상 있는경우에 Entity Graph 를 사용하면 (fetch join도 동일)

exception 발생

MultipleBagFetchException 에서  Multiple Bag 는 collection join이 두 개 이상이라는 의미

// 부모 Entity
@Entity
...
public class Parent {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "P_ID", nullable = false)
    private long id;

    // 양방향
    @OneToMany(mappedBy = "parent", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    private List<Child> childEntity = new ArrayList<>();
    
    // 양방향
    @OneToMany(mappedBy = "parent", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    private List<Child> dogEntity = new ArrayList<>();
}
// 부모 Repository
@Repository
public interface ParentRepository extends JpaRepository<Parent, Long> {

	@EntityGraph(attributePaths = {"childEntity", "dogEntity"})
	List<Parent> findAll();
}

다음과 같이 Parent 를 전체조회 하면 MultipleBagFetchException 이 발생한다

(NamedEntityGraph에 정의되어 있는  ~toMany 관계가 많아도 멀티백 패치 예외가 발생한다)

 

 

1. List형을 Set으로 변환

해결방법은 다음과 같이 자료형을 Set 형태로 변환해주는 방법이다.

위와 같은 방법은 데이터의 순서를 보장할수 없기 때문에 다시 정렬이 필요하다

// 부모 Entity
@Entity
...
public class Parent {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "P_ID", nullable = false)
    private long id;

    // 양방향
    @OneToMany(mappedBy = "parent", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    private Set<Child> childEntity = new HashSet<>();
    
    // 양방향
    @OneToMany(mappedBy = "parent", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    private Set<Child> dogEntity = new HashSet<>();
}

 

 

2. batch Size 적용

@BatchSize를 List 혹은 Set 위에 정의하면 인메모리에 가져오는 것이 아닌 호출하는 당시에 한번에 모든 데이터를 가져오는 동작구조를 가진다

// 부모 Entity
@Entity
...
public class Parent {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "P_ID", nullable = false)
    private long id;

    // 양방향
    @BatchSize(size = 100)
    @OneToMany(mappedBy = "parent", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    private List<Child> childEntity = new ArrayList<>();
    
    // 양방향
    @BatchSize(size = 100)
    @OneToMany(mappedBy = "parent", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    private List<Child> dogEntity = new ArrayList<>();
}

 

 

주의해야할 점

 

batch size에 fetch join을 걸면 안된다. (@Query 어노테이션 사용시)

fetch join이 우선시되어 적용되기 때문에 batch size가 무시되고 fetch join을 인메모리에서 먼저 진행하여 List가 MultipleBagFetchException가 발생

(https://jinyoungchoi95.tistory.com/40)

'Spring Boot > Spring Data JPA' 카테고리의 다른 글

querydsl 에서 case문 사용하기  (0) 2024.01.07
QueryDsl) BooleanExpression  (1) 2023.12.17
queryDsl 정리 및 예제  (1) 2023.12.03
JPA) JPA Auditing으로 자동화  (0) 2023.12.01
QueryDSL) From 절 SubQuery를 쓰고싶어  (2) 2023.11.23