Spring/JPA

[Spring/JPA] EntityGraph

오잎 클로버 2023. 2. 15. 11:45
728x90

EntityGraph 란?

연관관계가 있는 엔티티를 조회할 경우, 지연로딩으로 설정되어 있다면 연관관계에서 종속된 엔티티는 
  쿼리 실행 시, select 쿼리 대신 proxy 객체를 만들어 엔티티가 적용시킨다.
그 후, 해당 proxy 객체가 호출될 때마다 select 쿼리가 전송된다.
@EntityGraph 는 연관관계가 지연로딩으로 되어있을 때 fetch 조인을 사용하여 여러 번의 쿼리를 한 번에 해결할 수 
  있는 점에서 fetch-join 을 어노테이션을 통해 사용할 수 있도록 한 기능이다.
(fetch-join은 일반 join 과 달리 연관 엔티티도 함께 영속 상태가 된다는 점이 있다.)

예를 들어 다음과 같은 엔티티들과 연관관계가 있다고 가정해 보자.

@Entity
@Getter
@Builder
@AllArgsConstructor @NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Owner {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    @Column(nullable = false)
    private String name;

    @Builder.Default
    @OneToMany(mappedBy = "owner", cascade = CascadeType.PERSIST, orphanRemoval = true)
    private List<Company> companies = new ArrayList<>();

}
@Entity
@Getter
@Builder
@AllArgsConstructor @NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Company {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String name;

    @ManyToOne(fetch = FetchType.LAZY)
    private Owner owner;

}

(*ManyToOne 의 기본 fetch 전략은 EAGER(즉시 로딩) 방식이다.)

이어서 다음과 같은 테스트 작성한 후, 발생한 SQL 들을 로그로 보면

@Test
void saveCompany() {
    // given
    companyDao.save(company("Lobotomy Corporation"));
    companyDao.save(company("Procyon Industries"));

    // when
    List<Company> all = new ArrayList<>();
    companyDao.findAll().forEach(all::add);

    // verify
    assertThat(all.size()).isEqualTo(2);
}
Hibernate: insert into company (name, owner_id) values (?, ?)
Hibernate: insert into company (name, owner_id) values (?, ?)
Hibernate: select c1_0.id,c1_0.name,c1_0.owner_id from company c1_0

(설정마다 다르겠으나, 필자의 경우에는 위와 같이 나왔다.)

로그를 보면 알듯이 owner 객체를 select 하지 않았다. (물론 company 칼럼의 owner_id 는 제대로 조회하였다.)

select 쿼리가 한 번만 발생하며, Company 객체 내에서 Owner 객체에 접근하기 전까지 Owner 에 대한 쿼리를

  수행하지 않기 때문이다. 

 

EnttiyGraph 를 사용한 fetch-join

Repository(DAO) 에 필요한 부분에 @EntityGraph 를 붙여 사용할 수 있다. 다음과 같이 사용할 수 있다.

@Override
@EntityGraph(attributePaths = "owner")
Iterable<Company> findAll();

attributePaths 인자에 fetch-join 을 할 대상(필드명)을 작성하여 해당 엔티티들을 fetch-join 할 수 있다.

변경된 DAO를 사용하여, 그대로 테스트 코드를 진행한 결과는 다음과 같다.

Hibernate: select c1_0.id,c1_0.name,o1_0.id,o1_0.name from company c1_0 left join owner o1_0 on o1_0.id=c1_0.owner_id

insert 쿼리는 동일하기에 임의로 작성하지 않았다.

위 테스트 로그와 달리 left join 이 발생하여 owner 역시 select 하였음을 볼 수 있다. 이는 즉시로딩 시 발생하는 쿼리와 유사하다. (혹은 동일할 수도 있다.)
(물론 현재 테스트에서는 owner 를 따로 지정하지 않았기에 실제로 테스트할 경우,  left join 이 발생하지 않는다.)

 

NamedEntityGraph

잘 사용하지 않는 방법 중 하나라고 하는 데, 이유를 Entity 클래스에 직접 작성해주어야 하기 때문이라고 한다.

다음과 같이 사용하면 된다고 한다. (필자는 한 번도 사용해 본 적이 없으며, 공식문서상에서만 보았다.)

@Entity
@NamedEntityGraph(name = "company.findAll", attributeNodes = @NamedAttributeNode("owner"))
@AllArgsConstructor @NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Company {
    // .. 생략
}

@Query("select c from Company c")
@EntityGraph("company.findAll")
List<Company> findAllCompanies();

위 테스트 결과와 동일한 쿼리가 발생하였다.

 

추가

@EntityGraph 의 type 은 두 가지가 있다. 그중 FETCH 가 default 이다.

  • FETCH: @EntityGraph 에 명시한 attribute 는 EAGER 로 패치한다. 나머지는 LAZY 로 패치한다.
  • LOAD: @EntityGraph 에 명시한 attribute 는 EAGER 로 패치한다. 나머지는 Entity 에 명시한 fetch type을 따른다.

 

 

이상입니다.