JPA #7 엔티티의 수정 (feat 변경 감지, 벌크 쿼리)

함형주·2023년 1월 7일
0

JPA

목록 보기
7/7

질문, 피드백 등 모든 댓글 환영합니다.

엔티티를 수정하는 방법이 여러가지가 있는데 이에 대해 알아보겠습니다.

엔티티를 단순히 조회, 저장하는 로직과는 다르게 주의해야할 부분이 있으니 잘 알고 사용해야 합니다.

수정 (변경 감지)

먼저 사용할 객체를 보겠습니다.

EntityManagerFactory emf = Persistence.createEntityManagerFactory("test");
EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
@Entity @Getter @Setter @AllArgsConstructor
public class User { // 예제에서 사용할 엔티티
	@Id Long id;
	String name;
    int age;
}

엔티티를 수정하는 방법에 변경 감지 (더티 체킹)이라는 기능이 사용됩니다.

먼저 코드로 확인해보겠습니다.

// User user = new User(1L, "user1", 20); <- db에 저장된 객체
User find = em.find(User.class, 1L);
find.setName("user11")
transaction.commit();

위 코드를 실행하고 db를 확인하면 실제로 User.name이 변경된 것을 볼 수 있습니다.

JPA는 transaction.commit()이 발생하면 먼저 영속된 엔티티에서 수정 사항이 있는지 살펴보고 만약 수정사항이 있을 경우엔 update 쿼리를 생성합니다.

원리는 영속성 컨텍스트에서 엔티티를 영속시킬 때 1차 캐시에 최초 상태의 엔티티를 스냅샷으로 저장합니다. 그리고 커밋이 발생할 때 영속된 엔티티와 스냅샷을 비교하여 변경된 부분을 찾아 update 쿼리를 생성하는 것 입니다.

때문에 JPA에선 엔티티의 변경사항을 db에 저장하기 위해 별다른 코드 없이 결과를 반영할 수 있어 매우 편리하게 사용됩니다.

하지만 실제로 JPA에서 변경 감지 기능을 사용할 때에는 주의사항이 필요합니다.

여러 개의 엔티티 수정

// User user1 = new User(1L, "user1", 20); <- db에 저장된 객체
// User user2 = new User(2L, "user2", 30); <- db에 저장된 객체
// User user3 = new User(3L, "user3", 40); <- db에 저장된 객체

User find1 = em.find(User.class, 1L);
User find2 = em.find(User.class, 2L);
User find3 = em.find(User.class, 3L);

find1.setName("user11");
find2.setName("user22");
find3.setName("user33");

transaction.commit();

만약 위와 같이 여러 엔티티를 변경 감지 기능으로 수정하면 어떤일이 발생하는지 쿼리 로그를 살펴보겠습니다.

Update 쿼리가 3개가 생성된 것을 확인할 수 있습니다.

하지만 where in 을 사용하여 하나의 쿼리로 처리할 수 있습니다. 때문에 여러 엔티티를 수정해야 하는 경우 벌크 쿼리를 생성해야합니다.

벌크 쿼리

만약 한 해가 지나 모든 User의 나이를 +1 하기 위해선 어떻게 해야할까요?

  1. 대상 User를 List로 조회
  2. User.age++
  3. 플러시 (변경감지)

이 과정을 거치면 조회한 User의 개수만큼 Update 쿼리가 발생하므로 매우 비효율적입니다. 이럴 경우 JPQL을 이용해 직접 벌크 쿼리를 생성해야합니다.

벌크 쿼리는 아래 처럼 생성할 수 있습니다.

em.createQuery("update User u set u.age = u.age + 1").executeUpdate();

update 쿼리를 생성하고 executeUpdate()를 사용하면 하나의 쿼리로 여러 엔티티(정확히는 테이블)를 수정할 수 있습니다.

추가로 setParameter()를 사용하여 파라미터로 전달할 수 있습니다. 파라미터는 JPQL에서 :로 지정할 수 있습니다. ex) em.createQuery("update User u set u.age = :param").setParameter("param", object)executeUpdate()

사진과 같이 하나의 쿼리로 같은 결과를 만들 수 있습니다.

때문에 여러 엔티티를 수정해야 한다면 변경 감지 기능 대신 직접 JPQL로 벌크 쿼리를 생성해야합니다.

벌크 연산은 Update 쿼리 외에도 delete 쿼리에도 적용 가능하며 그리고 하이버네이트구현체에선 select 쿼리에도 벌크 쿼리를 사용할 수 있습니다.

벌크 쿼리 주의점

그럼 벌크 쿼리 생성 후에 엔티티를 조회하여 잘 반영이 되었는지 확인해보겠습니다.

User user = em.find(User.class, 1L);
int newAge = user.getAge() + 1;
em.createQuery("update User u set u.age = :newAge").setParameter("newAge", newAge).executeUpdate();
User findUser = em.find(User.class, 1L);
System.out.println("findUser.getAge() = " + find123.getAge()); // 21이 나와야 함

분명 Update 쿼리가 잘 실행이 되었는데 막상 다시 엔티티를 조회하니 값이 변경되지 않았습니다. 하지만 db에서 테이블을 확인해보면 age가 21로 반영되어 있음을 알 수 있습니다.

이는 벌크 쿼리를 생성할 때 영속성 컨텍스트를 거치지 않고 바로 db에 쿼리를 생성하기 때문입니다.

그리고 영속성 컨텍스트가 이를 다시 조회할 때 pk를 기준으로 1차 캐시의 엔티티와 비교하여 값이 같기 때문에 기존의 엔티티를 그대로 사용하기 때문에 나타나는 문제입니다.

이러한 문제로 인해 큰 문제가 발생할 수 있으므로 벌크 쿼리를 생성한 후 그 엔티티를 사용할 일이 있다면 반드시 영속성 컨텍스트를 비워주어야 합니다. (위 코드에서 벌크 쿼리 이후 em.clear()를 추가한 후 다시 엔티티를 조회하면 결과가 잘 반영된 것을 알 수 있습니다.)

profile
평범한 대학생의 공부 일기?

0개의 댓글