DB/JPA
[JPA] 영속성 컨텍스트(Persistence Context)에 대해서 (1차캐시, 쓰기지연SQL저장소, 변경감지)
hubaek
2024. 11. 24. 20:32
현재 개발을 진행하면서 JPA를 계속 쓰고 있는데, Spring Data JPA를 쓰면서 CRUD가 정말 편하지만 워낙 추상화되어있어서 쿼리문을 어떻게 나가는지 보고, 예상도 하려면 JPA에서 중요한 개념 중 하나인 영속성 컨텍스트에 대해서 정리를 해볼 필요가 있어서 포스팅 하게 됐다.
영속성 컨텍스트(Persistence Context)란?
- 쉽게 말하자면 Entity 객체를 효율적으로 쉽게 관리하기 위해 만들어진 공간
- 엔티티 매니저를 통해 영속성 컨텍스트에 접근
ORM은 객체와 데이터베이스 테이블의 매핑을 통해 엔티티 클래스 객체 안에 포함된 정보를 테이블에 저장하는 기술이다.
JPA에서는 테이블과 매핑되는 엔티티 객체 정보를 영속성 컨텍스트를 통해 애플리케이션 내에서 오래 지속되도록 보관한다.
엔티티의 생명주기
- 비영속(transient) : 영속성 컨텍스트와 전혀 관계가 없는 새로운 상태
- 영속(managed) : 영속성 컨텍스트에 관리되는 상태
- 준영속(detached) : 영속성 컨텍스트에 저장되었다가 분리된 상태
- 삭제(remove) : 삭제된 상태
비영속(Transient)
Memo memo = new Memo(); // 비영속 상태
memo.setId(1L);
memo.setUsername("hubaek");
memo.setContents("비영속과 영속 상태");
- new 연산자를 통해 인스턴스화 된 Entity 객체를 의미
- 아직 영속성 컨텍스트에 저장되지 않았기 때문에 JPA의 관리를 받지 않음
영속(Managed)
em.persist(memo);
- persiste(entity) : 비영속 Entity(memo)를 EntityManager를 통해 영속성 컨텍스트에 저장하여 관리되고 있는 상태로 만듬
준영속(Detached)
- 준영속 상태는 영속성 컨텍스트에 저장되어 관리되다가 분리된 상태를 의미
em.detach(memo);
- 영속 상태에서 준영속 상태로 바꾸는 방법 - em.detach(entity)
- em.detach(memo) : 특정 Entity(memo)만 준영속 상태로 전환
- 영속성 컨텍스트에서 관리(Managed)되다가 분리(Detached)된 상태로 전환
em.clear();
- clear() : 영속성 컨텍스트를 완전히 초기화
- 영속성 컨텍스트의 모든 Entity를 준영속 상태로 전환
- 영속성 컨텍스트의 틀은 유지하지만 내용은 비워 새로 만든 것과 같은 상태
- 계속해서 영속성 컨텍스트 이용가능
준영속 상태에서 다시 영속 상태로 바꾸는 방법 - merge(entity)
em.merge(memo);
- merge(entity) : 전달받은 Entity를 사용하여 새로운 영속 상태의 Entity를 반환
삭제(Removed)
em.remove(memo);
- remove(entity) : 삭제하기 위해 조회해온 영속 상태의 Entity를 파라미터로 전달받아 삭제 상태로 전환
영속성 컨텍스트의 기능
영속성 컨텍스트의 장점
- 1차 캐시
- 동일성(identity) 보장
- 트랜잭션을 지원하는 쓰기 지연
- 변경 감지
1. 1차 캐시
- 영속성 컨텍스트는 내부적으로 캐시 저장소를 가지고 있습니다
- 저장한 Entity 객체들이 1차 캐시 즉, 캐시 저장소에 저장이 된다고 생각하면 된다
- 캐시 저장소는 Map 자료구조 형태로 되어 있음
- Key에는 @Id로 매핑한 기본키 값을 저장
- value에는 해당 Entity 클래스의 객체를 저장
- 영속성 컨텍스트는 1차 캐시의 Key에 저장한 식별자 값을 사용하여 Entity 객체를 구분하고 관리
- em.persist(memo) 메서드가 호출되면 memo Entity 객체를 1차캐시(캐시저장소)에 저장함
Entity 조회 : 1차 캐시에 조회하려는 Id가 존재하지 않은 경우
em.find(Memo,class, 1) 메서드 호출 후 1차 캐시에 저장되어 있지 않아서 DB에 접근하여 SELECT 조회 후 캐시 저장소에 저장
- 1차 캐시에 데이터가 없을 경우, 데이터베이스에서 조회한다.
- 데이터베이스에서 조회 한 후, 1차 캐시에 저장하고 반환
- 한 트랜잭션 내에서 해당 Entity를 다시 조회할 시, SELECT 쿼리가 나가지 않고 1차 캐시에 있는 것 반환
Memo memo1 = em.find(Memo.class, 1);
//memo1 DB 조회 후 캐시 저장소에 저장 (SELECT 쿼리 O)
Memo memo2 = em.find(Memo.class, 1);
// 1차 캐시에 있는 Memo를 반환 (SELECT 쿼리 X)
1차 캐시의 장점
- DB 조회 횟수를 줄여줌
- 객체 동일성 보장 - 1차 개치를 사용해 DB row 1개 당 객체 1개가 사용되는 것을 보장
객체 동일성 보장
Member member1 = em.find(Member.class, 1L);
Member member2 = em.find(Member.class, 1L);
// 이 때 == 비교를 하면?
System.out.println("result: "+(member1 == member2));
// 결과
result: true
2. 쓰기 지연 SQL 저장소(ActionQueue)
- JPA는 쓰기지연 저장소를 만들어 ActionQueue에 트랜잭션처럼 Query를 담고 있다가 commit 하면서 한번에 반영을 해줌
EntityTransaction et = em.getTransaction();
et.begin();
try {
Memo memo = new Memo();
memo.setId(2L);
memo.setUsername("hubaek");
memo.setContents("쓰기 지연 저장소");
em.persist(memo);
Memo memo2 = new Memo();
memo2.setId(3L);
memo2.setUsername("Baeks");
memo2.setContents("두번째 메모");
em.persist(memo2);
System.out.println("트랜잭션 commit 전");
et.commit();
System.out.println("트랜잭션 commit 후");
} catch (Exception ex) {
ex.printStackTrace();
et.rollback();
} finally {
em.close();
}
emf.close();
- 실제로 기록을 확인해보면 트랜잭션commit 호출 전까지는 SQL 요청이 없다가 트랜잭션 commit 후 한번에 Insert SQL 2개가 순서대로 요청된 것을 확인할 수 있습니다.
flush()
- 트랜잭션 commit 후 추가적인 동작이 있는데 바로 em.flush() 메서드의 호출이다.
- flush 메서드는 영속성 컨텍스트의 변경 내용들을 DB에 반영하는 역할을 수행
- 즉, 쓰기 지연 저장소의 SQL들을 DB에 요청하는 역할
flush() 동작 확인을 위해 직접 호출
EntityTransaction et = em.getTransaction();
et.begin();
try {
Memo memo = new Memo();
memo.setId(4L);
memo.setUsername("Flush");
memo.setContents("Flush() 메서드 호출");
em.persist(memo);
System.out.println("flush() 전");
em.flush(); // flush() 직접 호출
System.out.println("flush() 후\n");
System.out.println("트랜잭션 commit 전");
et.commit();
System.out.println("트랜잭션 commit 후");
} catch (Exception ex) {
ex.printStackTrace();
et.rollback();
} finally {
em.close();
}
emf.close();
- em.flush() 메서드가 호출되자 바로 DB에 쓰기 지연 저장소의 SQL이 요청되었습니다.
- 이미 쓰기 지연 저장소의 SQL이 요청 되었기 때문에 더 이상 요청할 SQL이 없어 트랜잭션이 commit 된 후에 SQL 기록이 보이지 않습니다.
3. 변경감지(Dirty checking)
- 영속성 컨텍스트에서 보관하는 데이터에 변경이 일어났는지 확인
- 비교시점 : 트랜잭션 commit 호출 후 fulsh() 메서드 호출 되면서 비교
- JPA는 영속성 컨텍스트에 Entity를 저장할 때 최초 상태(LoadedState)를 저장
- 트랜잭션이 commit되고 em.flush()가 호출되면 Entity의 현재 상태와 저장한 최초 상태를 비교
- 변경 내용이 있다면 Update SQL을 생성하여 쓰기 지연 저장소에 저장하고 모든 쓰기 지연 저장소의 SQL을 DB에 요청
- 마지막으로 DB의 트랜잭션이 commit 되면서 반영
- 따라서변경하고 싶은 데이터가 있다면 먼저 데이터를 조회하고 해당 Entity 객체의 데이터를 변경하면 자동으로 생성되고 DB에 반영됩니다.
System.out.println("변경할 데이터를 조회합니다.");
Memo memo = em.find(Memo.class, 4);
System.out.println("memo.getId() = " + memo.getId());
System.out.println("memo.getUsername() = " + memo.getUsername());
System.out.println("memo.getContents() = " + memo.getContents());
System.out.println("\n수정을 진행합니다.
memo.setUsername("Update");
memo.setContents("변경 감지 확인");
System.out.println("트랜잭션 commit 전");
et.commit();
System.out.println("트랜잭션 commit 후");
- memo Entity를 수정하고 트랜잭션 commit 후 Update SQL이 요청된 것을 확인 할 수 있다.