RE-Heat 개발자 일지

[JPA] [3] 영속성 관리 - 내부 동작 방식 본문

백엔드/JPA

[JPA] [3] 영속성 관리 - 내부 동작 방식

RE-Heat 2023. 8. 4. 23:58

https://www.inflearn.com/course/ORM-JPA-Basic

 

자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런 | 강의

초급자를 위해 준비한 [웹 개발, 백엔드] 강의입니다. JPA를 처음 접하거나, 실무에서 JPA를 사용하지만 기본 이론이 부족하신 분들이 JPA의 기본 이론을 탄탄하게 학습해서 초보자도 실무에서 자

www.inflearn.com

인프런 김영한 님의 강의를 듣고 작성한 글입니다.

 

 

[1] 영속성 컨텍스트 1

 

■ JPA에서 가장 중요한 2가지
1. 객체와 관계형 데이터베이스 매핑 (Object Relational Mapping)
    매핑 관점. 정적인 내용
2. 영속성 컨텍스트
    JPA가 내부적으로 어떻게 동작하는지 확인

 

■ 영속성 컨텍스트

엔티티 매니저 팩토리와 엔티티 매니저

처음 생성된 엔티티 매니저 팩토리는 클라이언트의 요청에 따라 엔티티 매니저를 생성한다. 그리고 엔티티 매니저는 DB Connection Pool에 있는 Connection을 이용해서 DB에 접근한다.

 

■ 영속성 컨텍스트

  • JPA를 이해하는데 가장 중요한 용어
  • "엔티티를 영구 저장하는 환경"이라는 뜻
  • EntityManger.persist(enitity);
    • 엄밀히 말하면 엔티티를 영속성 컨텍스에 저장하는 코드다.
    • 이전 챕터에선 DB에 저장하는 코드로 이해했다.

 

■ 엔티티 매니저? 영속성 컨텍스트?

  • 영속성 컨텍스트는 논리적인 개념이다.
  • 눈에 보이지 않는다.
  • 엔티티 매니저를 통해서 영속성 컨텍스트에 접근한다.

 

  • 스프링 프레임워크에서 EntityManager 주입받아서 사용하면, 같은 트랜잭션 범위에 있는 EntityManager는 동일 영속성 컨텍스트에 접근한다.
  • 따라서 동일한 @Transactional (같은 트랜잭션 범위 전파되는 경우에도) 이면 같은 영속성 컨텍스트에 접근한다.

 

■ 엔티티의 생명주기

 

  • 비영속(new/transient)
    영속성 컨텍스트와 전혀 관계가 없는 새로운 상태
  • 영속(managed)
    영속성 컨텍스트에 의해 관리되는 상태
  • 준영속(detached)
    영속성 컨텍스트에 저장되었다가 분리된 상태
  • 삭제(removed)
    삭제된 상태

 

① 비영속(new/transient)

//객체를 생성한 상태 (비영속)
Member member = new Member();
member.setId("member1");
member.setUsername("회원1");
  • 단순히 자바 객체를 생성·초기화만 해둔 상태다.
  • JPA와 전혀 관계없는 상태다.

 

② 영속 (managed)

//객체를 생성한 상태 (비영속)
Member member = new Member();
member.setId("member1");
member.setUsername("회원1");

//JPA 영속성 컨텍스트로의 접근은 엔티티 매니저를 통해서 한다
EntityManger em = emf.createEntityManger();
//JPA의 모든 데이터 변경은 트랜잭션 안에서 일어난다
em.getTransaction.begin();

//객체를 저장한 상태 (영속)
em.persist(member);
  • 엔티티 매니저를 통해 영속성 컨텍스트에 접근한다.
  • 엔티티 매니저를 통해 member 엔티티 객체를 영속성 컨텍스트에 저장.
  • 영속성 컨텍스트 안에서 member 엔티티 객체가 관리된다.

 

영속 상태라고 DB에 쿼리가 날아가지 않는다. 쿼리가 날아가는 시점은 커밋 이후다.

영속 상태(before, after)가 아닌 commit()을 해야 insert쿼리가 날아가는 것을 확인할 수 있다.

 

③ 준영속(detached)·삭제(removed)

//회원 엔티티를 영속성 컨텍스트에서 분리(준영속 상태)
em.detach(member);

//객체를 삭제한 상태
em.remove(member);

 

[2] 영속성 컨텍스트 2

■ 영속성 컨텍스트의 이점

  • 1차 캐시
  • 동일성(identity) 보장
  • 트랜잭션을 지원하는 쓰기 지연 (transactional write-behind)
  • 변경 감지(Dirty Checking)
  • 지연 로딩(Lazy Loading)

① 1차 캐시

1차 캐시에서 조회

영속성 컨텍스트 내부엔 1차 캐시가 있다. 

JPA는 엔티티를 조회해 올 때 1차 캐시부터 뒤지며, 1차 캐시가 해당 키값을 가진 엔티티를 가지고 있으면 그곳에서 조회한다.

 

데이터베이스 조회

member2를 조회하면 1차 캐시에 member2가 없기 때문에 DB에서 조회를 실행해 1차 캐시에 저장하고 member2를 반환한다. 이후 member2를 재조회하면 1차 캐시에서 member2를 반환한다.

 

1차 캐시로 DB 접근 횟수를 줄일 수 있다. 다만 성능상으로 큰 이점은 아니다.

엔티티 매니저는 트랜잭션이 끝나면 1차 캐시도 비워버린다. 따라서 찰나의 순간에만 이득이 있기 때문에 성능향상에 큰 도움을 주진 않는다.

 

같은 id값을 조회하니 select문이 단 한 번만 날아간다.

 

② 영속 엔티티의 동일성 보장

Member a = em.find(Member.class, "member1");
Member b = em.find(Member.class, "member1");

System.out.println(a == b); //동일성 비교 true

1차 캐시를 통해 영속 엔티티의 동일성을 보장한다.

구체적으론 1차 캐시로 반복 가능한 읽기(REPEATABLE READ) 등급의 트랜잭션 격리 수준을 데이터베이스가 아닌 애플리케이션 차원에서 제공한다.

 

참고] 트랜잭션 격리 수준 (직렬화가 격리 수준 가장 높고 이후 내림차순)

  • SERIALIZABLE (직렬화 가능) => 특정 트랜잭션이 사용 중인 모든 행을 다른 트랜잭션이 사용할 수 없도록 LOCK
  • REPEATABLE READ(반복가능한 읽기) => 자신의 트랜잭션이 생성되기 전 트랜잭션에서 COMMIT된 데이터만 읽음
  • READ COMMITTED(커밋된 읽기) => COMMIT이 된 데이터만 읽는다.
  • READ UNCOMMITED(커밋되지 않은 읽기) => COMMIT이나 ROLLBACK 여부 관계없이 다른 트랜잭션에서 읽기 O

https://velog.io/@yujiniii/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98-%EA%B2%A9%EB%A6%AC-%EC%88%98%EC%A4%80

 

[데이터베이스] 트랜잭션 격리 수준(Isolation level)

CS 스터디 | 데이터베이스 | Isolation level

velog.io

 

③ 엔티티 트랜잭션을 지원하는 쓰기 지연

EntityManager em = emf.createEntityManager(); 
EntityTransaction transaction = em.getTransaction(); 
//엔티티 매니저는 데이터 변경시 트랜잭션을 시작해야 한다. 
transaction.begin();  // [트랜잭션] 시작

em.persist(memberA); 
em.persist(memberB);
//여기까지 INSERT SQL을 데이터베이스에 보내지 않는다. 

//커밋하는 순간 데이터베이스에 INSERT SQL을 보낸다.
transaction.commit(); // [트랜잭션] 커밋

 

COMMIT 이전엔 쓰기 지연 SQL 저장소에 INSERT 쿼리문을 쌓아 둔다.

이후 COMMIT하는 시점에 쌓아 둔 INSERT 쿼리문을 날린다.

 

이런 방식으로 버퍼링(임시 저장소에 저장)을 가능하게 한다.

 

하이버네이트는 쓰기 지연 SQL 저장소에 몇 개를 담아둘지 정할 수 있는 배치 사이즈가 따로 있으며 그 적용도 상당히 간편한 편이다.

hibernate.jdbc.batch_size로 배치 사이즈 설정 가능. 이 기능을 알맞게 사용하면 성능향상도 가능하다.

 

④ 변경 감지

영속성 컨텍스트를 통해 변경을 감지할 수 있다.

1] 엔티티 수정

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(); // [트랜잭션] 커밋

update() 같은 코드를 따로 쓰지 않아도 값이 변경되면 update 쿼리가 자동으로 날아간다. 그렇다면 그 원리는 무엇일까?

 

2] 변경감지 원리

JPA는 커밋 시점에 내부적으로 flush()를 호출한다. 영속성 컨텍스트로 flush가 호출되면 JPA는 1차 캐시에 저장된 엔티티와 스냅샷(값을 읽어온 최초 시점)을 비교한다. 만일 스냅샷과 다른 부분이 있으면 JPA는 UPDATE 쿼리를 쓰기 지연 SQL 저장소에 저장한다. 그 뒤 flush()할 때 DB에 쿼리를 날리고 커밋을 마무리한다.

 

3] 엔티티 삭제

//삭제 대상 엔티티 조회
Member memberA = em.find(Member.class, “memberA"); 
em.remove(memberA); //엔티티 삭제

 

[3] 플러시

영속성 컨텍스트의 변경내용을 데이터베이스에 반영하는 것을 일컫는다.

데이터베이스 트랜잭션이 커밋되면 자동으로 flush가 발생한다고 보면 된다.

 

① 플러시 발생

  • 변경감지
  • 수정된 엔티티 쓰기 지연 SQL 저장소에 등록
  • 쓰기 지연 SQL 저장소의 쿼리를 데이터베이스에 전송 (등록, 수정, 삭제 쿼리)

② 영속성 컨텍스트를 플러시 하는 방법

  • em.flush() : 직접 호출
  • 트랜잭션 커밋 : 플러시 자동 호출
  • JPQL 쿼리 실행 : 플러시 자동 호출

flush()를 직접 호출하면 커밋 시점 이전에 insert문이 날아가는 걸 확인할 수 있다.

 

③ 플러시 모드 옵션

em.setFlushMode(FlushModeType.COMMIT)

FlushModeType.AUTO : 커밋이나 쿼리를 실행할 때 플러시 함 (기본값)
FlushModeType.COMMIT : 커밋할 때만 플러시

=> 중간에 다른 테이블을 불러올 때 2번째 설정을 할 수 있으나 현업에선 대부분 AUTO 세팅으로 개발을 진행한다.

 

[4] 준영속 상태

영속 상태에 놓인 엔티티를 영속성 컨텍스트에서 분리하는 것. 이 상태면 영속성 컨텍스트가 제공하는 기능을 사용하지 못한다.

 

준영속 상태로 만드는 방법

  • em.detach(entity)
    특정 엔티티만 준영속 상태로 전환
  • em.clear()
    영속성 컨텍스트를 완전히 초기화
  • em.close()
    영속성 컨텍스트를 종료

예시]

① em.detach

SELECT 쿼리만 나가고 UPDATE 쿼리는 나가지 않는다. 왜냐하면 em.detach로 영속성 컨텍스트에서 빠졌기 때문이다.

 

② em.close()

find()로 select문 불러온 것도 영속성 컨텍스트에서 제외됐으므로 다시 한번 SELECT문을 날리는 걸 확인할 수 있다.