TIL/김영한의 자바 ORM 표준 JPA 프로그래밍 - 기본편

객체지향 쿼리 언어 - 벌크 연산

minOE 2025. 7. 10. 18:47
728x90

벌크 연산

• 재고가 10개 미만인 모든 상품의 가격을 10% 상승하려면?

    • JPA 변경 감지 기능으로 처리할 경우, 다음과 같은 방식으로 동작함

        1. 재고가 10개 미만인 상품을 리스트로 조회한다.
        2. 조회된 상품 각각의 가격을 10% 증가시킨다.
        3. 트랜잭션 커밋 시점에 JPA의 변경 감지(Dirty Checking)가 동작하여
           변경된 각 상품에 대해 UPDATE SQL을 실행한다.

    → 만약 변경된 상품이 100건이라면, UPDATE SQL이 100번 실행됨
      (비효율적이며 성능 저하 우려 있음)

벌크 연산 예제

• 쿼리 한 번으로 여러 테이블 로우를 변경할 수 있다 (→ 엔티티를 일일이 변경하지 않음)

    • JPA에서는 JPQL의 벌크 연산(Bulk Operation)을 통해 실행

    • 예: UPDATE, DELETE 쿼리

        - UPDATE: 조건에 맞는 여러 엔티티의 값을 한 번에 변경

        - DELETE: 조건에 맞는 여러 엔티티를 한 번에 삭제

        - INSERT: JPQL 자체는 지원하지 않지만, Hibernate는 아래처럼 지원함
            (insert into ... select ... 구문)

    • executeUpdate() 메서드 사용 시, 영향 받은 row 수(int)를 반환

        → 예: 20개의 row가 수정되었다면 executeUpdate()20 반환

    • 단, 벌크 연산은 영속성 컨텍스트를 무시하므로 1차 캐시와 동기화 되지 않음
        → 이후 em.clear() 등으로 캐시 초기화 권장

 

       // 벌크 연산
            int resultCount = em.createQuery("update Member m set m.age = 20")
                .executeUpdate();

            System.out.println("resultCount = " + resultCount);

 

 

 

벌크 연산 주의

• 벌크 연산은 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리 실행

    → 즉, 1차 캐시나 스냅샷을 거치지 않고 DB에 바로 반영됨

• 벌크 연산은 가능한 한 트랜잭션 내에서 가장 먼저 실행하는 것이 안전함

• 벌크 연산 수행 후에는 반드시 em.clear() 또는 flush + clear
    영속성 컨텍스트를 초기화해야 데이터 불일치 문제를 방지할 수 있음

package org.hellojpa;

import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.EntityTransaction;
import jakarta.persistence.Persistence;
import org.hellojpa.jpql.Member;
import org.hellojpa.jpql.Team;

public class JpaMain {
    public static void main(String[] args) {

        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager();

        EntityTransaction tx = em.getTransaction();
        tx.begin();

        try {

            Team teamA = new Team();
            teamA.setName("팀A");
            em.persist(teamA);

            Team teamB = new Team();
            teamB.setName("팀B");
            em.persist(teamB);

            Member member1 = new Member();
            member1.setUsername("회원1");
            member1.setTeam(teamA);
            em.persist(member1);

            Member member2 = new Member();
            member2.setUsername("회원2");
            member2.setTeam(teamB);
            em.persist(member2);

            Member member3 = new Member();
            member3.setUsername("회원3");
            member3.setTeam(teamB);
            em.persist(member3);


            
            // 벌크 연산
            int resultCount = em.createQuery("update Member m set m.age = 20")
                .executeUpdate();

            System.out.println("resultCount = " + resultCount);

            Member findMember = em.find(Member.class, member1.getId());
            System.out.println("findMember = " + findMember.getAge());


            tx.commit();
        } catch (Exception e) {
            tx.rollback();
            e.printStackTrace();
        } finally {
            em.close();
        }
        emf.close();

    }
}




결과가 0으로 나오는 이유 요약
- 벌크 연산은 영속성 컨텍스트를 무시하고 DB에 직접 쿼리를 실행합니다.
- 즉, em.find()로 가져오는 member1은 이미 영속성 컨텍스트에 존재하는 캐시된 엔티티입니다.

Member findMember = em.find(Member.class, member1.getId());
System.out.println("findMember = " + findMember.getAge());  // → 0


- findMember는 DB가 아닌 1차 캐시(영속성 컨텍스트)에서 조회됨
- 이 캐시에는 age = 0인 원래 값이 남아 있음
- 벌크 업데이트 결과가 영속성 컨텍스트에 반영되지 않음 → 그래서 0으로 보이는 것


JPA 벌크 연산 주의사항 실습 예제 (1차 캐시 동기화 문제)

package org.hellojpa;

import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.EntityTransaction;
import jakarta.persistence.Persistence;
import org.hellojpa.jpql.Member;
import org.hellojpa.jpql.Team;

public class JpaMain {
    public static void main(String[] args) {

        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager();

        EntityTransaction tx = em.getTransaction();
        tx.begin();

        try {

            Team teamA = new Team();
            teamA.setName("팀A");
            em.persist(teamA);

            Team teamB = new Team();
            teamB.setName("팀B");
            em.persist(teamB);

            Member member1 = new Member();
            member1.setUsername("회원1");
            member1.setTeam(teamA);
            em.persist(member1);

            Member member2 = new Member();
            member2.setUsername("회원2");
            member2.setTeam(teamB);
            em.persist(member2);

            Member member3 = new Member();
            member3.setUsername("회원3");
            member3.setTeam(teamB);
            em.persist(member3);



            // 벌크 연산
            int resultCount = em.createQuery("update Member m set m.age = 20")
                .executeUpdate();

            em.clear(); // 영속성 컨텍스트 초기화

            System.out.println("resultCount = " + resultCount);

            Member findMember = em.find(Member.class, member1.getId());
            System.out.println("findMember = " + findMember.getAge());


            tx.commit();
        } catch (Exception e) {
            tx.rollback();
            e.printStackTrace();
        } finally {
            em.close();
        }
        emf.close();

    }
}

 

 

 

728x90