JPA가 내부적으로 어떻게 동작하는지 제대로 알려면 영속성 컨텍스트를 이해해야한다.
오늘은 영속성 컨텍스트를 이해하기 위한 여러 요소들을 다룬다.

클라이언트에서 새로운 요청이 들어올 때마다 애플리케이션은 해당 요청을 처리하기 위한 독립적인 엔티티 매니저를 생성한다. 각각의 요청은 자신만의 고유한 엔티티 매니저를 가지게 되며, 이를 통해 동시성 문제를 방지하고 요청 간의 독립성을 보장할 수 있다.
영속성 컨텍스트란 무엇일까? 이는 JPA가 엔티티 데이터를 영구적으로 저장하고 관리하기 위해 제공하는 논리적인 환경이다. 영속성 컨텍스트는 엔티티의 생명주기를 관리하며, 이를 통해 데이터의 일관성과 영속성을 효과적으로 보장할 수 있다.
엔티티 매니저는 영속성 컨텍스트에 접근하기 위한 주요 인터페이스 역할을 수행한다. 개발자는 엔티티 매니저를 통해 영속성 컨텍스트의 기능을 활용할 수 있다.
엔티티 매니저가 새로 생성될 때마다, 해당 엔티티 매니저와 1대1로 매칭되는 전용 영속성 컨텍스트가 함께 생성된다. 이러한 1대1 관계는 각 엔티티 매니저가 자신만의 독립된 영속성 컨텍스트를 가질 수 있게 하여, 데이터 관리의 격리성과 안정성을 보장한다.


예를 들면, Member 객체를 생성만 한 상태이다.

예를 들면, Member 객체를 생성 후 Entity Manager에 Persist해서 엔티티를 영속성 컨텍스트에 주입한 상태이다.
Persist를 한다고 바로 데이터베이스에 저장되는 것은 아니며, 영속성 컨텍스트에 엔티티가 저장된다.

기존에 영속성 컨텍스트에 영속 상태로 있던 엔티티를 영속성 컨텍스트에서 분리한다.

실제 데이터베이스에서 엔티티에 매핑되어있는 테이블을 지운다.

영속성 컨텍스트에는 1차 캐시 공간이 존재한다. 키가 Id 밸류가 엔티티 객체 자체로 저장된다.
조회 로직이 동작 과정과 함께 1차 캐시가 가져다 주는 이점을 알아보자

조회 과정에서 Id로 엔티티를 조회하고자 할 때 DB가 아니라 1차 캐시에서 먼저 조회를 시도한다.
만약에 2번 멤버를 DB에서 조회하고자 할 때, 1차 캐시에 존재하지 않는 상태에는 DB에서 조회 하고 결과를 1차 캐시에 저장 후 결과를 반환한다.
이로써 다음에 2번 멤버를 다시 조회할 때는 1차 캐시에서 먼저 조회하기 때문에 효율적이다.
하지만 엔티티 매니저는 데이터베이스의 트랜잭션 단위로 만들어지기 때문에 트랜잭션이 종료되면 영속성 컨텍스트와 1차 캐시또한 전부 지워지기 때문에 유의미한 차이를 가져다 주지는 못한다.

다음과 같은 로직을 실행했을 때 같은 영속성 컨텍스트에 존재하는 엔티티에 대해서는 동일성을 보장한다. 이 또한 1차 캐시가 있기 때문에 가능하다.

같은 트랜잭션에서 멤버A와 멤버B를 영속성 컨텍스트에 넣어놓는다. 이를 바로 데이터베이스에 저장하지 않고 해당 트랜잭션을 커밋할 때 데이터베이스에 저장한다.

멤버 A를 영속성 컨텍스트에 넣으면 멤버 A가 1차 캐시에 우선 들어가고, JPA에서 MemberA 엔티티를 분석해 인서트 쿼리를 생성하고 쓰기 지연 SQL 저장소에 넣어둔다. 멤버 B에 대해서도 똑같이 동작한다. 그리고 트랜잭션 커밋 시점에 쓰기 지연 SQL 저장소에 있던 쿼리가 flush되어서 데이터베이스에서 쿼리가 실행된다.
이러한 동작 방식은 버퍼링을 통한 최적화를 지원한다. 배치 사이즈는 /META-INF/persistence.xml에서 프로퍼티 태그로 설정할 수 있다.
<property namne="hibernate.jdbc.batch_size value=10"/>

Member 객체에서 setter를 사용하면 바로 그 변경을 감지하여 상태를 데이터베이스에 그대로 반영해준다.

커밋을 하면 1차 캐시에 있는 엔티티와 스냅샷을 비교한다. 스냅샷은 값을 읽어온 최초 시점의 상태를 저장한 것이다.
영속성 컨텍스트가 flush되는 시점에 엔티티와 스냅샷을 하나 하나 비교하여 달라진 엔티티를 찾는다.
그리고 변화가 감지되면 쓰기 지연 SQL 저장소에 UPDATE 쿼리를 저장해 놓은 후 트랜잭션이 커밋되는 시점에 SQL 저장소에 있는 쿼리를 데이터베이스에 반영한다.

변경 감지와 동일한 메커니즘으로 동작한다. 영속성 컨텍스트가 flush되는 시점에 변경을 감지하고 SQL 저장소에 DELETE 쿼리를 저장 후 트랜잭션이 커밋되는 시점에 데이터베이스에 쿼리를 반영한다.
위에서 플러시라는 단어가 자주 등장한다. 플러시는 정확히 어떤 동작을 수행할까?
플러시는 영속성 컨텍스트의 변경내용을 데이터베이스에 반영하는 것이다. 플러시에 대해 오해할 수 있는데 플러시를 실행한다고 영속성 컨텍스트가 비워지는 것이 아니며, 영속성 컨텍스트의 변경 내용을 실제 데이터베이스에 반영한다.
플러시를 실행하는 방식은 세 가지가 있다.
em.flush() 코드로 직접 호출


코드를 통해 직접 flush를 실행했다. 그 결과 트랜잭션이 커밋되기 이전에 INSERT 쿼리가 데이터베이스에 반영되는 것을 확인할 수 있다.
트랜잭션 커밋
플러시가 발생하면, 영속성 컨텍스트의 1차 캐시에서 수정된 엔티티에 대한 변경을 감지하고 그 변경에 대한 쿼리를 SQL 저장소에 등록한다. 그리고 트랜잭션이 커밋되는 시점에 SQL 저장소에 등록된 쿼리가 실제 데이터베이스에 반영된다.
JPQL 쿼리 실행

persist된 Member 객체들이 아직 데이터베이스에 반영되지 않은 상태에서 Member 조회 쿼리가 실행되면, JPA는 정확한 결과를 가져오기 위해 자동으로 flush를 실행한다. 이때 영속성 컨텍스트의 Member 객체들이 데이터베이스에 반영되므로, JPQL 실행 시 Member들을 정상적으로 조회할 수 있다.
영속 상태인 엔티티를 영속 상태에서 분리하는 것이다.
em.detach

find를 통해 영속성 컨텍스트에 존재하지 않는 Member를 영속성 컨텍스트로 불러온 후 setter로 update해주는 코드이다.

로그를 보면 select문만 실행되고 update문은 실행되지 않은 것을 확인할 수 있다.
detach로 영속성 컨텍스트에서 분리한 후 커밋했기 때문이다.
em.clear()
clear는 영속성 컨텍스트에 있는 내용을 전부 초기화하는 명령어이다.
em.close()
영속성 컨텍스트를 종료한다.
JPA는 자바의 컬렉션을 다루듯이 데이터베이스에 쉽게 접근할 수 있게 해주어 개발 과정을 매우 편리하게 만들어준다.
오늘은 영속성 컨텍스트에 대해 공부했는데, 영속성 컨텍스트가 마치 어플리케이션과 실제 DB 사이에 존재하는 또 다른 데이터베이스 같다는 생각을 했다.
또한, 영속성 컨텍스트에서 엔티티의 생명 주기를 관리해주고 다양한 최적화를 제공하기 때문에 개발자가 적절히 활용한다면 간단한 설정만으로도 시스템의 효율성을 크게 향상시킬 수 있을 것이라고 생각한다.