[Spring/Springboot] 영속성(Persistence) - 1차 캐시/변경 감지(Dirty Checking)/지연 로딩(Lazy Loading)
2024. 12. 15. 23:07ㆍCS/Spring
public static void main(String[] args){
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf. createEntityManager();
EntityTransactiono tx = em.getTransaction();
tx.begin();
try{
//비영속
Member member = new Member();
member.setId(100L);
member.setName("HelloJPA");
//영속
em.persist(member);
//영속 상태가 되었다고 DB에 바로 쿼리가 날라가는 것이 아님
//transaction을 커밋하는 시점에 영속성 컨텍스트에 있는게 DB에 쿼리가 날라가게 됨
//준영속
em.detach(member);//회원 엔티티를 영속성 컨텍스트에서 분리
//삭제
em.remove(member); //객체를 삭제한 상태
tx.commit();
}catch(Exception e){
tx.rollback();
}
application 이랑 DB 사이에 중간계층이 하나 있는 것 ⇒ 영속성 컨텍스트의 이점
1. 1차 캐시
- 1차 캐시 : DB 한 Transaction 안에서만 영향있음
Member member = new Member();
member.setId("member1");
member.setUsername("회원1");
//1차 캐시에 저장됨
em.persist(member);
//1차 캐시에서 조회 가능
Member findMember = em.find(Member.class, "member1");
//**DB까지 가지 않고, 1차 캐시 안에 있는 값을 그냥 조회 가능**
Member findMember = em.find(Memeber.class, "member2");
**//DB에 없는 member2경우 DB에서 조회하고, 영속성 컨텍스트안 1차 캐시에 저장하고, 반환함
//이후에 조회하면 1차 캐시에서 조회**
2. 동일성(identity)보장
Member a = em.find(Member.class, "member1");
Member b = em.find(Member.class, "member1");
System.out.println(a==b);//동일성 비교 true
3. 트랜젝션을 지원하는 쓰기 지연(transaction write-behind)
EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
//엔티티 매니저는 데이터 변경시 트랜젝션을 시작해야함
transaction.begin(); // 트랜젝션 시작
em.persist(memberA);
em.persist(memberB);
//여기까지 INSERT SQL을 DB에 보내지 않음
//1차 캐시에 저장됨과 동시에 JPA가 entity를 분석해서 insert SQL을 생성하고
//쓰기지연 SQL 저장소에 쌓아둠
transaction.commit(); //트랙잭션 커밋
//COMMIT 하는 순간 DB예 INSERT SQL을 보냄
//쓰기 지연 SQL 저장소에 있던 애들이 DB로 날라가면서 커밋됨
- “hibernate.jdbc.batch_size" value=에 설정한 size만큼 한방에 모아서 쿼리를 날림
4. 변경 감지(Dirty Checking)
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(); // 트랜잭션 커밋
- JPA는 commit을 하면 flush()를 함
- flush() → 엔티티와 스냅샷 비교 “어! memberA 바뀌었네!” → UPDATE SQL 쿼리를 (쓰기 지연 저장소에) 생성 → flush(DB에 반영) → DB에 commit
- 스냅샷 : 내가 값을 딱 읽어온(영속성 컨텍스트에 들어온) 최초 시점의 상태를 딱 떠두는 것
- 엔티티 삭제
Member memberA = em.find(Member.class, "memberA);
em.remove(memberA);//엔티티 삭제
- flush
- 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영
- JPA는 값을 바꾸면 transaction이 commit 되는 시점에 데이터베이스에 반영한다!
- 플러시 발생하면 무슨일이 발생하는가(DB에 transaction이 commit 되면, flush가 자동으로 발생)
- 변경 감지
- 수정된 엔티티 쓰기 지연 SQL 저장소에 등록
- 쓰기 지연 SQL 저장소의 쿼리를 데이터베이스에 전송(등록, 수정, 삭제 쿼리)
- 영속성 컨텍스트를 플러시 하는 방법
- em.flush() - 직접 호출
- “쿼리를 DB에 미리 반영하고 싶어~ 쿼리를 미리 좀 보고 싶어~”
- 1차 캐시 역시 원래대로 생성됨 다만, 쓰기 지연 SQL 저장소의 쿼리가 DB에 반영되는 것임
- transaction commit - 플러시 자동 호출
- JPQL query 실행 - 플러시 자동 호출
- JPQL 쿼리 실행 시 플러시가 자동으로 호출되는 이유
- em.flush() - 직접 호출
- 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영
em.persist(memberA);
em.persist(memberB);
em.persist(memberC);
//중간에 JPQL 실행
query = em.createQuery("select m from Member m", Member.class);
List<Member> members = query.getResultList();
//JPQL은 무조건 쿼리가 날라가기 때문에 위처럼 조회해도 조회가 됨
Flush
영속성 컨텍스트를 비우지 않음
영속성 컨텍스트의 변경내용을 DB에 동기화
트랜젝션이라는 작업 단위가 중요 ⇒ 커밋 직전에만 동기화 하면 됨
5. 지연 로딩(Lazy Loading)
- 프록시(proxy)
- em.find() : DB를 통해서 실제 엔티티 객체 조회
- em.getReference(): DB 조회를 미루는 가짜(프록시) 엔티티 객체 조회
- getReference() 호출하는 시점에는 DB 쿼리를 안 하는데도 객체가 조회됨
- DB의 쿼리를 하는 시점은 getReference로 찾은 값이 실제 사용되는 시점
- 프록시 특징
- 실제 클래스를 상속 받아서 만들어짐
- 실제 클래스와 겉 모양이 같음
- 사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 됨
- 프록시 객체는 실제 객체의 참조(target)을 보관
- 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드 호출
- 프록시 객체의 초기화
👉 프록시 객체 특징
1. 프록시 객체는 처음 사용할 때 한번만 초기화
2. 프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아님
- 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근이 가능한 것일 뿐
3. 프록시 객체는 원본 엔티티를 상속 받음 ⇒ 따라서, 타입 체크시 주의 ( == 비교 실패, 대신 instance of 사용)
4. 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티 반환
5. 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제 발생
- 지연로딩
- 지연로딩 LAZY를 사용해서 프록시로 조회
- Member를 조회할 땐 딱 Member만 가져오고, Team은 proxy로 가져옴
- 지연로딩 LAZY를 사용해서 프록시로 조회
@Entity
public class Member extends BaseEntity{
@Id
@GeneratedValue
@Column(name = "member_id")
private Long id;
@Column(name = "username")
private String name;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "team_id")
private Team team;
.
.
.
}
- 실제 Team의 어떤 속성을 사용하는 시점에 프록시 객체가 초기화되면서 DB에서 해당 값을 가져옴
- 즉시로딩
- Member와 Team을 자주 함께 사용한다면? 즉시 로딩 EAGER을 사용해서 함께 조회
@Entity
public class Member extends BaseEntity{
@Id
@GeneratedValue
@Column(name = "member_id")
private Long id;
@Column(name="username")
private String name;
@ManyToOOne(fetch = FetchType.EAGER)
@JoinColumn(name = "team_id")
private Team team;
.
.
.
}
- 쿼리가 나갈 대 애초부터 Member와 Team을 조인해서 한방에 다 가져옴
- proxy가 아닌 진짜가 다 나오기 때문에 이미 초기화가 끝난 상태이며 초기화가 필요 없음
👉 프록시와 즉시로딩 주의
1. 가급적 지연 로딩만 사용(특히 실무에서)
2. 즉시 로딩을 적용하면 예상치 못한 SQL이 발생
- 즉시 로딩은 JPQL에서 N+1 문제를 일으킴
3. @ManyToOne, @OneToOne은 기본이 즉시 로딩이기 때문에 따로, LAZY로 설정해 줄 것
4. @OneToMany, @ManyToMany는 기본이 지연로딩
'CS > Spring' 카테고리의 다른 글
[Spring/Springboot] Fetch Join (0) | 2024.12.15 |
---|---|
[Spring/Springboot] JPA 다양한 쿼리 방법 - JPQL/QueryDSL (0) | 2024.12.15 |
[Spring/Springboot] 도메인/양방향매핑/N+1 문제 (0) | 2024.12.15 |
[Spring/Springboot] 서블릿(Servlet) (0) | 2024.12.15 |
[Spring/Springboot] AOP 관점 지향 프로그래밍 (0) | 2024.12.15 |