개발/spring

[jpa] EntityManager를 타지 않는 작업

방푸린 2024. 11. 20. 11:41
반응형

 

JPA에서 EntityManager를 타지 않고 데이터베이스에 직접적으로 영향을 주는 쿼리는 주로 영속성 컨텍스트를 우회하여 실행된다. 이런 쿼리들은 영속성 컨텍스트를 거치지 않으므로 JPA EntityListeners, Hibernate EventListener, Hibernate Interceptor 등이 호출되지 않으니 주의해야 한다.  

이런 작업들은 JPA가 아닌 네이티브 SQL 쿼리처럼 작동하며 영속성 컨텍스트를 사용하지 않아 성능 면에서 유리하지만, JPA의 1차 캐시자동 변경 감지 기능은 무시되므로 사용 시 데이터 일관성 문제에 유의해야한다..!

 

1. deleteInBatch

  • JpaRepository에서 제공하는 deleteInBatch는 엔티티를 영속성 컨텍스트에 로드하지 않고 데이터베이스에서 직접 삭제 쿼리를 실행함
  • JPQL 기반 단일 DELETE 쿼리 실행.
  • 직접 쿼리 실행: deleteInBatch는 JPQL을 통해 바로 데이터베이스에서 삭제를 수행하며, 삭제된 엔티티들을 영속성 컨텍스트에서 관리하지 않는다. 따라서 캐시된 상태와 데이터베이스 상태 간의 불일치가 발생할 수 있음.
  • 엔티티 상태 무시: 삭제할 엔티티들이 반드시 영속성 컨텍스트에 존재하지 않아도 삭제 작업이 가능.
    • 엔티티 리스트에 포함된 엔티티가 영속성 컨텍스트에 있더라도, 자동으로 flush 되지 않는다!!
  • 개별 엔티티를 삭제할 때 delete() 메서드를 호출하면 각 엔티티별로 DELETE 쿼리가 발생하지만, deleteInBatch는 단일 DELETE 쿼리를 실행하여 성능을 최적화함.
    • deleteInBatch는 WHERE id IN (...) 형태의 SQL DELETE 쿼리를 생성하고 실행

주의사항

  1. 영속성 컨텍스트: 삭제 작업 후 영속성 컨텍스트와 데이터베이스 간 불일치가 발생할 수 있으므로, 사용 전에 컨텍스트를 비우거나 동기화를 고려
  2. 트랜잭션 필요: deleteInBatch는 반드시 트랜잭션 내에서 호출해야 합니다. 그렇지 않으면 TransactionRequiredException이 발생
  3. 연관 관계: 삭제 대상 엔티티가 다른 엔티티와 연관 관계가 있는 경우, 외래 키 제약 조건으로 인해 예외가 발생할 수 있으므로 관계 설정을 명확히 해야
List<MyEntity> entitiesToDelete = myEntityRepository.findAllByStatus("INACTIVE");
myEntityRepository.deleteInBatch(entitiesToDelete);

2. update / delete JPQL 쿼리

  • JPQL의 update와 delete는 EntityManager를 통하지 않고 데이터베이스에 직접 영향을 준다
  • 영속성 컨텍스트를 거치지 않기 때문에 영속 상태의 엔티티는 업데이트되지 않음
    • 이 방식으로 삭제된 엔티티는 영속성 컨텍스트와 동기화되지 않아 영속성 컨텍스트에 같은 엔티티가 존재한다면 불일치가 발생할 수 있음
@Modifying
@Query("UPDATE MyEntity e SET e.status = :status WHERE e.id = :id")
void updateStatusById(@Param("id") Long id, @Param("status") String status);

@Modifying
@Query("DELETE FROM MyEntity e WHERE e.status = :status")
void deleteByStatus(@Param("status") String status);

3. @Query + 네이티브 쿼리

  • 네이티브 쿼리를 사용하면 영속성 컨텍스트를 무시하고 데이터베이스에 직접 접근
  • @Query에서 nativeQuery = true를 지정하면 네이티브 SQL 쿼리가 실행
@Modifying
@Query(value = "DELETE FROM my_table WHERE status = :status", nativeQuery = true)
void deleteByStatusNative(@Param("status") String status);

4. JdbcTemplate

  • JdbcTemplate은 JPA를 통하지 않고 순수 SQL을 사용해 데이터베이스에 직접 접근한다
  • 영속성 컨텍스트를 전혀 사용하지 않으므로 성능적으로 빠르지만, 엔티티 매핑과는 무관하게 작동함
@Autowired
private JdbcTemplate jdbcTemplate;

public void deleteInactiveUsers() {
    String sql = "DELETE FROM users WHERE status = ?";
    jdbcTemplate.update(sql, "INACTIVE");
}

5. EntityManager.createNativeQuery

  • JPA의 EntityManager를 통해 네이티브 SQL 쿼리를 직접 실행할 수 있는데 이는 영속성 컨텍스트를 거치지 않는다.
@Transactional
public void deleteByCustomQuery(EntityManager entityManager, String status) {
    String sql = "DELETE FROM my_table WHERE status = :status";
    entityManager.createNativeQuery(sql)
                 .setParameter("status", status)
                 .executeUpdate();
}

6. 외부 툴 사용

데이터 처리 작업을 완전히 애플리케이션 외부로 이동하면 엔티티 리스너 및 이벤트 리스너가 작동하지 않음

  • 데이터베이스의 Stored Procedure.

EntityManager를 타지 않는 쿼리와 영속성 컨텍스트를 함께 사용하면?

실제 프로젝트에서 난 이슈

EntityManager를 타지 않는 쿼리(예: 네이티브 SQL, deleteInBatch, JPQL의 UPDATE/DELETE 등)와 영속성 컨텍스트를 함께 사용하는 경우, 데이터베이스 상태와 영속성 컨텍스트 간의 불일치 문제가 발생할 수 있다. JPA의 영속성 컨텍스트는 1차 캐시에 엔티티의 상태를 유지하고, 데이터 변경이 있을 경우 이를 동기화하여 데이터의 일관성을 보장한다. 하지만, 영속성 컨텍스트를 우회하는 작업은 이 동기화를 방해한다.

1. 주요 문제: 데이터 불일치

시나리오

  • 영속성 컨텍스트에 특정 엔티티가 로드된 상태(EntityManager의 1차 캐시).
  • 데이터베이스에서 직접적으로 데이터를 수정하거나 삭제.
  • 영속성 컨텍스트는 이러한 변경 사항을 인지하지 못함.

결과

  • 이후 EntityManager를 통해 조회 시, 여전히 변경 전 상태가 반환될 수 있음.
  • 캐시와 DB 간 데이터 불일치 문제가 발생.

2. 해결 방법

(1) 영속성 컨텍스트 초기화

영속성 컨텍스트를 명시적으로 초기화하거나 새로 고침하면 DB 변경 사항을 반영할 수 있음

  • EntityManager.clear(): 모든 영속성 컨텍스트 초기화(detached).
  • EntityManager.refresh(entity): 특정 엔티티(하나)를 데이터베이스 상태로 새로 고침.
    • 만약 영속 상태가 아닌 엔티티(Detached)에 대해 호출하면 예외 발생: 
    • javax.persistence.EntityNotFoundException 또는 IllegalArgumentException
@Transactional
public void processEntitiesSafely() {
    List<MyEntity> entities = myEntityRepository.findAll();

    myEntityRepository.deleteAllInBatch();

    // 영속성 컨텍스트 초기화
    entityManager.clear();
    // entityManager.refresh(entity); 사용 시 단건 처리만 가능하가에 for loop 필요

    // 이후 상태 확인 시 영속성 컨텍스트에 데이터 없음
    System.out.println("Entities after clear: " + myEntityRepository.findAll());
}

(2) 영속성 컨텍스트를 사용하지 않는 작업에서는 캐시 의존 금지

영속성 컨텍스트를 무시하는 작업을 수행한 후에는, 동일한 EntityManager를 통해 엔티티 상태를 확인하거나 사용하지 않아야

(3) 트랜잭션 분리

영속성 컨텍스트가 불일치를 일으킬 여지를 없애기 위해, 트랜잭션을 분리하거나 비영속 작업만 포함된 별도의 서비스 레이어를 작성

@Transactional
public void performOperation() {
    // JPQL DELETE 작업을 별도 메서드에서 수행
    bulkDeleteInSeparateTransaction();

    // 이후 영속성 컨텍스트를 사용하는 작업
    MyEntity entity = entityManager.find(MyEntity.class, 1L);
    System.out.println(entity.getStatus());
}

// 별도 트랜잭션
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void bulkDeleteInSeparateTransaction() {
    myEntityRepository.bulkDeleteByStatus("INACTIVE");
}

(4) Spring Data JPA + @Modifying 쿼리 사용

Spring Data JPA에서 @Modifying을 사용하여 JPQL 또는 네이티브 SQL로 데이터베이스를 직접 변경하면 영속성 컨텍스트를 자동으로 초기화하도록 설정할 수 있다.

  • clearAutomatically = true 속성을 추가하면 JPQL 실행 후 자동으로 영속성 컨텍스트가 초기화됨
@Modifying(clearAutomatically = true)
@Query("UPDATE MyEntity e SET e.status = :status WHERE e.someField = :value")
void bulkUpdateStatus(@Param("status") String status, @Param("value") String value);

clear? flush? refresh?

영속성 컨텍스트

  • 영속성 컨텍스트는 애플리케이션의 메모리에서 엔티티를 관리하는 공간
  • 문제: 영속성 컨텍스트의 상태와 데이터베이스 상태가 불일치할 경우, 이후 작업에서 잘못된 결과를 초래할 수 있음

flush(영속성 컨텍스트 -> 디비)

  • 역할:
    • 영속성 컨텍스트의 변경 사항(insert, update, delete 등)을 즉시 데이터베이스에 반영
    • 영속성 컨텍스트는 그대로 유지되며, 동기화된 최신 상태를 유지; 데이터베이스에만 영향
    • 커밋을 한 건 아니라서 롤백 가능
  • 적합한 상황:
    • 영속성 컨텍스트 상태를 유지하면서 현재 상태를 데이터베이스와 동기화해야 할 때.
    • flush 후에도 엔티티를 계속 사용할 필요가 있을 때.
  • 자동 플러시:
    • JPQL 쿼리 실행 전.
    • 트랜잭션 커밋 직전.

 

clear

  • 역할:
    • 영속성 컨텍스트를 완전히 초기화
    • 초기화 후, 이전에 관리되던 엔티티는 비영속 상태(Detached)
  • 적합한 상황:
    • 영속성 컨텍스트의 상태와 데이터베이스 상태 간의 불일치를 제거하려고 영속성 컨텍스트를 완전히 비워야 할 때.
    • 메모리 사용량을 줄이기 위해 대량 처리 후 컨텍스트를 초기화하려는 경우.

 

flush와 clear를 함께 사용하는 경우

  • 대량 작업을 처리하거나, 영속성 컨텍스트를 유지할 필요가 없는 경우
  • 일반적인 패턴:
    1. flush()로 변경 사항을 데이터베이스에 반영.
    2. clear()로 영속성 컨텍스트 초기화.

 

refresh(디비 -> 영속성 컨텍스트)

  • 역할
    • 데이터베이스의 최신 상태로 영속성 컨텍스트의 엔티티를 다시 로드
    • 영속성 컨텍스트에 있는 엔티티의 상태를 무시하고, 데이터베이스의 값을 가져와 동기화
  • 적합한 상황:
    • 영속성 컨텍스트에만 영향을 미침:
      • 데이터베이스의 최신 상태로 영속성 컨텍스트의 엔티티를 갱신
      • 변경되지 않은 데이터는 그대로 유지
    • 비영속(detached) 상태의 엔티티에 대해 호출하면 예외(IllegalArgumentException) 발생
    • 프록시를 초기화하거나 변경된 상태를 되돌릴 때

 

728x90
반응형