![[자바 ORM 표준 JPA 프로그래밍 - 기본편] 3. 영속성 관리 - 내부 동작 방식](https://img1.daumcdn.net/thumb/R750x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0PIzZ%2FbtsC53c4sIr%2FATLjEKgAM7zkAVtJqU82kk%2Fimg.png)
인프런 김영한 강사님의 [자바 ORM 표준 JPA 프로그래밍 - 기본편]을 수강하고 정리한 글입니다.
자바 ORM 표준 JPA 프로그래밍 - 기본편 강의 - 인프런
현업에서 실제로 JPA로 개발을 하고 있습니다. 그런 입장에서보면 지금 작성하고 있는 코드들이 어떻게 작동하는지 이해하는데 큰 도움을 주는 강의입니다. 다음은 제가 느낀 이 강의의 장점들
www.inflearn.com
📌 영속성 컨텍스트
✅ EntityManagerFactory와 EntityManager
위 사진 처럼, 웹 애플리케이션에는 단 하나의 EntityManagerFactory만 존재한다.
그리고 EntityManagerFactory가 매 요청마다 각각의 EntityManager 객체를 생성한다.
✅ 영속성 컨텍스트
엔티티를 영구적으로 저장하는 환경
entityManager.persist(entity);
위 명령어는 엔티티 정보를 DB에 저장하겠다는 의미가 아니다.
엔티티를 영속성 컨텍스트에 저장하겠다는 뜻이다.
✅ 스프링 프레임워크에서 EntityManager와 영속성 컨텍스트
스프링 프레임워크에서는 영속성 컨텍스트와 EntityManager는 1:N 관계이다.
✅ 엔티티의 생명 주기
1. 비영속 (new / transient) : 영속성 컨텍스트와 전혀 관계 없는 새로운 상태
2. 영속 (managed) : 영속성 컨텍스트에 관리되는 상태
3. 준영속 (detached) : 영속성 컨텍스트에 저장되었다가 분리된 상태
4. 삭제 (removed) : 영속성 컨텍스트에서 삭제된 상태
✅ 비영속 (new / transient)
영속성 컨텍스트와 전혀 관련 없는 새로운 상태를 의미한다.
//객체를 생성한 상태(비영속)
Member member = new Member();
member.setId("member1");
member.setUsername("회원1");
객체를 생성만 하고, 영속성 컨텍스트에 어떠한 명령도 하지 않은 상태다.
✅ 영속 (managed)
영속성 컨텍스트에 관리되는 상태를 의미한다.
//객체를 생성한 상태(비영속)
Member member = new Member();
member.setId("member1");
member.setUsername(“회원1”);
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
//객체를 저장한 상태(영속)
em.persist(member);
영속되었다고 해서 DB에 저장되었다는 뜻은 아니다.
트랜잭션에 커밋할 때 영속성 컨텍스트에 있던 것들이 DB에 일괄 저장된다.
✅ 준영속 (detached), 삭제 (removed)
준영속 상태는 영속성 컨텍스트에 저장되었다가 분리된 상태를 의미한다.
삭제 상태는 영속성 컨텍스트에서 삭제된 상태를 의미한다.
//회원 엔티티를 영속성 컨텍스트에서 분리, 준영속 상태 - 관계가 사라짐.
em.detach(member);
//객체를 삭제한 상태(삭제)
em.remove(member);
✅ 영속성 컨텍스트의 이점
- 1차 캐시를 이용할 수 있다.
- 동일성(Identity)를 보장한다.
- 트랜잭션 (Transaction) 을 지원하는 쓰기 지연 (Transactional Write-Behind)
- 변경 감지 (Dirty Check)
- 지연 로딩 (Lazy Loading)
✅ 엔티티 조회, 1차 캐시
1차 캐시 : 영속성 컨텍스트 내부에 존재하는 캐시
1차 캐시는 아래 기능들을 수행한다.
- 임시 저장소 역할 : 영속 상태의 엔티티들이 1차 캐시에 저장된다. 즉, DB에 엔티티가 저장되기 전에 임시로 보관되는 곳이다.
- 동일성 보장 : 같은 트랜잭션 안에서 동일한 엔티티를 조회할 경우, 1차 캐시에서 엔티티를 반환하기 때문에 항상 동일한 인스턴스가 보장된다. 이는 데이터의 일관성을 유지하는데 큰 도움이 된다.
- 트랜잭션 지원 : 트랜잭션을 통해 DB와의 상호 작용이 이루어진다.
- 쿼리 최적화 : DB에 접근하기 전에 1차 캐시를 우선 확인함으로써 불필요한 DB 접근을 줄이고 성능 향상에 기여한다.
//엔티티를 생성한 상태(비영속)
Member member = new Member();
member.setId("member1");
member.setUsername("회원1");
//엔티티를 영속
em.persist(member);
위 사진과 코드는 영속성 컨텍스트 내부의 1차 캐시에 엔티티를 임시 저장해두는 상태다.
✅ 1차 캐시에서 조회하기
Member member = new Member();
member.setId("member1");
member.setUsername("회원1");
//1차 캐시에 저장됨
em.persist(member);
//1차 캐시에서 조회
Member findMember = em.find(Member.class, "member1");
✅ 데이터베이스에서 조회하기
Member findMember2 = em.find(Member.class, "member2");
최초에 em.find()를 통해 조회할 때는 1차 캐시를 먼저 확인한다.
만약 1차 캐시에 찾는 값이 존재하지 않는다면, DB를 조회하고 조회해온 값을 1차 캐시에 먼저 저장한 후, 그 값을 반환한다.
(만약 1차 캐시에 찾는 값이 존재했다면 DB까지 가지 않는다.)
✅ 영속 엔티티의 동일성 보장
Member a = em.find(Member.class, "member1");
Member b = em.find(Member.class, "member1");
System.out.println(a == b); //동일성 비교 true - 1차 캐시가 있어서 가능한 일
첫 번째 find문에서는 1차 캐시에 해당 값이 존재하지 않음으로 DB에서 조회한 후, 1차 캐시에 먼저 저장하고, 그것을 반환한다.
두 번째 find문에서는 이전에 1차 캐시에 해당 값을 미리 저장해뒀으므로, 그것을 반환한다.
두 가지 모두 1차 캐시로부터 반환된 동일한 값이므로, 같다.
1차 캐시로 반복 가능한 읽기(Repeatable Read) 등급의 트랜잭션 격리 수준을 DB가 아니라, 애플리케이션 차원에서 제공한다.
✅ 엔티티 등록 : 트랜잭션을 지원하는 쓰기 지연
EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
//엔티티 매니저는 데이터 변경시 트랜잭션을 시작해야 한다.
transaction.begin(); // [트랜잭션] 시작
em.persist(memberA);
em.persist(memberB);
//기본적으로는 여기까지 INSERT SQL을 데이터베이스에 보내지 않는다. (예외도 있긴 함 후에 배움)
//커밋하는 순간 데이터베이스에 INSERT SQL을 보낸다.
transaction.commit(); // [트랜잭션] 커밋
위 코드의 과정을 아래 사진으로 자세히 보자.
결국 persist 과정에서는 1차 캐시에 존재하다가, commit()을 한 후에야 같은 transaction 내의 1차 캐시에 존재하는 값들을 그제서야 DB에 일괄 저장한다.
✅ 엔티티 수정 : 변경 감지 (Dirty Checking)
EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
transaction.begin(); // [트랜잭션] 시작
// 영속 엔티티 조회
Member memberA = em.find(Member.class, "memberA");
// 영속 엔티티 데이터 수정
memberA.setUsername("hi");
memberA.setAge(10);
//em.update(member) 이런 코드가 있어야 하지 않을까?
transaction.commit(); // [트랜잭션] 커밋
commit 시점에 flush가 발생한다. (변경 감지)
아래 사진은 변경 감지의 자세한 과정이다.
먼저 transaction.commit()을 하면 flush가 발생한다.
엔티티 SnapShot과 1차 캐시를 비교하여, 다른 것이 있다면 Update 쿼리를 생성한다.
쓰기 지연 SQL 저장소에 먼저 쿼리를 날리고, 그것을 DB에 flush 및 commit 하여 변경사항을 적용한다.
수정 시 별도의 persist 과정은 필요 없다.
✅ 엔티티 삭제
// 삭제 대상 엔티티 조회
Member memberA = em.find(Member.class, "memberA");
em.remove(memberA); // 엔티티 삭제
📌 Flush
✅ 플러시?
영속성 컨텍스트의 변경 내용을 실제 DB에 반영하는 것을 의미한다.
변경 감지(Dirty Checking) 발생 시, 수정된 엔티티를 쓰기 지연 SQL 저장소에 먼저 등록한다.
쓰기 지연 SQL 저장소의 쿼리를 DB에 최종 전송한다.
결국 트랜잭션이라는 단위가 중요하다.
commit 직전에만 동기화 하면 된다.
✅ 영속성 컨텍스트를 Flush 하는 방법
1. em.flush() : 플러시 직접 호출
2. 트랜잭션 호출 : 플러시 자동 호출
3. JPQL 쿼리 실행 : 플러시 자동 호출
Flush를 실행한다고 해서 1차 캐시가 없어지거나 하지 않고, 그대로 존재한다.
DB에 반영만 되는 것이다.
✅ JPQL 쿼리 실행 시 플러시가 자동 호출되는 이유
em.persist(memberA);
em.persist(memberB);
em.persist(memberC);
//중간에 JPQL 실행
query = em.createQuery("select m from Member m", Member.class);
List<Member> members= query.getResultList();
데이터베이스와 영속성 컨텍스트 간 동기화를 보장하기 위해서다.
만약 JPQL 쿼리 실행 때 플러시가 호출되지 않는다면 최근 변경한 데이터가 쿼리 결과에 반영되지 않을 수 있고, 이는 잘못된 결과를 초래할 수 있다.
JPQL 쿼리 수행 시, 플러시가 자동적으로 수행되므로 개발자가 별도로 신경쓸 것은 없어 편의성이 좋다.
✅ 플러시 모드 옵션
em.setFlushMode(FlushModeType.COMMIT)
FlushModeType.COMMIT : commit 할 때만 자동 플러시
FlushModeType.AUTO : commit이나 쿼리 실행 시 자동 플러시 (기본 값)
📌 준영속 상태
✅ 준영속 상태
영속 상태의 엔티티가 영속성 컨텍스트에서 분리 되는 것을 말한다. (detached)
준영속 상태가 되면, 영속성 컨텍스트가 제공하는 기능들을 사용할 수 없다.
✅ 준영속 상태로 만드는 방법
1. em.detach(entity) : 특정 엔티티만 선택하여 준영속 상태로 전환한다.
2. em.clear() : 영속성 컨텍스트 완전 초기화
3. em.close() : 영속성 컨텍스트 종료
'Backend > JPA' 카테고리의 다른 글
[자바 ORM 표준 JPA 프로그래밍 - 기본편] 6. 다양한 연관관계 매핑 (0) | 2024.01.04 |
---|---|
[자바 ORM 표준 JPA 프로그래밍 - 기본편] 5. 연관관계 매핑 기초 (0) | 2024.01.04 |
[자바 ORM 표준 JPA 프로그래밍 - 기본편] 4. 엔티티 매핑 (0) | 2024.01.04 |
[자바 ORM 표준 JPA 프로그래밍 - 기본편] 2. JPA 시작하기 (0) | 2024.01.03 |
[자바 ORM 표준 JPA 프로그래밍 - 기본편] 1. JPA 소개 (1) | 2024.01.03 |
개발자가 되고 싶어요.