Hibernate 에서는 데이터베이스의 부하를 줄이기 위해서 1차 캐시를 이용한다.
1차 캐시란 Transaction 내에서 작동하는 캐시를 뜻한다.
이 글은 위의 1차 캐시가 어떻게 작동하는지 알기에 본다고 생각하고, Hibernate 의 코드를 분석하는데만 신경을 쓸 것이다.
코드는 매우 간단합니다.
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Transactional
public void test() {
User user = new User("roach");
User save = userRepository.save(user);
User user2 = new User(save.getId(), "dodo");
User save2 = userRepository.save(user2);
userRepository.findById(save.getId());
}
}
위와 같은 Service Class 코드가 있고 해당 Transaction 에서 어떻게 1차 캐시를 생성하는지 알아볼 것입니다.
일단 save 메소드를 호출하게 되면 SimpleJpaRepository 의 save 메소드로 이동합니다.
여기서는 Entity 가 새로운 Entity 인지 혹은 이전에 존재했던 Entity 인지를 구분합니다.
그래서 새로운 값이라면 persist 가 작동합니다.
코드를 보면 Persist 가 작동하면서 PersistEvent 라는 것을 동작시키는데요.
이 부분을 보면서 Event-Driven 식으로 영속화와 그에 따른 관리들이 이루어진다는 것을 확인할 수 있습니다.
일단 Event 를 발행했으면 Event 를 처리해주는 Listener 를 찾아야 하는데요.
찾기 위해 DefaultPersistEventListener 라는 클래스로 이동합니다.
위의 코드를 보시면 Event 가 들어오면 해당 부분을 처리해주는 모습을 확인할 수 있습니다.
일단 위의 내용은 우리에게는 그다지 중요하지 않으니 넘어 가고 아래 코드를 쭉죽 내려가다보면 Entity 의 상태에 따라 특정 로직으로 처리하는 부분이 나옵니다.
우리가 처음으로 저장하는 Entity 이므로 아마도 해당 Entity 는 TRANSIENT 상태일 것입니다.
여기서 한가지 봐야할 사실이 있는데요.
Debugger 의 변수중 하나인 createCache 라는 Cache 가 한가지 존재합니다.
Type 은 IdentityHashMap 이네요. 무언가 1차 캐쉬같지 않나요? ㅎㅎ
일단은 코드를 쭉쭉넘겨 entityIsTransient 부분으로 이동해 봅시다.
여기서 대략적으로 코드의 문맥만 보고 추론해보자면
Entity 의 Proxy 를 Unwrap 하고, 해당 Entity 를 createCache 에 집어 넣는 것을 확인할 수 있습니다.
일단 넣으니까 아래와 같은 형태의 (key - value) 로 저장하고 있습니다.
제가 공부했을땐 id 가 key 형태로 저장된다고 들었는데 아직까진 아닌걸로 확인됬네요.
일단 코드를 더 진행시켜 봅시다.
점점 더 들어가다 보니 performSave 라는 메소드인 Save 를 수행하는 과정이 나옵니다.
드디어 찾은거 같은데요.
영속성 컨텍스트를 가져오고 Key 는 Entity 의 @Id 를 이용합니다.
영한님의 JPA 책에서 볼 수 있던 부분이네요 ㅎㅎ
PersistenceContext 에 Id 를 이용하여 Entity 를 저장하고 있습니다.
이제부터가 중요합니다.
User user2 = new User(save.getId(), "dodo");
를 했을때 영속성 컨텍스트에 해당 부분이 있는지 알고 Merge 를 하는지가 중요합니다.
보시면 일단 ID 값이 존재하므로 merge 방향으로 가는 것을 확인할 수 있습니다.
아까 말했듯이 Event-Driven 으로 진행하므로 merge 때도 똑같이 Event 를 발행하는 것을 확인할 수 있습니다.
그러면 Merge 부분에서는 Listener 코드를 어떻게 구성하고 있는지 확인해봅시다.
아마도 onMerge 라는 메소드로 구성되어 있을 것 입니다.
위의 코드를 보면 정확한데 ID 가 혹시라도 null 로 들어왔을 경우 때문에 체크를 하는 느낌이다
ID 가 null 이 아닐경우 영속성 컨텍스트에서 일단 관리되고 있는 entity 를 가져온다.
이유를 모른다면 이 글을 볼 것이 아니라 JPA 를 다시 공부해야 할 것이다.
왜냐면 이걸 모르면 nativeQuery 가 영속성 컨텍스트에 업데이트 되는지 안되는지도 모를 것이기 때문이다.
일단 이유를 설명해주자면 영속성 컨텍스트의 Entity Metadata 업데이트를 해줘야 하기 때문이다.
그래서 밑의 화면을 보면 managedEntity 우리가 기존에 생성해줬던 "roach" 라는 이름으로 잘 저장되어 있음을 확인해 볼 수 있다.
근데 한가지 신기한 사실은 Entity 의 상태를 DETACHED 준영속으로 바꾼다는 것이다.
내 추측이지만 Entity 를 DETACHED 시키는 이유는 기존의 roach 를 DETACH 시키기 위함으로 생각된다.
그래서 DETACHED 된 상태일때 들어가는 메소드를 확인해보니
위와 같이 source 라는 곳에서 entityName 과 복사된 Indetifier(1L) 을 통해서 roach Entity 를 다시 가져온다.
따라서 result 가 Null 이 아니므로 Merge 아래의 Merge 과정을 수행하게 된다.
Merge 과정 중에 DirtyInterceptor 마크를 남겨라.. 라는 등의 메소드 등도 볼 수 있다.
역시 코드를 까봐야 프레임워크의 이해도가 높아지는 것 같다.
이제 마지막으로 살펴봐야 할 부분은 SELECT Query 가 나가는지 안나가는지이다.
JPA 를 많이 공부했다면 안나간다고 생각할 것이다.
일단 findById 의 경우 LoadEvent 를 발생시킨다.
따라서 우리는 onLoad Method 를 찾아야 한다.
밑의 사진을 보면 영속성 컨텍스트에서 무언가 Proxy 를 가져오는 모습을 볼 수 있다.
따라서 쿼리는 아래와 같이 작성된다.
나중에 더 파봐야 할점
- createCache 는 어디에 사용하는 건가?
'Spring' 카테고리의 다른 글
[JPA] Transactional read only 일때 성능상 이점 (0) | 2022.03.21 |
---|---|
Kotlin Spring 에서 Required = false 대신 ?(nullable) 을 사용가능한 이유 (0) | 2022.03.17 |
MySQL 으로 형태소 분석기 없는 자동완성만들기 (0) | 2022.02.24 |
Spring 에서 왜 Private Method 는 Cglib Proxy 에 포함이 되지않을까? (0) | 2022.02.22 |
Redisson 으로 분산 Lock 구현하기 (0) | 2022.02.10 |