"딱 한번만 조회하는 데이터에 JPA의 변경 감지 기능이 필요할까?"
이번 글은 JPA와 일회성 읽기 전용 쿼리의 관계에 대해서 소개하고, Spring에서 사용되는 @Transactional(readonly = true)를 알아보겠습니다.
JPA의 변경 감지 기능은 어떻게 동작하나요?
JPA 변경 감지 동작 원리
JPA의 변경 감지(dirty checking) 동작 원리는 영속성 컨텍스트가 관리하는 엔티티의 변경을 감지하는 메커니즘입니다.
이를 위해 초기 상태의 스냅샷(snapshot) 을 유지하며, 트랜잭션이 커밋되기 전에 현재 엔티티 상태와 비교하여 변경된 필드가 있는 경우 자동으로 update 쿼리를 생성하므로, 개발자가 직접 update 쿼리를 실행할 필요가 없습니다.
변경 감지 기능 너무 편리한데, 읽기 전용 쿼리에 어떤 영향을 주나요?
JPA 변경 감지의 단점
변경 감지 기능은 매우 편리하지만, 모든 영속성 컨텍스트에서 관리되는 엔티티에 대해 스냅샷을 생성하고 비교하는 과정이 포함됩니다. 이는 단순한 읽기 전용 쿼리에도 불필요한 오버헤드를 발생시킬 수 있습니다.
특히, 트랜잭션이 커밋되거나 flush()가 수행될 때, JPA는 변경 감지를 위해 엔티티의 현재 상태를 스냅샷과 비교하는 작업을 수행합니다. 이 과정에서 불필요한 연산이 추가되므로, 대량의 읽기 작업이 있는 경우 성능 저하가 발생할 수 있습니다.
정리 하면, 단순히 조회만 하면 되는 상황에서도 불필요하게 변경 감지를 위한 스냅샷 비교가 수행되면서 불필요한 리소스가 낭비될 수 있습니다.
그렇다면, 조회만 하면 되는 상황엔 변경 감지 기능을 사용하지 않으면 되는거 아닌가요? 어떻게 하나요?
변경 감지 없이 조회하는 방법
@Transactional에서 (readOnly = true)라는 옵션을 주면, 하이버네이트 세션의 플러시 모드를 MANUAL로 설정하여, 강제로 플러시를 호출하지 않는 한 자동으로 플러시가 발생하지 않습니다. 즉, 트랜잭션이 커밋되더라도 영속성 컨텍스트가 플러시되지 않으며, 이에 따라 플러시 후 수행되는 스냅샷 비교 작업도 생략되므로 성능이 향상됩니다.
단, 영속성 컨텍스트를 플러시하지 않기 때문에 엔티티의 등록, 수정, 삭제가 반영되지 않으므로, 반드시 조회용 메서드에서만 사용해야 합니다.
@Transactional(readOnly = true)라는 옵션으로 인해 조회 성능이 향상되는지 테스트해보도록 하겠습니다.
비교 테스트
readOnly 옵션을 사용하지 않는 경우와 사용하는 두 가지 경우를 비교하여 테스트해보겠습니다.
10,000명의 유저 데이터를 DB에 insert한 후, 10,000번의 select 쿼리를 실행하여 성능을 비교하겠습니다.
먼저 DB에 10,000명의 유저를 insert해주느 코드는 다음과 같습니다.
@BeforeEach
void insertUserData() {
for(int i = 0; i < 10_000; i++) {
UserEntity user = new UserEntity("test" + i);
entityManager.persist(user);
if (i % 50 == 0) {
entityManager.flush();
entityManager.clear();
}
}
}
만개의 엔티티를 한꺼번에 영속성 컨텍스트에 저장하면 메모리 부하가 너무 크기 때문에, 50개씩 배치 처리하면서 주기적으로 flush 해주는 방식을 사용합니다.
ReadOnly = false 테스트
@Test
@Transactional
void givenMillionUser_WhenSelectAllWithoutReadOnly_thenReturnUserIds() {
long startTime = System.nanoTime();
IntStream.range(1, 10_000).forEach(i -> entityManager.find(UserEntity.class, i));
long endTime = System.nanoTime();
System.out.println("ReadOnly false : " + (endTime - startTime) / 10_000 + " ms");
}
테스트 결과
만개 유저를 조회하는데 약 11초가 걸렸습니다.
ReadOnly = true 테스트
@Test
@Transactional(readOnly = true) // readOnly=true 적용
void givenMillionUser_WhenSelectAllReadOnlyTrue_thenReturnUser() {
long startTime = System.nanoTime();
IntStream.range(1, 10_000).forEach(i -> entityManager.find(UserEntity.class,i));
long endTime = System.nanoTime();
System.out.println("ReadOnly true : " + (endTime - startTime) / 1_000_000_000.0 + "s");
}
테스트 결과
만개 유저를 조회하는데 약 9초가 걸렸습니다.
readOnly 옵션을 사용한 결과 약 2초 더 빠르게 수행됩니다.
정리
읽기 전용인 경우 @Transactional(readOnly = true) 옵션을 사용하면 JPA의 변경 감지 기능을 비활성화하여 불필요한 flush() 호출을 막고, 변경 감지에 소요되는 리소스를 줄여 더 효율적으로 조회할 수 있습니다.
단, readOnly = true는 영속성 컨텍스트(1차 캐시)에 엔티티를 저장하지 않으므로, 반복 조회 시 오히려 성능이 저하될 수도 있습니다. 따라서 데이터베이스 및 조회 패턴에 따라 성능 테스트를 통해 적절하게 적용하는 것이 중요합니다.
'JPA' 카테고리의 다른 글
JPA 비관적 락 (0) | 2025.02.07 |
---|---|
JPA 낙관적 락 (0) | 2025.02.06 |
find 메서드와 1차 캐시의 관계 (0) | 2025.02.04 |
IDENTITY 식별자 생성 전략과 persist()의 관계 (0) | 2025.02.03 |
동시성 문제 해결방법(Update Query편) (0) | 2025.01.29 |