본문 바로가기

Back-End/JPA

Spring Data JPA : 벌크성 수정 쿼리와 엔티티 그래프

대규모 데이터를 다루는 애플리케이션에서는 성능 최적화가 중요한 과제이다. JPA는 이를 벌크성 수정/삭제 쿼리와 엔티티 그래프라는 기능을 제공한다. 이번 글에서는 두 가지 개념을 설명하고 활용하는 방법을 살펴보겠다.

1. 벌크성 수정/삭제 쿼리

1.1 벌크 연산이란?

벌크 연산은 여러 엔티티를 한 번에 수정하거나 삭제하는 작업으로, JPA에서 제공하는 JPQL을 사용해 실행한다. 이는 성능 최적화를 위해 데이터베이스에 직접 업데이트/삭제 쿼리를 전달하며, 영속성 컨텍스트에 있는 데이터를 무시한다.


1.2 벌크성 수정 쿼리 예제

JPA로 벌크 수정

public int bulkAgePlus(int age) {
	int resultCount = em.createQuery(
    	"update Member m " +
        "set m.age = m.age + 1 " +
        "where m.age >= :age"
        )
        .setParameter("age", age)
        .executeUpdate();
        return resultCount;
}
  • JPQL 사용 : update 쿼리를 통해 age가 특정 값 이상인 회원들의 나이를 한 번에 증가시킨다.
  • executeUpdate() : 수정된 엔티티 수를 반환한다.

스프링 데이터 JPA로 벌크 수정

@Modifying
@Query("update Member m set m.age = m.age + 1 where m.age >= :age")
int bulkAgePlus(@Param("age") int age);
  • @Modifying : DML 쿼리(update, delete) 임을 나타낸다. 없으면 예외 발생.
  • clearAutomatically 옵션 : 벌크 연산 후 영속성 컨텍스트를 자동 초기화 시킨다.

1.3 벌크 연산 테스트

@Test
public void bulkUpdate() {
    // given
    memberRepository.save(new Member("member1", 10));
    memberRepository.save(new Member("member2", 19));
    memberRepository.save(new Member("member3", 20));
    memberRepository.save(new Member("member4", 23));
    
    // when
    // em.clear() @Modifying(clearAutomatically = true)이기 때문에 굳이 clear 안해도됨.
    int resultCount = memberRepository.bulkAgePlus(20);
    
    // then
    assertThat(resultCount).isEqualTo(2); // 수정된 엔티티 수 검증
}

1.4 벌크 연산 후 주의사항

  1. 영속성 컨텍스트와 DB 간의 불일치
    • 벌크 연산은 영속성 컨텍스트를 무시하고 실행된다.
      • 영속성 컨텍스트를 무시하기 때문에 DB는 업데이트가 되었지만 영속성 컨텍스트 안에 있는 1차 캐시는 수정되기 전으로 그대로 남아있어서 항상 주의 해야함.
    • DB 상태와 영속성 컨텍스트 상태가 달라질 수 있다.
  2. 권장 방안
    • 벌크 연산을 먼저 실행 : 영속성 컨텍스트에 인티티가 없는 상태에서 실행
    • 컨텍스트 초기화 : 벌크 연산 후 em.clear() 또는 @Modifying(clearAutomatically = true)를 사용.

2. 엔티티 그래프(Entity Graph)

2.1 엔티티 그래프란?

엔티티 그래프는 연관된 엔티티를 한 번에 가져오는 기능으로, JPQL의 페치 조인과 유사하다. 여러 연관된 데이터를 조회할 때 발생하는 N+1 문제를 해결하는데 유용하다.


2.2 JPQL 페치 조인

페치 조인을 사용하면 연관된 엔티티를 한 번의 SQL로 가져온다.

@Query("select m from Member m left join fetch m.team")
List<Member> findMemberFetchJoin();
  • left join fetch : 회원과 팀을 조인하며 팀 데이터를 즉시 로딩한다.
  • 장점 : 연관된 데이터를 한 번에 가져와 N+1 문제 해결

2.3 엔티티 그래프

스프링 데이터 JPA는 엔티티 그래프를 통해 JPQL 없이도 연관 데이터를 쉽게 페치 조인할 수 있도록 도와준다.

공통 메서드 오버라이드

@Override
@EntityGraph(attributePaths = {"team"})
List<Member> findAll();
  • attributePaths: 페치 조인할 연관 엔티티를 지정한다.
  • team 필드를 즉시 로딩한다.

JPQL + 엔티티 그래프

@EntityGraph(attributePaths = {"team"})
@Query("select m from Member m")
List<Member> findMemberEntityGraph();

 

 

메서드 이름 쿼리와 함께 사용

@EntityGraph(attributePaths = {"team"})
List<Members> findByUsername(String username);

2.4 엔티티 그래프 테스트

@Test
public void findMemberLazy() {
    // given
    Team teamA = new Team("teamA");
    teamRepository.save(teamA);
    memberRepository.save(new Member("member1", 10, teamA));
    
    em.flush();
    em.clear();
    
    // when
    List<Member> members = memberRepository.findAll();
    
    // then
    for (Member member : members) {
        System.out.println(member.getTeam().getName()); // 페치 조인으로 팀 데이터 로드
    }
}