이번엔 SQL 쿼리 로그로 확인해보는 영속성 캐시 이해하기 강의였습니다. 앞선 강의를 공부하는 과정에서 EntityManager의 문서를 굉장히 주의깊게 읽었기 때문에 이해하는 데 크게 어려움이 없었고, 강의에서도 친절하게도 예시를 보여주셔서 실습조차 필요하지 않은 수준이 되었습니다.
Entity Manager 직접 써보기
앞서 JPA에서 엔티티의 영속을 다루는 범위인 Persistence Context 를 다루는 인터페이스는 interface EntityManager 라는 것을 살펴봤습니다. 먼저 이걸 테스트에서 직접 불러와서 사용해봅니다.
@SpringBootTest 안에서는 EntityManager 를 @Autowire 로 DI받아 끌어올 수 있습니다.
@SpringBootTest
@Transactional // 저는 테스트에 필요했음 (Account 의 필드 때문에...)
open class EntityManagerTest {
@Autowired
lateinit var em: EntityManager
@Test
fun emTest() {
// accountRepo.findAll() 과 동일
println(em.createQuery("select a from Account a").resultList)
}
}
이처럼 직접 영속을 다루는 문맥인 EM을 끌어와 SQL을 부를 수 있습니다.
- createQuery 로 집어넣는 SQL은 JPQL 이라고 부른다고 합니다. 문법이 좀 이상한데 JPA 문법이라 그렇다능.
- 실제 끌어와지는 구현체는 Hibernate의 SessionImpl 이라고 하네요.
Entity Cache - "first level cache"
용어정리: Entity Cache = First Level Cache = Persistence Context...
이게 Persistence Context 입니다.
Once an entity becomes managed, that object is added to the internal cache of the current persistence context (EntityManager or Session). The persistence context is also called the first-level cache, and it’s enabled by default.
엔티티 하나가 한 번 관리가 되기 시작하면, 그 객체는 현재 영속맥락(Persistence Context - EntityManager 혹은 Session) 의 내부 캐시에 저장됩니다. "Persistence Context" 는 first-level cache 라고도 부르며, 기본적으로 활성화되어 있습니다.
- hibernate 문서, 13장 "캐시" 도입부 중
이해는 했는데 말로 설명하기 어려운 경우가 종종 있습니다. 이번 경우에도 그런거였는지 용어가 좀 혼재되어 있었는데요, EM = hibernate의 Session = Persistence Context = First level cache 입니다. 의미는 미묘하게 다르긴 한데, 이 설명의 문맥에서는 다 같은 거라도 봐도 무방하겠네요.
문서의 내용으로부터 짐작하셨겠지만, hibernate의 경우 2차 캐시도 있다고 하네요. 지금은 중요치 않으니 스킵.
Entity Cache: 엔티티를 바로 DB에 반영하지 않는다
JPA에는 DB에 저장되기 이전의 중간층인 1차 캐시가 있습니다. JPA를 통해 수정한 객체들이 실제로 DB에 반영되기 전에 임시로 메모리에 저장해두는 캐시입니다. 성능 향상 등이 목적이구요, 간단하게 예시를 들어보자면 아래와 같은 사례에 유용합니다.
- 같은 ID의 객체를 두 번 이상 연속으로 조회한다
- 같은 ID의 객체를 두 번 이상 연속으로 저장한다
위 경우 실제 DB에는 모두 한 번만 해도 되는 작업입니다. 그래서 중간에 캐시를 두고 특정 시점에 도달해서야 실제로 DB에 반영합니다.
개발자가 뭣하러 저런 짓을 하겠냐구요? JPA 의 추상화에 힘입어서 별의 별 짓을 아무 생각없이 하는 게 정말로 있을 수 없는 일일 거 같다고 생각하고 계시는 건 아니죠?
가령, 아래 테스트의 경우 findById 가 두 번 일어났지만 select 쿼리는 단 한번만 실행됩니다. (연관 테이블 조회하는 거 빼구요)
@Test
@Transactional
open fun findTwice() {
val acc = accountRepo.findById(1L)
val acc2 = accountRepo.findById(1L)
println(acc)
println(acc2)
}
Hibernate:
select
생략
from
account account0_
where
account0_.id=?
Hibernate: (연관 테이블)
select 생략
from
account_history accounthis0_
생략
Hibernate: (연관 테이블)
select 생략
from
review reviews0_
생략
보세요, println 은 두 번 되었지만 account 에 대한 select는 한 번만 실행되었죠.
Optional[Account(super=BaseAuditingEntity(createdAt=2022-02-22T21:38:44.117676, updatedAt=2022-02-22T21:38:44.117676), id=1, name=martin, email=martin@fastcampus.co.kr, accountHistories=[], reviews=[])]
Optional[Account(super=BaseAuditingEntity(createdAt=2022-02-22T21:38:44.117676, updatedAt=2022-02-22T21:38:44.117676), id=1, name=martin, email=martin@fastcampus.co.kr, accountHistories=[], reviews=[])]
영속되는 시점
실제 DB에 영속되는 시점은 3가지 정도입니다.
- 개발자가 명시적으로 flush() 를 호출했다 (Repository 에 있는 그거)
- "변기물내리는 버튼"이 flush 랍니다. 후루룩 쌓여있던 걸 내리는거죠.
- @Transactional 이 끝나서 commit 이 되었다
- 테스트에서는 자동으로 롤백이 되는 모양입니다.
(그렇담 테스트가 아니면 자동으로 commit되나? 이건 확실한지 확인을 해봐야겠지만... 스킵)- 그래서 @Transactional 이 달린 테스트에서 update 만 한 번 실행하면 SQL은 아예 실행되질 않습니다.
(강의 예시에 있엇음) - Repository 의 save() 같은 메서드들에는 기본적으로 @Transactional 이 붙어있어서, 개발자가 트랜젝션을 정의해주지 않으면 자동으로 개별 메소드가 트랜젝션으로 처리됩니다.
- 트랜젝션 관리는 다음 장부터 8개 동영상으로... 와...
- 그래서 @Transactional 이 달린 테스트에서 update 만 한 번 실행하면 SQL은 아예 실행되질 않습니다.
- 테스트에서는 자동으로 롤백이 되는 모양입니다.
- "복잡한 쿼리가 실행된다" = JPQL 이 실행되는 시점
- findAll() 이 이 경우에 해당합니다. 지금 변경된 거랑 DB에 있는거랑 합쳐야하는데, 어차피 DB에서 조회해와야 하는데 굳이 합치는 노력을 들이는 것보다는 일단 DB에 반영하고 가져오는 게 편하죠.
예시로 보는 1차 캐시
아래 코드에서 insert 쿼리는 단 한번만 일어납니다. flush가 실행되지 않았으면 조회만 되고 update는 일어나지 않았겠죠.
@Test
@Transactional
open fun saveTwice() {
val acc = accountRepo.findByName("martin", Sort.by(Sort.Order.desc("name"))).first()
acc.name = "asdf"
accountRepo.save(acc)
acc.name = "gg"
accountRepo.save(acc)
accountRepo.flush()
}
Hibernate:
select 생략
from
account account0_
생략
Hibernate:
update
account
set 생략
where
id=?
Hibernate: (연관 테이블)
insert
into
account_history
생략
진짜로 flush 를 빼볼까요?
@Test
@Transactional
open fun saveTwice() {
val acc = accountRepo.findByName("martin", Sort.by(Sort.Order.desc("name"))).first()
acc.name = "asdf"
accountRepo.save(acc)
acc.name = "gg"
accountRepo.save(acc)
// accountRepo.flush()
}
Hibernate:
select 생략
from
account account0_
where
account0_.name=?
order by
account0_.name desc
2022-02-22 21:47:47.711 INFO 23132 --- [ Test worker] o.s.t.c.transaction.TransactionContext : Rolled back transaction for test: [DefaultTestContext@35e52059 testClass = EntityManagerTest, testInstance = kr.co.fastcampus.jpasandbox
update가 사라져있습니다. 추가로 롤백 메시지까지...
본 포스팅은 패스트캠퍼스 환급 챌린지 참여를 위해 작성되었습니다.
패스트캠퍼스: https://bit.ly/37BpXiC
#패스트캠퍼스 #패캠챌린지 #직장인인강 #직장인자기계발 #패스트캠퍼스후기 #한번에끝내는JavaSpring웹개발마스터초격차패키지Online
전날의 내가 해피 먼데이래 어떻게 생각해 투즈데이?
ㄴ 투즈데이는졸려요
ㄴ 투즈데이는졸려요
'FastCampus - 한번에 끝내는 Java|Spring 웹 개발 > 04 JPA' 카테고리의 다른 글
JPA Ch 7 영속성 - 트랜젝션 - 패스트캠퍼스 챌린지 32일차 (0) | 2022.02.24 |
---|---|
JPA Ch 7 영속성 - 생명주기 - 패스트캠퍼스 챌린지 31일차 (0) | 2022.02.23 |
JPA Ch 7 영속성 - 개요 & 리얼 DB - 패스트캠퍼스 챌린지 29일차 (0) | 2022.02.21 |
JPA Ch 6 릴레이션 M:N - 패스트캠퍼스 챌린지 28일차 (0) | 2022.02.20 |
JPA Ch 6 릴레이션 1:N, N:1 - 패스트캠퍼스 챌린지 27일차 (0) | 2022.02.19 |