일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- 1차원 DP
- 2차원 dp
- 99클럽
- @Builder
- @Entity
- @GeneratedValue
- @GenericGenerator
- @NoargsConstructor
- @Query
- @Table
- @Transactional
- Actions
- Amazon EFS
- amazon fsx
- Android Studio
- ANSI SQL
- ApplicationEvent
- assertThat
- async/await
- AVG
- AWS
- Azure
- bind
- builder
- button
- c++
- c++ builder
- c03
- Callback
- case when
- Today
- Total
기록
JPA flush()와 clear(): JPA와 QueryDSL 사용 시 발생하는 1차 캐시 동기화 문제 해결하기 본문
JPA flush()와 clear(): JPA와 QueryDSL 사용 시 발생하는 1차 캐시 동기화 문제 해결하기
youngyin 2025. 1. 13. 00:00시작하면서
스프링 프로젝트에서 QueryDSL을 사용해 데이터를 업데이트할 때, 테스트 코드가 예상과 다르게 동작해 당황한 경험이 있으신가요? 이번 글에서는 QueryDSL로 데이터베이스 업데이트 후 JPA 1차 캐시와 동기화되지 않는 문제를 해결한 과정을 공유합니다.
문제 상황 개요
QueryDSL을 사용하여 테이블의 데이터를 직접 업데이트하는 메서드를 작성한 후, 테스트 코드에서 기대한 대로 데이터가 조회되지 않는 문제가 발생했습니다.
주요 동작 흐름
- 단계 1: JPA로 엔티티를 저장할 때, 데이터베이스에 값을 저장하고 동시에 1차 캐시(영속성 컨텍스트)에 엔티티 상태를 유지합니다.
- 단계 2: QueryDSL의 update() 메서드를 사용하여 데이터베이스의 값을 직접 수정합니다. 하지만 이 과정에서 JPA의 1차 캐시는 변경 사항을 인지하지 못합니다.
- 단계 3: JPA로 엔티티를 조회할 때, 1차 캐시에 있는 이전 상태의 데이터를 반환합니다. 이로 인해 데이터베이스의 최신 값이 반영되지 않는 문제가 발생합니다.
예제 코드
// Entity 저장
val entity = Entity(title = "original-title")
repository.save(entity)
// QueryDSL로 데이터베이스 직접 업데이트
queryFactory.update(QEntity.entity)
.set(QEntity.entity.title, "updated-title")
.where(QEntity.entity.id.eq(entity.id))
.execute()
// 엔티티 조회 (1차 캐시에서 이전 상태 반환)
val result = repository.findById(entity.id)
println(result.title) // 출력: original-title (실제 DB 값은 updated-title)
위 코드를 실행하면 result.title이 "original-title"로 출력되며, 데이터베이스에 저장된 "updated-title"과 다른 값이 반환됩니다.
테스트 실패 결과
이 테스트를 실행하면 다음과 같은 오류 메시지가 출력됩니다:
org.opentest4j.AssertionFailedError:
Expecting:
<["updated-title"]>
to contain:
<["original-title"]>
오류 메시지에서 알 수 있듯이, 테스트가 실패한 이유는 QueryDSL로 직접 실행한 SQL 업데이트가 JPA의 1차 캐시에 반영되지 않았기 때문입니다.
원인 분석
QueryDSL과 JPA 1차 캐시의 동기화 문제
JPA에서는 EntityManager가 엔티티를 관리하며, 1차 캐시(영속성 컨텍스트)에 엔티티의 상태를 저장합니다. 이렇게 하면 같은 트랜잭션 내에서 동일한 엔티티를 여러 번 조회할 때 데이터베이스에 반복적으로 접근할 필요 없이 1차 캐시에서 빠르게 값을 가져올 수 있습니다.
하지만 QueryDSL의 update() 메서드는 JPA 엔티티가 아니라 데이터베이스 테이블을 직접 수정하는 방식으로 동작합니다. 즉, JPA가 관리하는 1차 캐시와는 무관하게 SQL 쿼리를 실행하여 데이터베이스에만 변경 사항을 반영합니다. 이렇게 하면 대량 데이터를 효율적으로 수정할 수 있지만, JPA는 이 변경 사항을 알지 못하므로 1차 캐시에 있는 엔티티는 여전히 이전 상태를 유지하게 됩니다.
따라서 findById()를 호출하여 엔티티를 조회하면 데이터베이스에서 최신 값을 가져오는 것이 아니라, JPA의 1차 캐시에 남아 있는 이전 상태의 데이터를 반환하게 됩니다. 이로 인해 테스트 코드에서 update() 메서드를 호출한 후에도 findById()로 조회한 값이 수정되지 않은 것처럼 보이는 문제가 발생한 것입니다.
QueryDSL의 update() 방식은 주로 성능 최적화와 대량 데이터 처리가 필요한 상황에서 사용됩니다. JPA의 변경 감지를 이용해 개별 엔티티를 하나씩 수정하는 방식보다 빠르고 효율적이기 때문입니다. 그러나 이 방식으로 인한 동기화 문제를 해결하려면, 이후 작업에서 JPA가 데이터베이스의 최신 상태를 인식하도록 flush()와 clear() 같은 메서드를 활용해야 합니다.
해결 방법: flush()와 clear() 사용
- flush : 영속성 컨텍스트의 변경 사항을 데이터베이스에 즉시 반영하도록 강제합니다.
- clear : 영속성 컨텍스트를 초기화하여 1차 캐시를 비우고, 이후 조회 시 데이터베이스에서 새롭게 데이터를 가져오도록 합니다.
이 두 메서드를 적절히 활용하면 QueryDSL로 업데이트한 후에도 최신 데이터를 정확하게 조회할 수 있습니다.
해결 과정
수정된 테스트 코드
@Test
fun updateAndVerifyEntity() {
// given
val entity = Entity(title = "original-title")
repository.save(entity)
val entityId = entity.id
// when
queryFactory.update(QEntity.entity)
.set(QEntity.entity.title, "updated-title")
.where(QEntity.entity.id.eq(entityId))
.execute()
entityManager.flush()
entityManager.clear()
// then
val result = repository.findById(entityId)
assertThat(result).isNotNull
assertThat(result.get().title).isEqualTo("updated-title")
}
위 테스트 코드에서 flush()와 clear()를 통해 JPA의 1차 캐시를 비우고 데이터베이스의 최신 값을 정확하게 조회할 수 있도록 했습니다.
디버깅 코드 추가
val affectedRows = queryFactory.update(QEntity.entity)
.set(QEntity.entity.title, "updated-title")
.where(QEntity.entity.id.eq(entityId))
.execute()
println("Affected rows: $affectedRows")
entityManager.flush()
entityManager.clear()
디버깅 과정에서 affectedRows 값을 출력하여 실제로 몇 개의 레코드가 업데이트되었는지 확인하고, flush()와 clear()를 호출하여 JPA 1차 캐시 문제를 해결했습니다.
비즈니스 로직에서 clear() 사용을 지양해야 하는 이유
이 작업을 서비스 레이어에서 해야 하는지 고민했습니다. 서비스는 트랜잭션을 다루는 책임이 있기 때문에, flush()와 clear()를 그 안에 넣는 것이 맞지 않을까 생각했기 때문입니다.
하지만 최종적으로 테스트 코드에 추가한 이유는 다음과 같습니다:
- 테스트 환경에서만 필요한 동작: flush()와 clear()는 테스트 코드에서 JPA와 QueryDSL 간 동기화를 강제하기 위해 필요한 작업입니다. 비즈니스 로직에서는 일반적으로 JPA가 자동으로 동기화를 처리하므로, 이러한 강제 동기화는 필요하지 않습니다.
- 비즈니스 로직의 일관성 유지: 비즈니스 로직에서 entityManager.clear()를 호출하면 모든 엔티티에 대한 1차 캐시가 날아가고 JPA의 변경 감지 기능을 사용할 수 없게 됩니다. 이는 성능 저하 및 일관성 문제를 초래할 수 있기 때문에 일반적으로 비즈니스 코드에서는 사용하지 않는 것이 권장됩니다.
따라서 테스트 코드에서만 flush()와 clear()를 호출하여 동기화를 보장하고, 비즈니스 로직은 기존의 트랜잭션 관리 방식을 그대로 유지하는 것이 더 적절하다고 판단했습니다.
핵심 개념 정리
flush()와 clear()
설명 | 영속성 컨텍스트의 변경 내용을 DB에 반영 | 영속성 컨텍스트 초기화 (1차 캐시 비움) |
1차 캐시 | 유지 | 초기화 (비워짐) |
DB 반영 여부 | 즉시 반영 | 반영하지 않음 |
주요 용도 | DB와 동기화를 맞출 때 | 1차 캐시 무효화, 새로운 상태 조회 시 |
- flush()는 영속성 컨텍스트를 유지하면서 변경 사항을 즉시 데이터베이스에 반영하는 것이며, clear()는 영속성 컨텍스트를 완전히 초기화하여 이후 작업에서 데이터베이스에서 직접 조회하게 만듭니다.
결론
이번 경험을 통해 JPA와 QueryDSL을 함께 사용할 때 발생할 수 있는 동기화 문제를 이해하고 적절히 해결하는 방법을 배웠습니다. 특히 JPA의 영속성 컨텍스트와 1차 캐시 관리에 대한 이해가 중요하다는 점을 깨달았습니다.
'Web > Spring' 카테고리의 다른 글
Vaadin 프레임워크 소개: hawkBit 사례 분석(1) (0) | 2025.01.24 |
---|---|
AWS Elasticache를 Spring Boot에 연결하기 (0) | 2025.01.06 |
JWT와 Redis를 활용한 사용자 인증 프로세스 설계 (0) | 2024.12.31 |
테스트 환경에서의 In-Memory Map을 활용한 로그인/로그아웃 시스템 구현 (0) | 2024.12.30 |
Spring/이벤트 처리 예제 - ApplicationEvent, 모듈화와 확장성을 향상시키는 방법 (0) | 2024.12.16 |