엔티티를 조회할 때 지연로딩 전략을 사용하면 연결되어 있는 엔티티를 직접 fetch 조인으로 가져오거나 자동으로 엔티티를 불러오는 방법 외에도 엔티티 그래프를 사용하는 방법이 있다. 엔티티 그래프를 사용하면 JPQL은 fetch 조인을 사용하지 않은 상태로 두고 마치 fetch 조인을 사용한 것처럼 엔티티들을 한꺼번에 조회할 수 있다.
준비

테이블 간의 관계는 위와 같다.
@NamedEntityGraph(name = "withUserRole", attributeNodes = {
@NamedAttributeNode("userRole")
})
// 어노테이션 생략
@Entity
@Table(name = "users")
public class User extends BaseDateTimeEntity {
// 코드 생략
@OneToOne(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true)
private UserRole userRole;
}
ㄴ User 엔티티
@Entity
@Builder
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name = "user_roles")
public class UserRole extends BaseDateTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Integer id;
//@Id
@OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
@JoinColumn(name = "user_id")
private User user;
//@Id
@ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
@JoinColumn(name = "role_id")
private Role role;
}
ㄴ UserRole 엔티티
@Entity
@Builder
@Getter
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name = "roles")
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Integer id;
@Column(name = "name")
@Enumerated(value = EnumType.STRING)
private RoleEnum name;
@OneToMany(mappedBy = "role", cascade = CascadeType.ALL, orphanRemoval = true)
private Set<UserRole> userRole;
@ManyToMany
@JoinTable(
name = "role_authorities",
joinColumns = @JoinColumn(name = "role_id", referencedColumnName = "id"),
inverseJoinColumns = @JoinColumn(name = "authority_id", referencedColumnName = "id")
)
private Set<Authority> authorities;
}
ㄴ Role 엔티티
NamedEntityGraph
@Test
@DisplayName("NamedEntityGraph로 UserRole 조회 성공")
public void namedEntityGraphTest() {
// given
Role role = Role.builder().name(RoleEnum.ROLE_USER).build();
User user = User.builder().email("테스트").build();
UserRole userRole = UserRole.builder().user(user).role(role).build();
entityManager.persist(userRole);
entityManager.flush();
entityManager.clear();
// when
EntityGraph<?> withUserRole = entityManager.getEntityGraph("withUserRole");
Map<String, Object> hints = Map.of("jakarta.persistence.fetchgraph", withUserRole);
User foundUser = entityManager.find(User.class, user.getId(), hints);
// then
boolean loaded = entityManager.getEntityManagerFactory().getPersistenceUnitUtil().isLoaded(foundUser.getUserRole());
Assertions.assertThat(loaded).isTrue();
}



위의 테스트는 User와 연결된 UserRole 엔티티를 조회하는 시점에 엔티티그래프를 사용하여 한꺼번에 조회하는 테스트이다. 만약 User 엔티티 내의 UserRole 엔티티가 미리 로딩되어 있으면 테스트는 통과할 것이다.
영속성 전이로 세 엔티티를 한꺼번에 저장하고, User 엔티티를 새로 조회한다.
테스트는 잘 통과됐다. 실행된 쿼리를 보면 left join을 통해 user_roles 테이블까지 같이 조회한 것을 알 수 있다.
Subgraph
@NamedEntityGraphs(value = {
// userRole
@NamedEntityGraph(name = "withUserRole", attributeNodes = {
@NamedAttributeNode("userRole")
}),
// userRole, role
@NamedEntityGraph(name = "withRole", attributeNodes = {
@NamedAttributeNode(value = "userRole", subgraph = "userRole")
}, subgraphs = @NamedSubgraph(name = "userRole", attributeNodes = {
@NamedAttributeNode("role")
}))
})
// 어노테이션 생략
@Table(name = "users")
public class User extends BaseDateTimeEntity {
ㄴ User 엔티티 변경
기존의 UserRole 엔티티를 함께 가져오는 방법 외에 subgraph를 이용해서 UserRole은 물론 UserRole에 연결된 Role까지 가져올 수 있도록 설정하였다.
여러 엔티티 그래프를 사용하고 싶으면 @NamedEntityGraphs로 변경해야 한다.
그리고 지금은 "withRole" NamedEntityGraph만 사용하고 "withUserRole"은 사용하지 않는다.
@Test
@DisplayName("subgraph로 Role 조회 성공")
public void subgraphTest() {
// given
Role role = Role.builder().name(RoleEnum.ROLE_USER).build();
User user = User.builder().email("테스트").build();
UserRole userRole = UserRole.builder().user(user).role(role).build();
entityManager.persist(userRole);
entityManager.flush();
entityManager.clear();
// when
EntityGraph<?> withRole = entityManager.getEntityGraph("withRole");
Map<String, Object> hints = Map.of("jakarta.persistence.fetchgraph", withRole);
User foundUser = entityManager.find(User.class, user.getId(), hints);
// then
boolean loaded = entityManager.getEntityManagerFactory().getPersistenceUnitUtil().isLoaded(foundUser.getUserRole());
Assertions.assertThat(loaded).isTrue();
loaded = entityManager.getEntityManagerFactory().getPersistenceUnitUtil().isLoaded(foundUser.getUserRole().getRole());
Assertions.assertThat(loaded).isTrue();
}



이번에는 subgraph를 이용해서 UserRole과 UserRole의 Role을 가져와보자. 이렇게 하면 User 엔티티를 사용할 때 권한이 필요한 로직이 있으면 Role을 지연로딩을 하지 않고 바로 가져올 수 있다.
실행 결과를 보면 left join이 두 번 실행된 것을 볼 수 있다. UserRole과 Role을 한번에 조회한 것이다.
JPQL에서 엔티티 그래프 사용하기
@Test
@DisplayName("JPQL Role 조회 성공")
public void jpqlTest() {
// given
Role role = Role.builder().name(RoleEnum.ROLE_USER).build();
User user = User.builder().email("테스트").build();
UserRole userRole = UserRole.builder().user(user).role(role).build();
entityManager.persist(userRole);
entityManager.flush();
entityManager.clear();
// when
EntityGraph<?> withRole = entityManager.getEntityGraph("withRole");
String jpql = "SELECT u FROM User u WHERE u.id = :id";
User foundUser = entityManager.createQuery(jpql, User.class)
.setParameter("id", user.getId())
.setHint("jakarta.persistence.fetchgraph", withRole) // hint 설정
.getSingleResult();
// then
boolean loaded = entityManager.getEntityManagerFactory().getPersistenceUnitUtil().isLoaded(foundUser.getUserRole());
Assertions.assertThat(loaded).isTrue();
loaded = entityManager.getEntityManagerFactory().getPersistenceUnitUtil().isLoaded(foundUser.getUserRole().getRole());
Assertions.assertThat(loaded).isTrue();
}



JPQL을 사용하는 시점에 setHint()로 직접 hint를 설정하여 사용할 수도 있다.
동적 엔티티 그래프
@Test
@DisplayName("동적 엔티티 그래프 UserRole 조회 성공")
public void createEntityGraphTest() {
// given
Role role = Role.builder().name(RoleEnum.ROLE_USER).build();
User user = User.builder().email("테스트").build();
UserRole userRole = UserRole.builder().user(user).role(role).build();
entityManager.persist(userRole);
entityManager.flush();
entityManager.clear();
// when
EntityGraph<User> entityGraph = entityManager.createEntityGraph(User.class); // 동적 엔티티 그래프 생성
entityGraph.addAttributeNodes("userRole");
Map<String, Object> hints = Map.of("jakarta.persistence.fetchgraph", entityGraph);
User foundUser = entityManager.find(User.class, user.getId(), hints);
// then
boolean loaded = entityManager.getEntityManagerFactory().getPersistenceUnitUtil().isLoaded(foundUser.getUserRole());
Assertions.assertThat(loaded).isTrue();
loaded = entityManager.getEntityManagerFactory().getPersistenceUnitUtil().isLoaded(foundUser.getUserRole().getRole());
Assertions.assertThat(loaded).isFalse();
}



User 엔티티의 @NamedEntityGraph 설정을 사용하지 않고 사용하는 시점에 동적으로 엔티티 그래프를 만들어 사용할 수 있다. 기본적인 구조는 어노테이션을 사용할 때와 크게 다르지 않다.
UserRole 엔티티까지만 조회할 수 있도록 했으니 Role 엔티티는 미리 조회하지 않은 것을 알 수 있다.
동적 subgraph
@Test
@DisplayName("동적 subgraph Role 조회 성공")
public void createSubgraphTest() {
// given
Role role = Role.builder().name(RoleEnum.ROLE_USER).build();
User user = User.builder().email("테스트").build();
UserRole userRole = UserRole.builder().user(user).role(role).build();
entityManager.persist(userRole);
entityManager.flush();
entityManager.clear();
// when
EntityGraph<User> entityGraph = entityManager.createEntityGraph(User.class); // 동적 엔티티 그래프 생성
Subgraph<UserRole> subgraph = entityGraph.addSubgraph("userRole");// 서브 그래프 추가
subgraph.addAttributeNodes("role");
Map<String, Object> hints = Map.of("jakarta.persistence.fetchgraph", entityGraph);
User foundUser = entityManager.find(User.class, user.getId(), hints);
// then
boolean loaded = entityManager.getEntityManagerFactory().getPersistenceUnitUtil().isLoaded(foundUser.getUserRole());
Assertions.assertThat(loaded).isTrue();
loaded = entityManager.getEntityManagerFactory().getPersistenceUnitUtil().isLoaded(foundUser.getUserRole().getRole());
Assertions.assertThat(loaded).isTrue();
}



subgraph도 동적으로 설정할 수 있다. 이번에는 subgraph를 이용해서 UserRole과 UserRole에 연결된 Role을 조회했기 때문에 left join이 두 번 사용되었다.
fetchgraph vs loadgraph
힌트를 만들 때 jakarta.persistence.fetchgraph를 사용하였는데 여기에는 loadgraph도 사용할 수 있다. 둘의 차이점은 뭘까?
fetchgraph -> 엔티티 그래프에서 설정한 엔티티들만 가져옴
loadgraph -> FetchType.EAGER로 설정되어 있는 연관 관계의 엔티티도 자동으로 가져옴
'공부 > JPA' 카테고리의 다른 글
| [JPA][Querydsl] 동적 쿼리 작성, 정렬해서 Slice 반환하기 (0) | 2024.05.20 |
|---|---|
| [JPA] JPA 테스트 중 available: expected at least 1 bean which qualifies as autowire candidate. 에러 해결하기 (0) | 2024.05.08 |
| [JPA] 스프링 OSIV (Open Session In View) 사용하기 (0) | 2024.03.02 |
| [JPA] LazyInitializationException 해결하기 (0) | 2024.02.28 |
| [JPA] OneToOne에서의 N + 1 문제 (0) | 2024.02.27 |