영속성 관리
📌 영속성 컨텍스트(Persistence Context)란
엔티티를 영구 저장하는 환경으로 EntityManager를 통해 접근할 수 있다.
일반적으로 JPA에서는 persist 함수를 통해 해당 엔티티를 영속화(영속성 컨텍스트에 저장)한다.
EntityManger.persist(entity); //entity를 영속화
💡 Spring Data JPA의 영속화
Spring Data JPA에서는 Repository를 상속받은 JpaRepository라는 인터페이스를 제공한다. Repository 인터페이스를 정의하면 해당 Entity의 데이터를 사용할 수 있는데, 이것은 내부적으로 EntityManger 가 직접 대상 Entity의 데이터를 관리하기 때문이다. 그리고 Spring Data JPA에서 Repository의 내부 구현체를 자동으로 생성시켜 주기 때문에 굳이 직접 EntityManger를 사용하여 Persistence Layer를 구현하지 않아도 된다.
Reference:
https://jogeum.net/9
📌 엔티티의 생명 주기
🔎 비영속(new/transient)
영속성 컨텍스트와 전혀 관계가 없는 새로운 상태
//객체를 생성한 상태(비영속)
Member member = new Member();
member.setId("member1");
member.setUsername("회원1");
🔎 영속(managed)
영속성 컨텍스트에서 관리되는 상태
entitymanager.persist(entity)를 한 상태
하지만 실제로 데이터베이스에 저장된 상태는 아니며, 트랜잭션을 커밋하는 시점에 쿼리가 날아가 저장된다.
🔎 준영속(detached)
영속성 컨텍스트에 저장되었다가 분리된 상태
//회원 엔티티를 영속성 컨텍스트에서 분리, 준영속 상태
em.detach(member);
준영속 상태에서는 영속성 컨텍스트가 제공하는 이점을 누릴 수 없다.
✔ 준영속 상태로 만드는 방법
- em.detach(entity) ➡ 특정 엔티티만 준영속 상태로 전환
- em.clear() ➡ 영속성 컨텍스트를 초기화
- em.close() ➡ 영속성 컨텍스트를 종료
🔎 삭제(removed)
삭제된 상태
📌 영속성 컨텍스트의 이점
🔎 1차 캐시(=영속성 컨텍스트)
엔티티를 영속화하면 1차 캐시에 다음과 같이 Map 형식으로 저장된다.
find 함수를 실행하면 데이터베이스를 조회하기 전에 1차 캐시부터 조회하기 때문에 더 나은 검색 성능을 볼 수 있다. 또한, 1차 캐시에 해당 엔티티가 없을 경우
데이터베이스에서 조회한 후 1차 캐시에 저장하므로, 이후 해당 장점을 누릴 수 있다.
사실, 1차 캐시는 트랜잭션이 살아 있는 동안에만 즉 고객의 요청 1번을 처리하는 동안에만 생존하므로 큰 이점은 아니다.
영속성 컨텍스트는 트랜잭션의 ACID를 위해 트랜잭션 작업 단위로 사용하도록 해야 한다.
🔎 동일성 보장
Java 컬렉션에서 데이터를 조회했을 때처럼 동일성을 보장한다.
이것은 동일 트랜잭션 내에서 동일 객체 조회는 동일한 것으로 간주하기 때문에 가능하다.
Member findMember1 = em.find(Member.class, 101L);
Member findMember2 = em.find(Member.class, 101L);
System.out.println("result = " + (findMember1 == findMember2)); //true
tx.commit(); //커밋
🔎 트랜잭션을 지원하는 쓰기 지연(transactional write-behind)
JPA는 INSERT 해야 할 엔티티들을 쓰기 지연 SQL 저장소라는 곳에 모아둔다. 이렇게 요청을 모아두었다가 커밋하는 순간 한 번에 쿼리를 날린다.
이처럼 벌크 연산은 쿼리 소모를 줄여 성능 이점을 가져다준다.
EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
//엔티티 매니저는 데이터 변경 시, 트랜잭션을 시작해야 함
transaction.begin(); //트랜잭션 시작
em.persist(memberA);
em.persist(memberB);
//여기까지 INSERT SQL을 데이터베이스에 보내지 않는다.
//커밋하는 순간 데이터베이스에 INSERT SQL을 보냄
transaction.commit(); //트랜잭션 커밋
참고로 JPA는 내부에서 동적으로 객체를 생성해야 하므로, 생성자를 만들 때 기본 생성자를 따로 생성해둬야 한다.
# 한 번에 flush 할 객체의 개수를 지정(버퍼링 기능), 최적화 효과
<property name = "hibernate.jdbc.batch_size" value = "10" />
🔎 변경 감지(Dirty Checking)
영속 상태의 엔티티의 경우, setter 등으로 변경만 해도 JPA는 더티 체킹을 통해 이전에 저장된 스냅샷과 비교한다. 이렇게 비교한 후 Update SQL을 생성하여
쓰기 지연 저장소에 저장하고, 커밋을 만나면 flush()하여 한 번에 처리한다.
🔎 지연 로딩(Lazy Loading)
지연 로딩은 연관 관계가 있는 엔티티를 가져올 때, 해당 엔티티를 단독으로 획득할 수 있는 기술이다.
해당 엔티티와 맺어진 연관 관계마다 쿼리를 수행하지 않으므로 성능상 이점이 커 실무에서 반드시 사용되는 로딩 기법이다.
📌 플러시(Flush)
플러시는 영속성 컨텍스트의 변경 내용(쓰기 지연 SQL 저장소 내용)을 DB에 반영(동기화)하는 것을 말한다.
🔎 동작
- 변경 감지(Dirty Checking)
- 수정된 엔티티를 쓰기 지연 SQL 저장소에 등록
- 쓰기 지연 SQL 저장소의 쿼리를 DB에 전송
🔎 플러시 하는 방법
- em.flush() 또는 em.clear()로 직접 호출
- 트랜잭션 커밋 - 자동
- JPQL 쿼리 실행 - 자동
JPQL 쿼리는 데이터베이스로부터 최신화된 데이터를 얻어와야 한다. 만약, 영속화 후에 주문한 작업들이 쓰기 지연 SQL 저장소에 있다면 문제가 발생할 것이다.
따라서, JPQL을 실행하면 플러시부터 수행되어 데이터베이스를 최신화하는 것이다.
플러시 모드 기본 값은 자동(FlushModeType.AUTO)으로 되어 있으며, 변경이 필요하다면 다음과 같다.
em.setFlushMode(FlushModeType.COMMIT);
Reference:
https://www.inflearn.com/course/ORM-JPA-Basic/dashboard