여우발개발

3장. 영속성 관리 본문

기술 도서 리뷰/자바 ORM 표준 JPA 프로그래밍

3장. 영속성 관리

♬여우 2025. 2. 17. 13:45

3장의 주된 내용은 영속성 컨텍스트(consistence context)에 대한 이해이다.

entity가 영속 상태이어야 JPA가 제공하는 기능들을 사용할 수 있다.


  • JPA가 제공하는 기능
    • 엔티티를 매핑하는 설계 부분
    • 매핑한 엔티티를 실제 사용하는 부분

3장에서는 매핑한 엔티티를 엔티티 매니저를 통해 어떻게 사용하는지를 탐구한다.

 

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

// 비용이 아주 많이 든다.
EntityManagerFactory emf = Persistence.createEntityManagerFactory("{{persistence-unit name}}");
  • EntityManagerFactory
    • JPA를 동작시키기 위한 기반 객체 생성
    • factory라는 이름 답게 비용이 아주 많이 든다
      • 비용이 크므로 데이터베이스를 하나만 사용하는 경우 하나만 생성한다
      • 여러 스레드가 동시에 접근해도 안전하므로 스레드 간 공유 가능
    • hibernate 등 JPA 구현체들은 생성 시 DB connection pool도 만든다
      • spring 등 J2EE 환경에서는 해당 컨테이너가 제공하는 데이터소스 사용
// 비용이 적게 든다.
EntityManager em = emf.createEntityMnager();
  • EntityManager
    • 엔티티 매니저 팩토리에서 생성할 수 있다.
    • 여러 스레드에서 동시 접근 시 동시성 문제가 발생하므로 스레드 간 공유 불가
    • DB 연결이 꼭 필요한 시점까지 커넥션을 얻지 않는다. (성능적 이점)

 

3.2 영속성 컨텍스트란?

Persistence Context : 엔티티를 영구 저장하는 환경

→ 하지만… 좀 애매하다. 실제로 DB에 저장되는 것은 아니기 때문이다.

→ 그냥 1차 캐시라고 생각하는게?

em.persist(entity);
  • Persistence Context
    • EntityManager로 Entity를 저장 or 조회 시 persistence context에 보관/관리된다.
    • EntityManager를 통해서 접근, 관리할 수 있다.

 

3.3 엔티티의 생명주기

Entity entity = new Entity();
entity.setParam("param");

 

  • 비영속 (new/transient - 일시적인, 휘발성)
    • persistence context와 전혀 관계가 없는 상태
    • persist가 되기 전 새로 만든 entity 객체

 

em.persist(entity);
  • 영속 (managed)
    • persistence context에 저장되고, 관리되는 상태
    • DB에 저장된 상태와는 다르다. 영속 상태여도 DB 내에 없을 수 있다.
    • 조회해온 entity도 영속 상태이다.

 

em.detach(entity);

em.clear();
em.close();
  • 준영속 (detached)
    • persistence context가 관리하던 영속 entity가 관리하지 않게 된 상태
    • tx가 끝났다거나 할 때 발생. 개발자가 직접 발생시키기는 힘들다.
    • 이 상태에서 persistence context의 관리가 안된다는 사실을 인지하는 것이 중요.

 

em.remove(entity);
  • 삭제 (removed)
    • entity를 persistence context와 DB에서 삭제한다.

 

 

3.4 영속성 컨텍스트의 특징

  • 식별자 값
    • persistence context는 entity를 식별자(@Id) 값으로 구분함
    • 따라서 영속 상태는 식별자 값이 반드시 있어야 한다.
  • 데이터베이스 저장
    • 보통 tx 커밋 시 persistence context에 새로 저장된 entity를 DB에 반영한다. → flush
  • persistence context가 entity를 관리 시 얻는 장점
    • 1차 캐시
    • 동일성 보장
    • tx를 지원하는 쓰기 지연
    • 변경 감지
    • 지연 로딩

 

3.4.1 엔티티 조회

  • 1차 캐시
    • 모든 영속 상태의 entity는 persistence context 내부의 cache에 저장됨
    • 1차 캐시의 key는 entity의 식별자(@Id) 값
    • → 영속 상태는 식별자 값이 반드시 있어야 한다
  • 1차 캐시에서 조회
    • entity 조회 시 1차 캐시에 있다면 해당 entity 반환
  • DB에서 조회
    • entity 조회 시 1차 캐시에 없다면 DB에서 조회해 entity를 생성
    • 생성한 entity를 1차 캐시에 저장
    • 1차 캐시에 저장된 영속 상태의 entity 반환

→ 영속상태의 entity를 조회 시 메모리의 1차 캐시에서 바로 반환받을 수 있기 때문에 성능상 이점

→ 메모리의 인스턴스를 반환받기 때문에 영속 entity의 동일성이 보장됨

3.4.2 엔티티 등록

EntityMnager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();

// entityManager는 데이터 변경 시 트랜잭션이 필요함
tx.begin();
// persistence context에 등록 (DB에는 저장 안됨)
em.persist(entity);
// commit 시 DB에 INSERT SQL을 보냄
tx.commit();

쓰기 지연 (transactional write-behind)

  1. EntityManager는 transaction 커밋 전까지 쓰기 지연 SQL 저장소에 모아둠
    • Q. INSERT인지 UPDATE인지 어떻게 알지? → @Id(Primary Key)값을 보고 판단함
  2. transaction 커밋 시 모아둔 쿼리를 DB에 보냄
  • 트랜잭션을 지원하는 쓰기 지연이 가능한 이유
    1. 필요한 SQL을 발생 시 마다 전송하든
    2. SQL을 모아서 전송하든
    • 트랜잭션 안에서 커밋 전에만 수행되면 결과는 같다
    • 모아서 전송하면 db 접근이 줄어드므로 성능 최적화가 가능하다
  • (참고) spring-data-jpa의 .save() 사용 시
    • persist + flush가 같이 일어난다.
    • tx를 명시적으로 끝내지 않아도 flush된다.

3.4.3 엔티티 수정

프로젝트가 커지고 요구사항이 늘어나게 되면, 쿼리도 늘어나고 직간접적으로 비즈니스 로직이 SQL에 의존하게 된다.

JPA를 사용하면 entity를 그냥 수정해주면 알아서 처리해준다.

EntityMnager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();

// entityManager는 데이터 변경 시 트랜잭션이 필요함
tx.begin();
// 영속 entity 조회
Entity entity = em.find(Entity.class, "entity")

entity.setParam("param"); 

// commit 시 update 된 entity를 체크하고 DB에 UPDATE SQL을 보냄
tx.commit();

변경 감지 (dirty checking)

  • entity의 변경사항을 DB에 자동으로 반영함
  • 영속 상태의 entity에만 적용된다
  • 변경 감지 작동 순서
    1. entity를 영속화할 때 최초 상태를 snapshot 떠서 보관해둠
    2. flush 시점에 snapshot과 entity를 비교해 변경된 entity를 찾는다
    3. 변경된 entity가 있으면 UPDATE SQL을 생성해서 쓰기 지연 SQL 저장소에 보낸다
    4. transaction 커밋 시 모아둔 쿼리를 DB에 보냄
  • 변경 감지로 인해 실행된 UPDATE SQL
    • 기본적으로 entity의 모든 필드를 업데이트
    • 장점 (단점보다 더 많아서 기본으로 채택)
      • 모든 필드를 사용하면 수정 쿼리가 항상 같다
        • application 로딩 시점에 수정 쿼리를 미리 생성해두고 재사용할 수 있음
      • DB에 동일한 쿼리를 보내면 이전에 한번 파싱된 쿼리를 재사용함
    • 단점
      • DB에 보내는 데이터 전송량이 증가
      • @DynamicUpdate
        • 필드가 너무 많거나 데이터가 너무 클 때 수정된 데이터에 대해서만 동적으로 UPDATE SQL을 생성할 수 있음
        • 컬럼이 대략 30개 이상이면 더 빠를 수 있다. 직접 테스트해봐야 확실함
          • 한 테이블에 컬럼이 30개 이상이라는 것은 테이블 설계 상 책임 분리가 안되었을 가능성이 높다

3.4.4 엔티티 삭제

EntityMnager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();

// entityManager는 데이터 변경 시 트랜잭션이 필요함
tx.begin();
// 영속 entity 조회
Entity entity = em.find(Entity.class, "entity")

entity.remove();

// commit 시 delete 된 entity를 체크하고 DB에 DELETE SQL을 보냄
tx.commit();
  1. 등록과 비슷하게 flush 전까지 쓰기 지연 SQL 저장소에 모아둠
  2. flush 시 모아둔 쿼리를 DB에 보냄
  • remove가 호출되는 순간 해당 entity는 consistence context에서 제거된다.
    • 재사용하지 않도록 주의!

 

3.5 플러시

flush : consistence context의 변경 내용을 DB에 반영

  • flush를 실행하면 일어나는 일
    1. 변경 감지 동작
      1. 영속성 컨텍스트에 있는 모든 entity의 snapshot을 비교
      2. 감지된 수정된 엔티티는 수정 쿼리 만들어 쓰기 지연 SQL 저장소에 등록
    2. DB에 쓰기 지연 SQL 저장소의 쿼리 전송 (등록, 수정, 삭제 쿼리)
  • flush를 실행하는 방법
    1. 직접 호출
      • EntityManager.flush() 를 직접 호출해서 강제 flush
      • 테스트 시에나 사용되고 거의 사용되지 않음
    2. transaction commit 시 자동 호출
      • 변경 내용을 SQL로 전달하지 않고 트랜잭션만 커밋하면 DB에 반영되지 않기 때문에 자동 호출
    3. 객체지향 쿼리(ex. JPQL) 실행 시 자동 호출
      • JPQL은 SQL로 변환되어 DB에서 entity를 조회한다
      • JPQL의 SQL이 실행되기 전에 변경 사항이 DB에 반영되어야 하기 떄문에 자동 호출
  • find() 메소드는 flush가 호출되지 않는다
    • 이미 변경된 사항이 적용된 1차 캐시를 먼저 보기 때문

3.5.1 플러시 모드 옵션

플러시 모드를 직접 지정할 수 있다. (대부분 AUTO 그대로 사용)

  • FlushModeType.AUTO : (default) commit, 쿼리 실행 시 flush
  • FlushModeType.COMMIT : commit 시에만 flush → 성능 최적화 시 사용

 

3.6 준영속

영속상태의 entity가 영속성 컨텍스트에서 분리된 상태

  • 준영속 상태는 영속성 컨텍스트가 제공하는 기능을 사용할 수 없다
  • 비영속하고 다를 게 없는데 왜 따로 취급하는걸까?

3.6.1 엔티티를 준영속 상태로 전환 : detach()

em.detach(entity);
  • 특정 entity를 준영속 상태로 전환
  • 메소드 호출 시 1차 캐시부터 쓰기 지연 SQL 저장소까지 해당 entity를 관리하기 위한 모든 정보가 제거됨

3.6.2 영속성 컨텍스트 초기화 : clear()

em.clear();
  • 해당 영속성 컨텍스트 초기화해 모든 엔티티를 준영속 상태로 전환
  • 영속성 컨텍스트가 새로 만들어진 것과 같다

3.6.3 영속성 컨텍스트 종료 : close()

em.close();
  • 해당 영속성 컨텍스트 종료해 모든 엔티티를 준영속 상태로 전환
  • 영속성 컨텍스트를 사용할 수 없다
  • entity는 보통 영속성 컨텍스트가 종료되면서 준영속 상태가 된다
    • (직접 준영속을 만드는 일은 드물다)

3.6.4 준영속 상태의 특징

  • 영속성 컨텍스트가 관리하지 않는다
    • 1차 캐시, 쓰기 지연, 변경 감지, 지연 로딩을 포함한 모든 기능이 동작하지 않는다
  • 한 번 영속 상태였으므로 식별자 값이 있다
  • 굳이 책에서 언급하는 이유는 필요한 데이터가 지연 로딩으로 얻어져야 하는데 트랜잭션이 종료되어 값을 얻지 못하는 경우를 인지시키기 위해서 인 것 같다.

3.6.5 병합 : merge()

  • 비영속 상태의 entity를 인자로 받아 영속 상태의 새로운 entity를 받는다
// 준영속 상태의 엔티티를 받아서 새로운 영속 상태의 엔티티를 반환
Entity mergeEntity = em.merge(entity);
  1. 파라미터로 넘어온 entity의 식별자로 1차 캐시에서 entity를 조회
    1. 1차 캐시에 entity가 없다면 DB에서 entity를 조회해 1차 캐시에 저장
  2. 조회한 영속 entity의 모든 데이터를 준영속 entity의 데이터로 변경한다
  3. 영속 entity를 반환한다

→ ❗파라미터로 넘어온 준영속 entity는 merge 이후에도 준영속이다

// 준영속 상태의 엔티티는 더 이상 사용할 필요가 없다
// 기존 준영속 엔티티를 참조하던 변수를 영속 엔티티로 교체해주는 것이 안전함
entity = em.merge(detachedEntity);
  • 비영속 병합
Entity entity = new Entity();
entity = em.merge(entity);

비영속 엔티티도 영속으로 만들 수 있다

  • (참고) spring-data-jpa의 .save()는 병합으로 동작한다.

'기술 도서 리뷰 > 자바 ORM 표준 JPA 프로그래밍' 카테고리의 다른 글

4장. 엔티티 매핑  (0) 2025.03.07
2장. JPA 시작  (2) 2025.02.07
1장. JPA 소개  (1) 2025.02.02