김영한 강사님의 해당 강의를 통해 해당 글을 작성하였습니다.
들어가기 전에
이렇게가 있다. 우리는 이들 중 '영속성 컨텍스트'에 대해서 알아보자.
- 'EntityManagerFactory'를 통해 고객의 요청이 올 때마다 'Entity Manger'를 생성해준다.
- 이 'EntityManager'은 내부적으로 '커넥션 풀'을 이용하여 DB를 사용한다.
EntityManger.persist(entity)
크게 이렇게 4가지로 나뉘게 된다.
• 비영속 (new/transient)
영속성 컨텍스트와 전혀 관계가 없는 새로운 상태
• 영속 (managed)
영속성 컨텍스트에 관리되는 상태
• 준영속 (detached)
영속성 컨텍스트에 저장되었다가 분리된 상태
• 삭제 (removed)
삭제된 상태
이 생명주기에 대해서 좀 더 자세히 알아보자!! 🤔
//객체를 생성한 상태(비영속)
Member member = new Member();
member.setId("member1");
member.setUsername("회원1");
- 정말 객체를 생성하고 이에 대해 세팅만 한 상태를 말한다. =>'영속성 컨텍스트'에 들어가지 x
=> JPA에 관계없음.
//객체를 생성한 상태(비영속)
Member member = new Member();
member.setId("member1");
member.setUsername(“회원1”);
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
//객체를 저장한 상태(영속) => '컨텍스트'에 담김 => 1차캐시에
em.persist(member);
persist()
를 해준다고 DB에 바로 저장되는 것이 x
-> '영속성 컨텍스트'에 있는 1차 캐시에 저장됨- DB에 언제 저장되냐?
commit()
때 저장된다.
-> '영속성 컨텍스트'에서 DB로 쿼리가 날아간다.
웹 애플리케이션 제작에서 더 자세히 다뤄볼 것이지만 그래도 개념을 한 번씩 보고가보자!
//회원 엔티티를 영속성 컨텍스트에서 분리, 준영속 상태
em.detach(member);
앞에서도 말했지만 영속의 경우는
persist()
,em.find()
나 jpa를 통해서 조회를 했으나 '영속성 컨텍스트'에 없어서 DB에서 꺼내와서 '영속성 컨텍스트'에 올리는 것도 => 영속!
• em.detach(entity)
- 특정 엔티티만 준영속 상태로 전환
- JPA에서 관리하지 않겠다는 뜻이다.
- 이 직후 commit()
하면 아무 일도 일어나지 않는다.
ㄴ> JPA가 관리하지 않겠다고 한거라...
• em.clear()
• em.close()
//객체를 삭제한 상태(삭제)
em.remove(member);
• 1차 캐시
• 동일성(identity) 보장
• 트랜잭션을 지원하는 쓰기 지연(transactional write-behind)
• 변경 감지(Dirty Checking)
• 지연 로딩(Lazy Loading)
여기서 말하는 1차 캐시를 영속성 컨텍스트와 비슷하게 생각해도 좋다.
em.persist()
라는 것은 1차 캐시에 저장을 하겠다는 뜻인데
Member member = new Member();
member.setId("member1");
member.setUsername("회원1");
//1차 캐시에 저장됨
em.persist(member);
//1차 캐시에서 조회
Member findMember = em.find(Member.class,"member1");
어떻게 생겼을지 그림으로 직접 보자.
여기서 key가 PK, 값은 Entity가 되는데 '조회'를 시도했을 때 가장 먼저 1차 캐시에서 해당 객체를 찾는다.(물론 DB에 존재한다)
예를 들어 앞의 코드에서 member1을 조회했고 그 후 member2를 조회하라고 한다면...
- DB에서 조회하고(DB에 저장되어 있어야 함)
- 1차 캐시에다 가져와서 저장시킴
- 1차캐시 (현재로 이해하면 영속성 컨텍스트)에 저장된거를 반환
=> 그 이후에 얘를 다시 찾는다고 하면, 바로 1차캐시(영속성 컨텍스트)에서 찾을 수 있는거지
사실상 큰 도움은 안된다고 한다. 영속성 컨텍스트(EntityManager)은 데이터 베이스 트랜잭션 단위로 이루어져 있어서, 이것이 끝날 때 영속성 컨텍스트도 다 날라간다.
찰나의 순간에만 이득이 있기 때문에 모두와 공유하는 것이 아니기도 하고 성능적인 이점이 그렇게 크진 않다.
Member a = em.find(Member.class, "member1");
Member b = em.find(Member.class, "member1");
System.out.println(a == b); //동일성 비교 tru
- JPA가 영속 엔티티의 동일성 보장
- 마치 자바 컬렉션에서 똑같은 레퍼런스가 있는 것을 꺼낸 것처럼 => 1차캐시가 있기 때문에 가능하다.
- 1차 캐시로 반복 가능한 읽기(REPEATABLE READ) 등급의 트랜잭션 격리 수준을 데이터베이스가 아닌 애플리케이션 차원에서 제공
=> 조금 어려운 내용이라 '같은 트랜잭션 내'에서 비교하면 아 레퍼런스 변수니까 TRUE가 됬구나 이렇게 생각하자.
EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
//엔티티 매니저는 데이터 변경시 트랜잭션을 시작해야 한다.
transaction.begin(); // [트랜잭션] 시작
em.persist(memberA);
em.persist(memberB);
//여기까지 INSERT SQL을 데이터베이스에 보내지 않는다.
//커밋하는 순간 데이터베이스에 INSERT SQL을 보낸다.
transaction.commit(); // [트랜잭션] 커밋
- 1차 캐시에 들어감
- JPA가 얘를 분석해서
INSERT
SQL을 생성해서 '쓰기 지연 SQL 저장소'에 미리 저장시켜 쌓아놓음commit()
하게 되면,flush()
되어서 얘네들이 DB에 한 번에 날라감
JPA는 내부적으로 리프렉션 등이 쓰이기 때문에 동적으로 객체를 생성해야 한다.
결론: '기본 생성자'가 있어야 한다.
-> 꼭 public 일 필요 (x)
들어가기 전 우리는 버퍼링이라는 개념에 대해 알아야 한다.
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()
가 일어난다.
- 1차 캐시에선 '스냅샷'이란게 있다
∴ '스냅샷' 이란건 값을 DB에서 처음 읽어와서 집어넣었을 때건, 딱 읽어온 최초 시점을 찍어놓은 것JPA
가 Entity와 스냅샷을 비교함- 이 둘이 비교했는데.. 바꼈다면?
-> '쓰기 지연 SQL 저장소'에 UPDATE 쿼리를 생성해 만들어 둔다 => 변경감지- 이것을 DB에 반영 -> commit 해준다.
간단히 말하자면 즉시 로딩은 데이터를 조회할 때 연관된 데이터까지 한 번에 불러오는 것이고, 지연 로딩은 필요한 시점에 연관된 데이터를 불러오는 것이라고 할 수 있다.
지연 로딩을 사용하면 관련된 SQL문을 한 번에 불러오는 것이 아닙니다.
A. 테이블 설계가 복잡해질수록, 하나의 엔티티가 참조하는 테이블들은 늘어날 테고, 그에 따른 쿼리문도 굉장히 길어지겠죠. 이런 복잡한 쿼리문을 본 개발자는 해당 도메인이 어떻게 설계되었는지 확인해보아야 하고, 논리적인 Layer 분리가 이뤄지지 않은 셈이 되죠. 이러한 부분은 유지 보수를 힘들게 만들 수 있습니다.
또한 시간이 지나면 변화되고 추가가 되기 마련이기에 '성능 이슈'가 발생할 가능성을 줄이고자 즉시로딩 보다는 지연로딩을 더 권장합니다.
commit
시점에 DELETE 쿼리가 나간다.//삭제 대상 엔티티 조회
Member memberA = em.find(Member.class, “memberA");
em.remove(memberA); //엔티티 삭제
commit()
이 일어나면 flush()
가 발생• em.flush()
- 직접 호출
• 트랜잭션 commit()
- 플러시 자동 호출
• JPQL 쿼리 실행 - 플러시 자동 호출
-> DB에 쿼리로 라도 날라가야 하는데 아예 날라가지 않기 떄문에 자동으로 호출해주어야 한다.
em.setFlushMode(FlushModeType.COMMIT)
• FlushModeType.AUTO
:커밋이나 쿼리를 실행할 때 플러시 (기본값)
-> 가급적 손 대지 말고 이거로 쓰자...
• FlushModeType.COMMIT
: 커밋할 때만 플러시
• 영속성 컨텍스트를 비우지 않음
• 영속성 컨텍스트의 변경내용을 데이터베이스에 동기화
• 트랜잭션이라는 작업 단위가 중요
-> 어쨌든 commit 직전에만 변경사항을 동기화 해주면 됨