개발그래머
JPA 1차 캐시가 동작하지 않는 경우 본문
개요
- 1차 캐시가 동작하지 않아 생각한 쿼리의 양보다 많은 쿼리가 나가는 경우가 종종있었다.
- 어떠한 경우 어떠한 문제로 발생하는지 한번 알아보자.
예제 코드
@Entity
class Post (
var title: String,
var content: String,
var subTitle: String
) : BaseEntity()
@MappedSuperclass
abstract class BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long? = null
@CreationTimestamp
@Column(updatable = false)
val createdAt: LocalDateTime? = null
@UpdateTimestamp
@Column
var updatedAt: LocalDateTime? = null
protected set
}
interface PostRepository : JpaRepository<Post, Long> {
fun findByTitle(title: String): Post?
@Query("select p from Post p where p.id = :postId")
fun findByPostId(@Param("postId") id: Long): Post?
}
여러 상황들
1
fun getPost() {
postRepository.findById(1L)
postRepository.findById(1L)
}
위와 같이 Transaction없이 findById를 두번 호출하게 되면 쿼리가 몇번나가게 될까요?
2번 쿼리가 나가게 됩니다.
메서드에 Transactional이 없는 경우 1차 캐시를 사용할 수 없어 쿼리가 두번 나가게 됩니다.
위 같은 경우 아래와 같이 수정해주어야 합니다.
@Transactional
fun getPost() {
postRepository.findById(1L)
postRepository.findById(1L)
}
@Transactional을 걸더라도 postId에 해당하는 entity가 존재하지 않으면 쿼리가 두번 나가게 됩니다.
2
@Transactional
fun getPost() {
val foundPost = postRepository.findById(1L).orElseThrow()
postRepository.findByTitle("타이틀")
}
findById로 값을 가져온 후에 repository에서 title로 해당 post를 검색하는 경우는 어떨까요?
2번의 쿼리가 나가게 됩니다.
Hibernate: select p1_0.id,p1_0.content,p1_0.created_at,p1_0.sub_title,p1_0.title,p1_0.updated_at from post p1_0 where p1_0.id=?
Hibernate: select p1_0.id,p1_0.content,p1_0.created_at,p1_0.sub_title,p1_0.title,p1_0.updated_at from post p1_0 where p1_0.title=?
id를 사용한 쿼리한번 title을 사용한 쿼리한번 이렇게 나가게 됩니다.
3
@Transactional
fun getPost() {
val foundPost = postRepository.findByTitle("타이틀")
postRepository.findById(1L).orElseThrow()
}
위의 상황에서 두개의 쿼리의 위치만 바꾸게 되면 몇번의 쿼리가 나갈까요?
이같은 상황에서는 1번의 쿼리만 나가게 됩니다.
4
@Transactional
fun getPost() {
postRepository.findByTitle("타이틀")
postRepository.findByTitle("타이틀")
}
findByTitle을 두번 호출하였을 때는 몇번의 쿼리가 나갈까요?
이 상황도 마찬가지로 2번의 쿼리가 나가게 됩니다.
Hibernate: select p1_0.id,p1_0.content,p1_0.created_at,p1_0.sub_title,p1_0.title,p1_0.updated_at from post p1_0 where p1_0.title=?
Hibernate: select p1_0.id,p1_0.content,p1_0.created_at,p1_0.sub_title,p1_0.title,p1_0.updated_at from post p1_0 where p1_0.title=?
5
@Transactional(readOnly = true)
fun getPost(postId: Long) {
postRepository.findById(postId).orElseThrow()
postRepository.findByPostId(postId)
}
마지막으로 findById룰 호출한 후 jpql인 findByPostId를 호출하게 되면 어떻게 될까요?
이 상황도 2번의 쿼리가 나갑니다.
결론
위와 같은 상황을 보았을 때 파악할 수 있는 점은 두가지가 있습니다.
1번 1차 캐시를 사용하기 위해서는 트랜잭션이 적용되어야 한다.
2번 1차 캐시를 사용하기 위해서는 findById를 사용해야 한다.

1차 캐시는 영속성 컨텍스트 내부에 엔티티를 보관하는 저장소이며 트랜잭션을 시작하고 종료할 때까지만 1차 캐시가 유효하다라고 영한님의 JPA 책에 나와있으며 위의 그림과 같은 구조를 띄고 있습니다.
1차 캐시를 사용하기 위해서 findById를 사용해야하는 이유는 내부 저장소의 구조 때문입니다.
엔티티를 보관하는 장소는 내부적으로 맵(Map) 구조를 사용하여 엔티티를 저장하고 이 맵은 엔티티의 식별자(ID)를 키로 사용하고 해당 엔티티를 값으로 저장하고 있습니다.
그렇기 때문에 findByTitle과 같은 경우에는 1차 캐시의 값을 호출할 수 없습니다.
또한, 직접적인 JPQL 쿼리도 내부적으로 1차 캐시에 값을 호출하지 않고 바로 쿼리를 날리는 구조이기에 사용할 수 없습니다.
'Spring' 카테고리의 다른 글
Spring Boot 트러블슈팅(org.springframework.context.ApplicationContextException: Unable to start web server) (0) | 2024.12.07 |
---|---|
ApplicationRunner, CommandLineRunner를 활용하여 운영 효율 높이기 (0) | 2024.03.31 |
Spring Event[2] @TransactionalEventListener 사용법 (0) | 2024.02.04 |
Spring Event[1] @EventListener 사용법 (0) | 2024.01.21 |