출처 - 인프런 김영한님 자바 ORM 표준 JPA 프로그래밍 - 기본편
인강의 내용이 있기 때문에 출처를 밝힙니다.
영속성 컨텍스트까지 복습했기에 기록 시작 ~
영속성 컨텍스트와 엔티티의 생명주기
영속성 컨텍스트 (Persistence Context) 는 엔티티를 영구 저장하는 환경으로, JPA 에서 가장 중요한 개념 중 하나이다.
논리적인 개념으로써 우리 눈에 보이지 않고, 영속 컨텍스트는 엔티티 매니저를 통해 접근할 수 있다.
엔티티는 크게 4가지의 생명주기를 가지고 있고, 엔티티가 생명주기 중 하나인 영속(managed) 상태라면, 영속 컨텍스트에 의해 관리되고
있는 상태라고 할 수 있다.
1. 비영속 (new/transient)
말 그대로 영속 컨텍스트에 의해 관리되고 있는 상태가 아닌 것.
엔티티를 생성하고 영속 상태로 만들지 않으면, 비영속 상태라고 할 수 있다.
Member member = new Member();
member.setId(1L);
member.setUsername("new MemberA")
2. 영속 (managed)
엔티티가 영속 컨텍스트에 의해 관리되고 있는 상태.
엔티티매니저.persist() 메서드를 통해 엔티티를 영속 상태로 만들 수 있다.
비영속 상태
Member member = new Member();
member.setId(1L);
member.setUsername("new MemberA");
영속 상태로 전환
em.persist(member);
3. 준영속 (detached)
엔티티를 영속 컨텍스트에서 분리한 상태.
사실상 비영속 상태와 같다.
Member 엔티티를 영속 상태에서 준영속 상태로 전환
em.detach(member);
엔티티 매니저 닫기
em.close()
엔티티 매니저 내부 영속 컨텍스트 날려버리기
em.clear()
4. 삭제 (removed)
엔티티를 영속 컨텍스트와 DB 에서 삭제.
엔티티 삭제
em.remove(member);
왜 영속 컨텍스트를 쓰는 것인가 ?
1. 1차 캐시
영속 컨텍스트 내부에는 1차 캐시라는 것이 존재하는데, 영속 상태의 엔티티는 모두 이곳에 차곡차곡 저장된다.
엔티티 매니저를 통해 엔티티를 조회할 때, 먼저 DB 에 접근하는게 아닌, 1차 캐시에 접근해서 엔티티가 있는지 찾는다.
만약 1차 캐시에 엔티티가 있다면 DB 에 접근하지 않기 때문에 SELECT 문이 따로 나오지도 않는다.
네트워크를 통해 DB에 접근하는 비용은 애플리케이션 내부 메모리에 접근하는 비용보다 비싸기 때문에 1차 캐시로 인한 성능상 이점이
꽤나 있을 것 같지만, 1차 캐시를 가지는 엔티티 매니저는 트랜잭션 단위이기 때문에 트랜잭션이 끝나면 1차 캐시도 지워진다.
찰나의 순간에만 성능상 장점이 있고, 사실 그렇게 유효한 성능상의 이점은 없다. 다만 1차 캐시를 통해 얻을 수 있는 구조적인 장점이 많다.
비영속 상태
Member member = new Member();
member.setId(1L);
member.setName("MemberA");
영속 상태
em.persist(member);
영속 콘텍스트에 1번 멤버에 대한 1차 캐시가 생성되었기 때문에 1번 멤버는 DB 가 아닌 1차 캐시에서 조회한다.
즉, SELECT 문이 따로 DB 로 날아가지 않는다.
Member findMemberA1 = em.find(Member.class, 1L);
MemberB 가 저장되어 있지 않다고 가정하면, 1차 캐시에 없기 때문에 이건 DB 에서 조회한 뒤 1차 캐시에 저장한다.
Member findMember2 = em.find(Member.class, 2L);
2. 영속 엔티티의 동일성 보장
1차 캐시의 장점 중 하나. 영속 컨텍스트에서 한 객체를 몇 번 조회하던 조회된 엔티티는 모두 같음을 보장할 수 있다.
즉, 한 트랜잭션 내에서 조회한 같은 엔티티는 모두 같은 참조 값을 가진다.
Member findMember1 = em.find(Member.class, 1L);
Member findMember2 = em.find(Member.class, 1L);
System.out.println(findMember1 == findMember2);
실행결과
true
3. 쓰기 지연
쓰기 지연이란, 영속 컨텍스트 내부에 변경이 발생됐을 때 DB 로 바로 쿼리를 보내지 않고 영속 컨텍스트 내부의 SQL 문 저장소에 쿼리를
모아뒀다가 영속 컨텍스트가 플러시(Flush) 하는 시점에 모아둔 쿼리를 한꺼번에 DB 로 보내는 기능이다.
즉, 영속 컨텍스트 내부에는 사실 1차 캐시 뿐 아니라, SQL 문 저장소가 존재하고, 1차 캐시에는 영속화 된 엔티티를, SQL 문 저장소는
쿼리문을 차곡차곡 모아두게 된다.
뭔가 지금까지는 em.persist() 하면 바로 쿼리문이 나가는 것으로 생각했는데 사실 이 때가 아님.. 확인해보자.
Setter 로 데이터를 셋팅했는데, 귀찮으니까 생성자를 하나 만들어주자. 기본 생성자도 만들어야 한다 !!
(엔티티는 기본 생성자를 생성해 DB 값을 Reflection 하기 때문에 꼭 기본 생성자가 필요)
아래 실행결과를 확인해보면 신기하게도 em.persist() 시점이 아닌, tx.commit() 시점에 쿼리문이 나가는 것을 확인할 수 있다.
트랜잭션을 commit 하게 되면 내부적으로 플러시가 발생하게 되고, 이 때 등록, 수정, 삭제한 엔티티를 DB 에 한꺼번에 반영하기
때문에 tx.commit() 시점에 쿼리문이 나가게 되는 것이다. 플러시에 대해서는 영속성 컨텍스트 이후 섹션에서 기록 ~
1. Member 엔티티에 생성자 하나 만들기
JPA 엔티티에서는 항상 기본 생성자가 필요하다. (public 아니여도 된다)
public Member() {}
public Member(Long id, String name) {
this.id = id;
this.name = name;
}
.. 생략 ..
2. Member 영속화
Member memberA = new Member(150L, "A");
sout("====")
em.persist(memberA);
sout("====")
sout("Before commit")
tx.commit();
sout("After commit")
실행 결과
===
===
Before commit
Hibernate:
/* insert hellojpa.Member
*/ insert
into
Member
(name, id)
values
(?, ?)
After commit
4. 변경 감지 (Dirty Checking)
말 그대로 변경을 자동으로 감지해서 DB 값과 엔티티 값을 동기화하는 것.
엔티티 매니저에는 따로 엔티티를 수정할 수 있는 메서드가 없지만, 엔티티는 당연히 수정될 수 있다.
수정하고 싶은 엔티티를 조회하고 해당 엔티티의 값을 변경하면, 자동으로 DB에 UPDATE 쿼리가 나가면서 변경 값이 DB에 저장되는데
이걸 변경 감지라고 하는 것이다. 근데 어떻게 이런 일이 일어날까 ?
3 단계로 알아보자.
1. 영속 컨텍스트가 플러시되는 시점에 스냅샷(1차 캐시에 값이 들어온 최초 시점의 상태)과 엔티티의 현재 상태를 비교한다.
2. 달라진 엔티티를 찾고, 달라진게 있다면 자동으로 UPDATE 쿼리가 생성되며 쓰기 지연 SQL 저장소에 쿼리가 저장된다.
3. 이제 UPDATE 쿼리가 DB 로 보내지면서 수정된 엔티티 값이 DB 에 반영된다 !
Member findMemberA = em.find(Member.class, 1L);
여기서 엔티티 값에 변화가 있기 때문에 스냅샷과 엔티티가 달라진다.
findMemberA.setName("MemberA");
트랜잭션 commit 시점에 항상 플러시가 발생하기 때문에 여기서 변경 감지를 실시한다.
tx.commit();
플러시 ?
플러시란 영속 컨텍스트 1차 캐시의 엔티티 값과 DB 값을 동기화하는 작업을 말한다.
이름에서 오해할 수 있는데, 플러시가 발생한다고 영속 컨텍스트가 비워지는 건 아니고 변경 감지에 의해 쓰기 지연 SQL 저장소에 저장된
SQL 쿼리문을 DB 에 반영하는 작업을 수행할 뿐이다.
플러시는 다음과 같은 3 가지 상황에서 발생하고 1번에 대해 자세히 알아보도록 하자.
1. em.flush() 직접 호출
2. 트랜잭션 commit 시점에 자동으로 호출
3. JPQL 쿼리 실행 시 자동으로 호출
em.flush() 직접 호출
우리는 트랜잭션 commit 이전에 어떤 쿼리문이 나가는지 확인할 수 없는데, commit 때 변경 감지에 의해 1차 캐시의 스냅샷과 엔티티를
비교하고 INSERT, UPDATE 등 쿼리문을 저장, 이후에 DB 에 쿼리문을 날리기 때문이다.
즉, 직접 플러시를 호출하게 되면, commit 시점이 아니더라도 미리 쿼리문을 확인하거나 DB 에 변경 사항을 반영할 수 있는 것이다.
코드로 확인해보자. (사실 플러시를 직접 호출하는 상황은 많이 없다)
플러시를 직접 호출한 시점에서 변경 감지에 의해 INSERT 문이 DB 로 날아간 것을 확인할 수 있다.
만약 플러시를 직접 호출하지 않았다면, 트랜잭션 commit 시점에서 INSERT 문이 나갔을 것이다.
Member member = new Member(200L, "Member200");
em.persist(member);
System.out.println("===Before");
em.flush();
System.out.println("===After");
tx.commit();
실행 결과
===Before
Hibernate:
/* insert hellojpa.Member
*/ insert
into
Member
(name, id)
values
(?, ?)
===After
'JPA' 카테고리의 다른 글
[인강 복습] JPA 기본편 다양한 연관관계 매핑 (#5) (0) | 2022.07.06 |
---|---|
[인강 복습] JPA 기본편 연관관계 매핑 기초 (#4) (0) | 2022.07.05 |
[인강 복습] JPA 기본편 요구사항 분석과 기본 매핑 까지 (#3) (0) | 2022.06.16 |
[인강 복습] JPA 기본편 JPA 시작하기 까지 (#1) (0) | 2022.06.10 |