영속성 컨텍스트
참고
Persistence Context
jpa의 핵심적인 기능으로, 영속성을 관리해준다. 눈에 보이지 않는 추상적인 개념으로, EntityManager를 통해 영속성 컨텍스트의 기능을 이용할 수 있다.
영속성 컨텍스트를 통해 다음과 같은 이점을 얻을 수 있다.
- 1차 캐시
- 동일성 보장
- 쓰기 지연
- 더티 체킹(변경 감지)
- 지연 로딩(lazy loading)
Entity Lifecycle
영속성 컨텍스트의 관리를 받는 객체를 엔티티라고 한다. 엔티티에는 다음과 같은 생명주기가 있다.
- 비영속 (new)
- 엔티티 객체가 생성됐지만, 영속성 컨텍스트가 관리하지는 않는 상태
- 영속 (persist)
- 영속성 컨텍스트가 관리중인 상태
- 엔티티 매니저의 persist() 메서드를 사용하면 영속 상태가 된다.
- 엔티티 매니저로 조회해온 객체는 영속 상태이다.
- 준영속 (detach)
- 영속성 컨텍스트가 관리중이었으나, 관심을 끊은 상태
- 영속성 컨텍스트의 기능을 전혀 받지 못한다.
- 엔티티 매니저의 detach() 메서드를 사용하면 준영속 상태가 된다.
- 엔티티 매니저의 claer() 메서드를 사용하면 해당되는 모든 영속 객체가 준영속 상태가 된다.
- 영속 상태의 객체가 있을 때, 엔티티 매니저가 사라지면 그 객체는 준영속 상태가 된다.
- 삭제 (removed)
- 삭제되어 DB와 영속성 컨텍스트에서 사라져 손댈 수 없는 상태
준영속 vs 비영속
먼저 엔티티 객체가 언제 어떻게 준영속이 되는 걸까?
- EntityManager의 detach() 메서드를 사용한다.
- 영속 상태의 엔티티 객체들이 있다고 할 때, 엔티티 매니저의 clear()를 사용하면 전부 준영속 상태가 된다.
- 영속 상태의 엔티티 객체들이 있다고 할 때, 트랜잭션도 끝났고 영속성 컨텍스트도 죽어버렸을 때 영속 상태였던 엔티티 객체들이 전부 준영속 상태가 된다.
그래서 준영속과 비영속은 어떤 차이인가?
- 준영속은 적어도 한 번은 영속 상태를 유지한 시절이 있는 객체
비영속은 단 한 번도 영속 상태를 유지한 적이 없는 객체
⇒ 영속 상태가 되려면 식별자가 필수로 존재해야 하기 때문에~ 준영속은 무조건 id 값을 갖고 있다~
비영속은 있을 수도 있고 없을 수도 있다
1차 캐시
서버의 메모리에 접근하는 것에 비해 DB에 접근하는 것은 비용이 많이 든다. 영속성 컨텍스트는 이를 보완하기 위해 캐시를 두어 DB 접근을 최소화한다.
(영속성 컨텍스트 이미지)
위의 그림을 보면,
- member1이란 키 값을 통해 멤버 엔티티를 조회한다. ⇒ 영속성 컨텍스트의 1차 캐시에 저장되있지 않은 것을 확인한 후에, DB로부터 값을 조회한다. ⇒ 조회한 값으로 엔티티 객체를 만들어 가져오며, 그 엔티티 객체는 1차 캐시에 저장된다.
- member1이란 키 값을 통해 멤터 엔티티를 다시 조회한다. ⇒ 영속성 컨텍스트의 1차 캐시에 member1을 가진 멤버 객체가 있는지 확인한다. ⇒ 있는 것을 확인하고 DB접근 없이 엔티티 객체를 가져온다.
캐시 덕분에 디비 접근을 두 번 할 것을 한 번으로 끝내버렸다~
추가로
- 1차 캐시는 식별자와 함께 엔티티를 매칭해서 저장한다.
- 그렇기 때문에 id 값이 없는 엔티티 객체는 영속 상태가 될 수 없다.
동일성 보장
영속성 컨텍스트에서 같은 값을 여러 번 조회를 하면, DB 조회 없이 1차 캐시에서 엔티티 객체를 받아온다. 그 덕에 jpa를 통해 여러번 조회하여 얻은 다수의 객체들은 인스턴스 참조값이 다 같다.
그렇기 때문에 == 비교를 통해서도 같다는 결과를 받을 수 있다.
언제까지?
하나의 엔티티 매니저 객체를 사용하는 동안에만 보장된다.
쓰기 지연
1
2
3
4
5
6
7
8
9
System.out.println(1);
em.persist(member);
System.out.println(2);
em.remove(member);
System.out.println(3);
위 코드를 실행하면 콘솔에 어떻게 찍힐 거 같음?? (트랜잭션 생략, member는 영속 상태라고 가정)
결과는
1
2
3
[insert query]
[delete query]
이렇다.
- 영속성 컨텍스트는 로직을 수행하며 작성한 쿼리들을 쓰기 지연 sql 저장소에 차곡차곡 쌓아둔다.
- 트랜잭션이 커밋되어 커넥션을 반납할 때, 쓰기 지연 sql 저장소에 쌓인 쿼리를 한 번에 db에 날린다.
왜 그렇게 할까?
- 결론부터 말하면 성능 때문이다.
- 커밋될 때 한 번에 모아둔 쿼리를 날리면, DB 커넥션을 오래 잡아두지 않아도 된다.
- 트랜잭션이 테이블을 오래 잡아두지 않아도 된다.
- 불필요한 쿼리를 줄여준다.
불필요한 쿼리를 어째 줄여줄까
1
2
3
4
5
6
7
Member member = em.find(Member.class, 1L);
Thread.sleep(1000L);
member.setName("babo");
member.setName("boba");
member.setName("bogeun");
위 코드를 돌리면 몇 개의 쿼리가 날라갈까? (트랜잭션 생략)
위 코드를 돌리면 커넥션을 얼마나 잡아두고 있을까?
결과는
[select query]
[update query]
두 개이다.
- 만약 변경이 일어날 때마다 쿼리를 날렸다면 적어도 2개의 쿼리는 더 날렸을 것이다.
- 만약 트랜잭션 시작과 끝까지 db를 잡아두고 있었다면, 최소 1초 이상 db가 묶여있었을 것이다.
왜 내 생각대로 안 움직이지..?
1
2
3
4
5
Member member = new Member();
member.setName(babo);
em.persiste(member);
System.out.println("내가 먼저!!");
위 코드를 돌리면 어떻게 될까? (트랜잭션 생략)
결과는
[insert query]
내가 먼저!!
와 같다.
위에서 본 대로면 출력이 먼저되고 쓰기 지연 때문에 커밋될 때 쿼리가 날라가야 되는 거 아녀?
- 엔티티는 id 값이 있어야지만 영속 상태가 될 수 있다.
- 그 때문에 @Entity 어노테이션 달면 id 없다고 난리를 친다.
- 사실 위 예제의 Member 클래스의 id는 @GeneratedValue(starategy = GenerationType.IDENTITY)이다
- 예제만 보고 딱 맞출 수 없다.
- IDENTITY의 경우 db에게 아이디 값 시퀀스 전략을 맡겨버린다.
- 때문에 db에 쿼리를 날려야 id 값을 알 수가 있다.
- 그래서 이 경우 어쩔 수 없이 쓰기 지연이고 뭐고 내부터 디비 좀 쓸게! 하고 날려버린다.
변경 감지
1
2
3
4
Member member1 = em.find(Member.class, 1L);
Member member2 = em.find(Member.class, 2L);
member1.setName("babo");
위 코드를 돌리면 몇 개의 쿼리가 날라갈까? (트랜잭션 생략)
결과는
[select query]
[select query]
[update query]
member1의 변경을 저장하는 코드가 없었는데 왜 업데이트 쿼리가 날라가지?
- member1과 member2는 EntityManager를 통해 조회해 온 엔티티 객체라 영속 상태이다.
- 영속성 컨텍스트는 엔티티 객체가 영속 상태가 되는 시점의 상태를 스냅샷으로 저장해둔다.
- 커밋 시점에 영속 상태인 엔티티 객체가 스냅샷과 다른 것을 감지하여 업데이트 쿼리를 날려주는 것이다.
JPA 기본 메서드
- persist(entity)
- 엔티티 객체를 영속 상태로 바꿔준다.
- 앞에도 질리도록 언급했지만, 식별자 값이 없는 엔티티 객체는 영속 상태가 될 수 없다.
- detach(entity)
- 영속 상태의 엔티티 객체를 준영속 상태로 바까준다.
- remove(entity)
- 영속 상태의 엔티티 객체를 remove 한다(실제 디비에서 지워버림)
- flush()
- 변경 감지를 해서 쓰기 지연 sql 저장소에 있는 쿼리들을 전부 커밋 해버린다.(디비와 동기화)
- 얘도 트랜잭션 내에서 사용이 되어야 한다
- 이거 쓰면 영속성 컨텍스트(1차 캐시 등)가 싹 날라가나?
- ㄴㄴ 쓰기 지연 sql 저장소를 즉시 솩 커밋해서 비워버림
- 기존에 영속 상태이던 객체들은 그대로 잘 동작함
- 얘는 거의 직접 쓸 일이 없다~ 그럼 간접적으로 언제 쓰이나?
- 트랜잭션 커밋이 일어날 때
- JPQL 쿼리를 실행할 때
- 이거는 왜지??
- em.persist(member1); em.persist(member2); em.persist(member3); [jpql로 모든 member 조회]
- 이 경우 뒤에 영속 상태가 된 객체들이 jpql문으로 인해 조회가 되어야 하기 때문에, jpql문을 실행하기 전에 flush로 디비에 반영시키는 것이다.
- 엔티티 매니저에 flush 속성을 설정해줄 수 있다.
- FlushModeType.AUTO
- 커밋이나 쿼리를 실행할 때 플러쉬한다.
- 얘가 default이며, 얘를 쓰는게 안전하다. 이유는 위에 있음
- FlushModeType.COMMIT
- 커밋 시에만 플러쉬한다.
- FlushModeType.AUTO
- clear()
- 영속성 컨텍스트를 싹 비워준다.