영속성 컨텍스트
Last updated
Last updated
Entity를 영구 저장하는 환경
영속성 컨텍스트는 논리적인 개념이라 눈에 보이지 않는다.
EntityManager를 통해 접근한다.
Entity를 DB에 저장하는 코드
영속성 컨텍스트를 통해 Entity를 영속화 한다는 뜻이다.
즉, Entity를 영속성 컨텍스트에 저장하는 것이다.
EntityManager를 생성하면 그 안에 1:1로 영속성 컨텍스트가 생성된다.
EntityManager 안에 영속성 컨텍스트라는 눈에 보이지 않는 공간이 생기는 것이다.
비영속
영속
준영속
삭제
new/transient
최초에 객체를 생성한 상태
즉, 영속성 컨텍스트와 전혀 관계 없는 새로운 상태
Member 객체를 생성만 하고 EntityManager에 아무것도 안 넣은 상태
managed
persist() 한 상태
영속성 컨텍스트에서 관리되는 상태
객체 생성 후에 EntityManager를 가져와서 persist로 객체를 넣어준다.
그럼 EntityManager 안에 있는 영속성 컨텍스트에 member가 들어가면서 영속 상태가 된다.
영속 상태가 된다고 해서 DB에 쿼리가 날아가는 게 아니다. commit을 해야 반영된다.
detached
영속성 컨텍스트에 저장되었다가 분리된 상태
영속성 컨텍스트와 아무 관계가 아닌 상태
removed
DB에서 데이터를 삭제하는 상태
사실상 영속성 컨텍스트라고 생각해도 된다.
member를 만들고 set 했을 때는 단지 생성만 한 비영속 상태다.
영속화를 하면 PK가 키, Entity가 값이 되어 1차 캐시에 저장된다.
1차 캐시에 저장 해두면, 조회할 때 DB 대신 1차 캐시를 먼저 뒤져서 반환할 수 있다.
member2처럼 DB엔 있지만 1차 캐시엔 없다면
DB에서 조회한 데이터를 1차 캐시에 저장한 뒤 반환한다.
이후에 다시 member2를 조회하면, 1차 캐시에 있는 member2가 반환된다.
성능상 이점은 크게 없다.
EntityManager는 트랜잭션 단위로 만들고 트랜잭션이 종료되면 삭제된다.
이때 1차 캐시도 함께 날아간다.
많은 고객에게 요청이 와도 트랜잭션을 각자 가지고 있어 찰나의 순간에만 사용된다.
find() 한 지점에는 select 쿼리가 나가지 않았다.
member가 1차 캐시에 먼저 저장됐기 때문이다.
1차 캐시 때문에 같은 데이터를 2번 조회하면 select 쿼리가 한 번만 나간다.
자바 컬렉션에서 꺼낸 데이터는 레퍼런스가 같은데, JPA도 이와 같은 동일성을 보장해준다.
반복 가능한 읽기 등급(REPEATABLE READ)의 트랜잭션 격리 수준을 제공한다.
1차 캐시를 이용해 DB가 아닌 애플리케이션 차원에서 제공할 수 있게 되었다.
persist() 한다고 바로 쿼리를 보내지 않는다.
JPA가 메모리에 쿼리를 쌓고 있다가 트랜잭션을 커밋하는 순간 DB에 보낸다.
영속성 컨텍스트에는 쓰기 지연 SQL 저장소가 있다.
memberA를 persist 하면 일단 1차 캐시에 들어간다.
이와 동시에 JPA가 Entity를 분석해서 insert 쿼리를 생성한다.
memberB를 persist 할 때도 1차 캐시에 일단 넣는다.
이때도 insert 쿼리를 생성해 쓰기 지연 저장소에 쌓아둔다.
트랜잭션을 커밋하는 시점이 되어서야 쓰기 지연 SQL 저장소에 있던 쿼리가 flush 되면서 DB에 날아간다.
memberA와 B에 대한 두 쿼리가 구분선 아래 즉, 커밋 시점에 날아가는 걸 확인할 수 있다.
value 값 만큼의 쿼리를 모아서 DB에 날릴 수 있는 옵션도 있다.
만약 persist() 할 때마다 DB에 쿼리를 날리면 최적화 하기가 어렵다. 이렇게 버퍼링처럼 여러 개를 한 번에 모아서 보낸다면, 옵션 하나로 성능 최적화를 할 수 있다.
JPA는 자바 컬렉션에 넣은 것처럼 값을 다루는 게 목적이다.
컬렉션에서 꺼낸 값을 변경했다고 다시 컬렉션에 집어넣지 않는다.
마찬가지로 JPA는 데이터 변경 후에 persist()
를 쓰지 않아도 된다.
비밀은 영속성 컨텍스트에 있다.
1차 캐시에는 키(id)와 값(entity) 외에도 스냅샷이 있다.
스냅샷은 최초로 영속성 컨텍스트에 들어온 상태를 본 떠둔 것이다.
커밋하는 시점에 내부적으로 flush()
되면서 Entity와 스냅샷을 비교한다.
스냅샷과 일일이 비교해서 바뀐 게 있으면 update 쿼리를 만든다.
쿼리를 쓰기 지연 SQL 저장소에 저장한 뒤 DB에 반영한다.
삭제는 remove()
만 해주면 된다.
다른 변경과 똑같이 쓰기 지연 저장소에 쿼리를 모았다가 커밋 시점에 실행한다.
그래도 수정할 때 persist()
를 호출해야 하지 않냐고 하는 사람도 있다. 안 하는 것이 답이다.
만약 이렇게 특정 조건일 때만 update() 하기 위해 명시적으로 사용해도, 어차피 commit을 하면 update 쿼리가 무조건 날아간다. 괜한 로직상의 오류가 발생하지 않게 그냥 생략하자.