[JPA] 영속성 컨텍스트
✔ JPA에서 가장 중요한 2가지
객체와 RDB의 테이블을 어떻게 매핑?
영속성 컨텍스트, JPA 내부 동작이 어떻게 되는가?
- 객체와 관계형 데이터베이스 매핑하기( Object Relational Mapping )
- 객체와 관계형 데이터베이스를 어떻게 매핑할 것인가?
- DB와 객체를 어떻게 설계하고 중간의 JPA를 어떻게 사용할 것인가?
- 영속성 컨텍스트
- 실제 JPA가 내부에서 어떻게 동작 하는가?
📌 영속성 컨텍스트 1-1
- JPA를 이해하는데 가장 중요한 용어
- 엔티티를 영구 저장하는 환경
- 영속성 컨텍스트는 눈에 보이지 않는다
- 영속성 컨텍스트는 엔티티 매니저를 통해 접근한다
- EntityManager.persist(entity);
- 여기서 persist 메서드는 DB에 저장하는게 아니라, 영속성 컨텍스트에 저장하는 것을 의미
- 엔티티 매니저를 생성하면 그 안에 1:1로 영속성 컨텍스트가 생성이 된다.
- 엔티티 매니저 안에 눈에 보이지 않는 공간이 생긴다 생각
엔티티 매니저와 영속성 컨텍스트의 관계
엔티티 매니저 안에 눈에 보이지 않는 PersistenceContext가 생긴다
엔티티의 생명주기
-
비영속 ( new/transient )
- 영속성 컨텍스트와 전혀 관계가 없는 새로운 상태
Member member = new Member(); member.setId(105L); member.setName("HelloJPA"); // em.persist(member)에 넣어야 하는데 넣지 않았다
-
영속 ( managed )
- 영속성 컨텍스트에 관리되는 상태
Member member = new Member(); member.setId(105L); member.setName("HelloJPA"); em.persist(member); // em.persist(member)를 통해 영속 상태가 되었다
-
준영속 ( detached )
- 영속성 컨텍스트에 저장되었다가 분리된 상태
Member member = new Member(); member.setId(105L); member.setName("HelloJPA"); em.persist(member); em.detached(member); //영속성 컨텍스트의 상태를 준영속으로 변경 및 분리
-
삭제 ( remove )
- 삭제된 상태
Member member = new Member(); member.setId(105L); member.setName("HelloJPA"); em.persist(member); em.remove(member);
📌 영속성 컨텍스트의 이점 1-2
애플리케이션과 DB사이에 보이지 않는 어떤 부분 존재?
- 1차 캐시
- 동일성 보장
- 트랜잭션을 지원하는 쓰기 지연 ( transaction write-behind )
- 변경 감지 ( Dirty Checking )
- 지연 로딩 ( Lazy Loading )
📌 영속성 컨테스트
✅ 엔티티 조회, 1차 캐시
- 영속성 컨텍스트는 내부에 1차 캐시를 가지고 있다
- 키(Key)는 @Id, 값(Value)는 Entity가 된다
//비영속 상태
Member member = new Member();
member.setId("member1");
member.setUserName("회원1");
//엔티티를 영속
em.persist(member);
Member member2 = new Member();
member.setId("member2");
member.setUserName("회원2");
// member2 저장
em.persist(member2);
// 영속성 컨텍스트에서 조회, 없을 경우 DB 접근
em.find(Member.class, "member2");
//1차 캐시 영역
+--------------------+
|@id Entity |
|"member1" member1|
|"member2" member2|
| |
+--------------------+
✅ 조회를 하는경우
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction(); // 트랜잭션을 얻는다
tx.begin(); // 트랜잭션 시작 -> DB 트랜잭션 시작
//여기서 가져온 Entity는 DB에서 가져온 값으로, 1차캐시 영역에 저장이 된다
Member a = em.find(Member.class, "member1");
//여기서 가져온 Entity는 DB가 아닌, 1차 캐시에서 값을 가져온다
Member b = em.find(Member.class, "member1");
Application 최초 로드(load) 시점에서는 DB에서 값을 조회한다.
후에 해당 객체(Entity)를 1차 캐시에 저장한다.
위 같은 상황에서 JPA는 우선 1차 캐시에 해당 값이 존재하는지 확인을 한다. 만약 캐시에 해당 값(member1 obj)가 존재한다면 DB에 접근하지 않고 해당 캐시에서 값을 조회하여 반환 해준다. (없을 경우엔 DB에 접근하여 값을 조회 한다)
또한 @Id member2가 영속성 컨텍스트에 존재하지 않는 경우에는 DB에서 해당 member2 값을 조회한 후에 영속성 컨텍스트에 member2를 등록한 후 해당 객체(member2)를 반환한다.
엔티티 매니저는 트랜잭션 단위로 만들고 데이터베이스 트랜잭션이 끝나는 경우 모든 영속성 컨텍스트를 지운다. (1차 캐시도 같이 날라감) 위 같은 이유로 인해
성능에 상당한 이점을 주지는 않는다.
✅ 영속 엔티티의 동일성 보장
Member a = em.find(Member.class, "member1");
Member b = em.find(Member.class, "member1");
System.out.println(a == b); //동일성 비교 True
동일한 트랜잭션 단위 내에서 수행을 해야 TRUE가 나온다.
Java Collection에서 값을 꺼내 비교를 하면 TRUE를 반환 하는 것처럼
JPA는 동일한 Entity의 동일성을 보장한다.
1차 캐시로 반복 가능한 읽기(REPEATABLE READ) 등급의 트랜잭션 격리 수준을 데이터베이스가 아닌 애플리케이션 차원에서 제공한다.
✅ 엔티티 등록: 트랜잭션을 지원하는 쓰기 지연
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
// 트랜잭션을 얻는다
EntityTransaction tx = em.getTransaction();
// 트랜잭션 시작 -> DB 트랜잭션 시작
tx.begin();
em.persist(memberA);
em.persist(memberB);
//여기까지 INSERT SQL을 DB에 보내지 않는다
//커밋하는 순간 flush가 되면서 데이터베이스에 모든 INSERT SQL을 보낸다
tx.commit();
영속성 컨텍스트 안에는 1차 캐시 뿐만 아니라, 쓰기 지연 SQL 저장소가 존재한다. 위 코드 에서 persist(memberA)를 통해 memberA가 1차 캐시로 들어가고, 동시에 JPA가 해당 엔티티를 분석한다. 후에 INSERT 쿼리를 생성하여 쓰기 지연 SQL 저장소에 저장
✅ 엔티티 수정
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
//1차 캐시에 저장
Member memberA = em.find(Member.class, 150L);
//스냅샷 떠 둔 부분이 1차 캐시에 존재 한다
memberA.setName("ZZZZZ");
//em.update(member) 이런 코드가 있어야 하지 않을까??
tx.commit();
+--------------------------------------+
|@id Entity 스냅샷 |
|"member1" member1 memberA 스냅샷 |
|"member2" member2 memberB 스냅샷 |
| |
+--------------------------------------+
✅ 변경 감지
JPA는 엔티티가 생성되는 시점에 최초로 한번 스냅샷을 생성한다.
✨ JPA는 값을 바꾸면 트랜잭션이 커밋되는 시점에 값(Entity)가 변경된다.
✨ 코드 작성 시 반드시 위 부분을 유의 하면서 작성하자.
- JPA는 Transaction Commit이 발생하면 내부적으로 flush()가 호출 된다.
- 해당 Entity(변경한)와 최초로 생성된 Entity의 스냅샷을 비교한다.
- 변경된 내용이 있는 경우 JPA는 UPDATE SQL을 생성하여 쓰기지연 SQL 저장소에 저장한다.
- Commit시 해당 쿼리가 실행된다.
📌 영속성 컨테스트: 플러시
플러시란 무엇이고 어떤 상황에서 발생이 되는지 알아보자.
✅ 플러시 발생하면 무슨일이 생기는가?
영속성 컨텍스트의 변경 내용을 데이터베이스에 반영 한다.
쉽게 말해 INSERT, UPDATE.. 등의 쿼리를 데이터베이스에 반영한다.
✨ 데이터베이스 트랜잭션이 발생하면 플러시(flush)가 발생한다는 부분이 중요하다.
- 변경 감지가 일어난다.
- 수정된 엔티티 쓰기 지연 SQL 저장소에 등록한다.
- 쓰기 지연 SQL 저장소의 쿼리를 데이터베이스에 전송(등록, 수정, 삭제 쿼리)
- 플러시는 영속성 컨텍스트를 지우는 개념이 아니다.
- 영속성 컨텍스트의 변경 내용(쓰기 지연 저장소의 내용)을 데이터베이스에 동기화
- 트랜잭션이라는 작업 단위가 중요 (커밋 직전에만 동기화)
✅ 영속성 컨텍스트를 플러시하는 방법
- em.flush()
- 수동 호출 (즉시 쿼리 호출)
- 트랜잭션 커밋
- 플러시 자동 호출
- JPQL 쿼리 실행
- 플러시 자동 호출
✅ JPQL 쿼리 실행 시 플러시가 자동으로 호출되는 이유
em.persist(memberA);
em.persist(memberB);
em.persist(memberC);
//실제 쿼리는 안날라간다 여기서
//중간에 JPQL 실행
//flush가 안 날라가면 데이터 조회 시 예외가 발생할 것이다
query = em.createQuery("select m from Member m", Member.class);
List<Member> members = query.getResultList();
- JPA는 JPQL이 있을 시에는 무조건 flush를 날린다.
- 위 같은 상황에서는 flush가 실행 되어야 하는 상황이다.
- DB에 값이 없는데 값을 조회하는 상황.
✅ 플러시 모드 옵션
em.setFlushMode(FlushModeType.COMMIT)
✨ 플러시 옵션은 가급적 AUTO로 사용하는 것을 권장한다.
- FlushModeType.AUTO
- 커밋이나 쿼리를 실핼할 때 플러시(가급적 AUTO로 사용)
- FlushModeTYpe.COMMIT
- 커밋할 때만 플러시
📌 영속성 컨텍스트: 준영속 상태
✅ 영속 상태
- 1차 캐시에 올라간 상황
- 조회를 했는데 1차 캐시에 없는 경우 해당 Entity를 1차 캐시에 적재한 상황
✅ 준영속 상태
- 영속 상태의 엔티티가 영속성 컨텍스트에서 분리(detached)
- 영속성 컨텍스트가 제공하는 기능을 사용 못함
✅ 준영속 상태 만드는 방법
- em.detach(entity)
- 특정 엔티티만 준영속 상태로 전환
- em.clear()
- 영속성 컨텍스트를 완전히 초기화
- em.close()
- 영속성 컨텍스트를 종료
댓글남기기