영속성 컨텍스트(Persistence Context) = ‘엔티티를 관리하는 전체 작업 공간
이것은 어떤 특정한 메모리 자료구조 하나만을 지칭하는 것이 아니라, JPA가 엔티티 객체들을 관리하기 위해 조성해 놓은 논리적인 환경이자 시스템 전체를 의미한다. (실제 코드 상으로는 보통 하나의 EntityManager 당 하나의 영속성 컨텍스트가 생성된다.
영속성 컨텍스트 ‘내부’의 핵심 구성 요서 3가지
Map<EntityUID, Entity> 형태)입니다. 캐시 히트를 통해 DB 접근을 줄여준다.JPA에서 트랜잭션이 시작되면 내부에 ‘영속성 컨텍스트’라는 가상의 공간이 생긴다.
이 안에는 1차 캐시라는 Map 형태의 저장소(Key: DB PK, Value: Entity 객체)가 존재한다.
@Transactional
public void viewPost() {
// 1. 처음 조회: 1차 캐시에 없으므로 DB로 SELECT 쿼리를 날린다.
// 후에 1차 캐시에 저장 하고 반환
Post post1 = postRepository.findById(1L);
// 2. 두 번째 조회 : 이미 1차 캐시에 있으므로 DB를 가지 않고 캐시에서 바로 가져온다!
Post post2 = postRepository.findById(1L);
}
post1 과 post2를 == 비교하면 true가 나온다. JPA는 같은 트랜잭션 안에서 동일한 식별자(PK)를 가진 엔티티에 대해 객체의 동일성을 보장해 주기 때문이다.update() 메서드가 없을까?게시글의 제목이나 내용을 수정할 때, JPA 코드를 보면 save()나 update() 를 명시적으로 호출하지 않는 경우가 많다. 그냥 객체의 값만 바꿨는데 알아서 UPDATE 쿼리가 날아간다.
@Transactional
public void updatePostTitle(Long postId, String newTitle) {
Post post = postRepository.findById(postId); // 1차 캐시에 저장 + '스냅샷' 생성
post.setTitle(newTitle); // 객체의 값만 변경
// postRepository.save(post); <- 이런 코드가 필요 없다.
} // 트랜잭션 종료 시점
Q : 프로젝트에는 save()가 있는데 이건 뭐지?
A : 결론 → updatePost는 DirtyChecking이 맞고, createPost의 save()는 반드시 필요하다.
save() = 새로 만든 객체를 JPA에 등록할 때, 새로 생성된 것은 비영속이기 때문에 JPA가 모른다. 그래서 JPA가 관리하기 위해 save()가 필요한 것이다.new 로 만든 객체는 JPA가 모르는 비영속 상태라서 save()로 등록해야 한다.findById() 같은 것들은 이미 JPA가 관리를 하기 때문에 save()가 필요하지 않다.우리는 객체를 수정하거나 생성했다고 해서 그 즉시 DB에 쿼리가 날아가는 것은 아니다. JPA는 쿼리를 ‘쓰기 지연 SQL 저장소’라는 곳에 차곡차곡 모아둔다. 이 모아둔 쿼리들을 실제 DB에 동기화하는 작업이 Flush(플러시) 이다.
Flush가 발생하는 타이밍은 보통 다음 세 가지이다.
SELECT * FROM post 같은 JPQL을 날리면 어떻게 될까?em.flush() 를 직접 호출할 때. (테스트 코드 작성할 때 외에는 실무에서 직접 쓸 일은 거의 없다)Q : 만약 게시글의 제목을 수정했는데(post.setTitle()), 메서드가 끝나기 전에 예상치 못한 에러가 발생해서 트랜잭션이 롤백(Rollback)된다면, DB의 데이터와 1차 캐시의 상태는 각각 어떻게 될까?
A : 트랜잭션이 커밋되기 전에 롤백되었으므로 DB에는 아무런 변화가 없다.
하지만 1차 캐시에 대해서는 초기 상태로 되돌아가는 것이 아니라, 그냥 파괴(clear)되어 버린다.
post 객체는 더 이상 JPA의 관리를 받지 못하는 준영속 상태가 된다.Q : 1차 캐시에 대해서 설명할 때, post1 과 post2는 서로 다른 인스턴스 같은데 == 비교하면 true가 나온다고 했는데 Java에서는 서로 다른 인스턴스는 메모리 주소 할당이 서로 다르기 때믄에 == 비교하면 false가 나와서 equals()를 쓰는 걸로 알고있는데 이건 왜 이런거지?
A : 말한대로 자바에서 new 키워드로 각각 생성한 서로 다른 인스턴스는 메모리 주소가 다르기 때문에 == 로 비교하면 false가 나오는 것이 맞다.
그런데 JPA의 1차 캐시에서 post1 == post2가 true 가 나오는 이유는, post1과 post2 가 서로 다른 인스턴스가 아니기 때문이다. 완벽히 동일한 하나의 메모리 주소를 가리키고 있다.
작동 원리를 순서대로 보면 이렇다.
post1): findById(1L)을 호출하면 JPA는 1차 캐시(내부적으로 Map<Object, Object> 형태)를 뒤져본다. 비어있으니 DB에 SELECT 쿼리를 날려 데이터를 가져온다.new Post()를 해서 자바 객체를 하나 만든 다음, 1차 캐시에 Key: 1L, Value: 방금 만든 Post 인스턴스의 메모리 주소 형태로 저장한다. 그리고 post1 변수에 그 주소를 준다.post2): 다시 findById(1L)을 호출한다. JPA가 1차 캐시를 확인해 보니 1L이라는 Key가 이미 있다.Post 인스턴스의 메모리 주소를 그대로 post2에게 던져줍니다.결과적으로 post1과 post2는 힙(Heap) 메모리에 떠 있는 단 하나의 동일한 객체를 쳐다보고 있는 쌍둥이 참조 변수일 뿐이다. 그래서 == 비교 시 메모리 주소가 같으므로 true가 반환됩니다. JPA는 이를 통해 애플리케이션 레벨에서 '동일성(Identity) 보장'이라는 엄청난 장점을 제공한다.
영속성 컨텍스트 안에는 1차 캐시와 스냅샷이 존재.
Post pst = new Post(”새 글”);em.persist(post); (저장) 또는 postRepository.findById(1L); (조회해서 영속성 컨텍스트로 끌고 옴)DELETE 쿼리를 날리기로 예약되어 있습니다.em.remove(post); 또는 postRepository.delete(post);