출처
본 글은 인프런의 김영한님 강의 자바 ORM 표준 JPA 프로그래밍 - 기본편
을 수강하며 기록한 필기 내용을 정리한 글입니다.
-> 인프런
-> 자바 ORM 표준 JPA 프로그래밍 - 기본편 강의
요약
- 영속성 컨텍스트는 JPA 내부에서 DB와 소통하는 중간 단계이다.
- 영속성 컨텍스트는 Entity Manager와 1:1 혹은 N:1 관계로 맺어진다.
- 객체는 영속성 컨텍스트와 DB에 포함되어 있는지 여부에 따라 비영속, 영속, 준영속, 삭제 상태가 된다.
- 영속성 컨텍스트를 통해 1차 캐시, 동일성, 쓰기 지연, 변경 감지, 지연 로딩 등의 이점을 가질 수 있다.
- 영속성 컨텍스트의 변경 내용과 DB를 동기화 시키는 작업을 flush라 한다.
1. 영속성 컨텍스트와 엔티티 상태
1-1. EntityManagerFactory, EntityManager
- 하나의 어플리케이션이 만들어 질 때, EntityManagerFactory가 생성된다.
- EntityManagerFactory는 Client의 요청이 들어올 때마다 EntityManager를 생성하며, 요청이 끝나면 폐기한다.
- EntityManager는 DB와 소통하며, 중간에 영속성 컨텍스트가 있다.
- 영속성 컨텍스트는 EntityManager와 1:1 혹은 N:1 관계로 생성된다.
1-2. 영속성 컨텍스트
- 영속성 컨텍스트는 EntityManager와 DB 사이의 중간 단계를 담당한다.
- EntityManager의 persist(), find() 등의 함수들이 호출 될 때, 바로 DB에 반영되는 것이 아니라 영속성 컨텍스트에 반영되는 것이다.
1-3. 엔티티 상태
(1) 비영속 상태, 객체 생성
Cookie cookie = new Cookie();
cookie.setId(1L);
cookie.setTaste("Chocolate");
- 다음과 같이 cookie라는 객체를 생성하고, 멤버 변수를 설정한다.
- 이는 그냥 Cookie라는 클래스의 cookie 객체를 선언한 것이다.
- 영속성 컨텍스트와 전혀 상관없다. : 비영속 상태
(2) 영속 상태, 1차 캐시, persist(), find()
<1차 캐시>
- 영속성 컨텍스트 내에는 1차 캐시가 있다.
- 객체가 1차 캐시에 들어가면 '영속 상태'가 되며, 이는 'flush' 과정 전까지 DB에 반영되지 않는다.
- 1차 캐시에 들어가는 과정은 다음 두가지를 통해 이루어 질 수 있다.
-> persist(), find()
<persist()>
eM.persist(cookie);
- persist() 함수를 호출하면 cookie 객체는 영속성 컨텍스트에 반영된다.
- 이는 영속성 컨텍스트에 존재하는 1차 캐시에 cookie 내용을 넣어두는 것이다.
- 해당 과정을 통해 영속성 컨텍스트의 관리 하에 들어간다. : 영속 상태
- 아직 DB에 반영되진 않는다. : 쿼리를 전달하지 않는다.
<find()>
Cookie chocoCookie = eM.find(Cookie.class, 1L);
- find() 함수를 통해서도 객체가 1차 캐시에 들어갈 수 있다.
- find() 함수는 아래 과정을 통해 수행된다.
- find() 함수의 파라미터로 전달된 내용을 1차 캐시에서 먼저 조회한다.
- 1차 캐시에 해당 객체가 없을 경우, DB에 쿼리문을 전달하여 가져온다.
- 가져온 객체를 먼저 1차 캐시에 저장한 후, 반환한다.
- 다음 동일한 객체를 find() 함수로 조회할 경우, DB에서 가져오지 않고 1차 캐시에서 가져올 수 있다.
-> 불필요한 DB 소통을 줄일 수 있다.
(3) 준영속, 삭제 상태, detach(), remove()
eM.persist(cookie);
eM.detach(cookie);
- 준영속 상태는 영속성 컨텍스트 내 1차 캐시에 있는 객체를 다시 뺌으로써 영속성 컨텍스트의 관리를 받지 않는다.
eM.remove(cookie);
- 삭제 상태는 1차 캐시 및 DB에서 아예 삭제된 상태이다.
2. 영속성 컨텍스트의 장점
- 영속성 컨텍스트의 장점은 모두 하나의 트랜잭션 내에서 가능하다는 것을 유의해야 한다.
2-1. 1차 캐시
- persist() 혹은 find() 함수를 통해 1차 캐시에 등록될 수 있다.
- 1차 캐시를 통한 이점은 find(), 쓰기 지연, Dirty Checking에서 나타난다.
(1) find()
Cookie chocoCookie1 = eM.find(Cookie.class, 1L);
Cookie chocoCookie2 = eM.find(Cookie.class, 1L);
- 다음과 같이 동일한 데이터를 여러번 find 할 경우, 첫번째 find() 과정에서 DB로부터 가져온 후, 1차 캐시에 저장하게 된다.
- 이후에 수행되는 find() 과정은 DB와의 소통 없이 1차 캐시에서 가져올 수 있게 된다.
(2) 쓰기 지연 (transactional write-behind)
eM.persist(chocoCookie1);
eM.persist(chocoCookie2);
...
eM.persist(chocoCookie10);
- 영속성 컨텍스트는 'flush' 과정 전까지 DB로 쿼리문을 전달하지 않는다.
- 따라서 다음과 같이 여러번의 persist() 과정이 있어도 이를 1차 캐시에 저장해 둔다.
- 이와 더불어 각각의 insert 쿼리문을 SQL 저장소에 쌓아둔다.
- 이후 'flush' 과정을 통해 한번에 DB에 반영시킨다.
- 이는 구현 내용에 따라 버퍼링 기능을 기대할 수 있다.
(3) Dirty Checking
- 다음은 find() 를 통해 조회한 데이터를 수정하는 과정이다.
Cookie chocoCookie = eM.find(Cookie.class, 1L);
cookie.setTaste("vanilla");
tX.commit();
- 일반적으로 DB의 데이터를 수정할 때, 다음과 같은 과정을 생각하게 된다.
- DB로부터 데이터를 가져온다.
- 가져온 데이터의 내용을 수정한다.
- 다시 DB로 보내 내용을 update 한다.
- 하지만 JPA에서는 3번 과정을 생략한다. : Dirty Checking
- Dirty Checking 과정은 아래와 같이 이루어 진다.
- find() 함수를 통해 조회한 데이터가 1차 캐시에 저장된다. 이때, 원본을 따로 또 저장한다.
- 데이터 내용을 수정한다.
- commit()을 통해 flush가 수행될 때, 원본과 비교한다.
- 수정된 내용이 있을 경우, UPDATE 쿼리를 SQL 저장소에 추가한 후, DB로 전달한다.
- 따라서 따로 DB에 반영하겠다는 코드를 작성하지 않아도 알아서 다 해준다.
- 이는 Java의 컬렉션에 저장되어 있는 값을 수정하듯이 엔티티를 활용하는 것에 의미가 있다.
- 하지만 만약 수정한 객체가 DB에 반영되지 않기를 원할 경우, '준영속 상태'를 활용하면 된다.
<준영속 상태 활용>
Cookie chocoCookie = eM.find(Cookie.class, 1L);
chocoCookie.setTaste("vanilla");
eM.detach(chocoCookie);
tX.commit();
- 다음과 같이 detach() 함수를 통해 chocoCookie 객체를 1차 캐시에서 빼낼 경우(준영속 상태), 이는 flush 과정에서 제외된다.
- 준영속 상태로 만드는 방법은 아래와 같다.
-> eM.detach(entity) : 특정 객체를 골라서 빼낼 수 있다.
-> eM.clear() : 영속 컨텍스트 내 모든 객체를 뺀다.
-> eM.close() : 영속 컨텍스트를 아예 종료시킨다.
2-2. 동일성
Cookie chocoCookie1 = eM.find(Cookie.class, 1L);
Cookie chocoCookie2 = eM.find(Cookie.class, 1L);
System.out.println(chocoCookie1 == chocoCookie2);
- 다음과 같이 1차 캐시에 저장되어 있는 동일한 객체를 가져와서 이를 비교할 경우, 동일함(==)을 보장해준다.
- 이 또한 마치 Java의 컬렉션 내에서 동일한 값을 꺼내와 비교한 것과 같은 느낌을 받을 수 있다.
3. flush
- flush는 영속성 컨텍스트의 변경 내용을 DB에 반영하는 과정이다.
(영속성 컨텍스트 내용과 DB 내용을 동기화 시켜주는 작업이라 생각하면 된다.)
3-1. flush 과정
- 기존 내용에서 수정된 내용이 있는지 확인 후, 있을 경우 해당 내용을 담은 쿼리문을 SQL 저장소에 등록 (Dirty Checking)
- SQL 저장소의 쿼리를 DB에 전송.
(flush 과정이 수행된다고 1차 캐시 내용이 모두 지워지거나 아예 마무리 지어지는건 아니다. DB 내용을 동기화 시켜주는 것 뿐.)
(하지만 주로 commit()을 통해 flush 과정이 이루어 진다.)
3-2. flush 과정이 수행되는 경우
(1) 트랜잭션이 커밋될 때
- 다음과 같이 commit() 함수가 호출되면 flush 과정이 자동으로 수행된다.
eM.persist(chocoCookie);
tX.commit();
(2) 직접 호출 : flush()
- 다음과 같이 commit() 전, DB에 바로 적용하는 등 필요에 따라 직접 호출도 가능하다.
eM.persist(chocoCookie);
eM.flush();
...
tX.commit();
(3) JPQL 쿼리 실행
- 다음과 같이 JPQL이 활용될 경우, 자동으로 flush 과정이 수행된다.
eM.persist(chocoCookie);
jpql = eM.createQuery("select c from Cookie c", Cookie.class);
- 위 코드의 경우, JPQL로 인한 flush 때문에 'chocoCookie' 객체의 persist() 과정이 commit() 이전에 바로 DB에 반영되게 된다.
- 만약 이를 방지하고 싶다면 flush mode를 바꿔주면 된다.
< flush mode option >
- 활용 : eM.setFlushMode(FlushModeType.COMMIT)
- 종류
-> FlushModeType.AUTO : Default.
-> FlushModeType.COMMIT : commit()에서만 자동으로 flush.