nyximos.log

[JPA] 영속성 관리 본문

Programming/JPA

[JPA] 영속성 관리

nyximos 2022. 2. 21. 16:50

 

자바 ORM 표준 JPA 프로그래밍

김영한

 

 

👩‍🌾 persistence.xml 설정

JPA는 persistence.xml을 사용해서 필요한 설정 정보를 관리한다.

META-INF/persistence.xml 클래스 패스 정보에 있으면 별도의 설정 없이 JPA가 인식할 수 있다.

 

META-INF/persistence.xml

일반적으로 연결할 데이터베이스당 하나의 영속성 유닛 persistence-unit 을 지정한다.

javax.persistence로 시작하는 속성은 JPA 표준 속성으로 특정 구현체에 종속되지 않는다.

  • 버전 지정 : <persistence version="2.2"

필수 속성

  • 영속성 유닛 이름 지정 : <persistence-unit name="hello">
  • JDBC 드라이버 : <property name="javax.persistence.jdbc.driver" value="org.h2.Driver"/>
  • 데이터베이스 접속 아이디 : <property name="javax.persistence.jdbc.user" value="sa"/>
  • 데이터베이스 접속 비밀번호 : <property name="javax.persistence.jdbc.password" value=""/>
  • 데이터베이스 접속 URL : <property name="javax.persistence.jdbc.url" value="jdbc:h2:tcp://localhost/~/hello"/>
  • 데이터베이스 방언 : <property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/>

옵션 속성

  • 하이버네이트가 실행한 SQL 출력 : <property name="hibernate.show_sql" value="true"/>
  • 하이버네이트가 실행한 SQL을 출력할 때 보기 쉽게 정렬 : <property name="hibernate.format_sql" value="true"/>
  • 쿼리 출력시 주석도 함께 출력<property name="hibernate.use_sql_comments" value="true"/>
  • <property name="hibernate.hbm2ddl.auto" value="create" />
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.2"
             xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd">
<persistence-unit name="hello">
    <properties>
        <!-- 필수 속성 -->
        <property name="javax.persistence.jdbc.driver" value="org.h2.Driver"/>
        <property name="javax.persistence.jdbc.user" value="sa"/>
        <property name="javax.persistence.jdbc.password" value=""/>
        <property name="javax.persistence.jdbc.url" value="jdbc:h2:tcp://localhost/~/hello"/>
        <property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/>

        <!-- 옵션 -->
        <property name="hibernate.show_sql" value="true"/>
        <property name="hibernate.format_sql" value="true"/>
        <property name="hibernate.use_sql_comments" value="true"/>
        <property name="hibernate.hbm2ddl.auto" value="create" />
    </properties>
</persistence-unit>
</persistence>

 

 

👩‍🏭 엔티티 매니저 팩토리, 엔티티 매니저, 트랜잭션 관리

엔티티 매니저 팩토리 생성

엔티티 매니저 팩토리 = 엔티티 매니저 만드는 공장

JPA를 시작하려면 EntityManagerFactory를 생성해야 한다.

생성 비용 高 → DB 하나를 사용하는 애플리케이션은 일반적으로 EntityManagerFactory를 하나만 생성

여러 스레드가 동시에 접근해도 안전 → 서로 다른 스레드 간에 공유 가능

EntityManagerFactory emf = Persistence.createEntityManagerFactory("영속성 유닛 이름");

META-INF/persistence.xml에 있는 정보를 바탕으로 EntityManagerFactory 생성

  • 지정한 이름의 persistence-unit을 찾는다.
  • 설정 정보를 읽어서 JPA를 동작시키기 위한 기반 객체를 만든다.
  • JPA 구현체에 따라 데이터베이스 커넥션 풀도 생성한다.

→ 엔티티 매니저 팩토리를 생성하는 비용이 크다.

→ 애플리케이션 전체에서 딱 한 번만 생성하고 공유해서 사용하자.

 

 

엔티티 매니저 생성

엔티티 매니저 팩토리에서 엔티티 매니저 생성

대부분의 JPA 기능 제공 (등록/수정/삭제/조회)

데이터베이스 커넥션과 밀접한 관계 → 스레드 간 공유 & 재사용 🙅‍♀️

여러 스레드가 동시에 접근 → 동시성 문제 발생

EntityManager em = emf.createEntityManager();

 

종료

사용이 끝난 엔티티 매니저는 종료해준다.

em.close();

애플리케이션을 종료할 때 엔티티 메니저 팩토리도 종료해준다.

emf.close();

 

트랜잭션 관리

JPA 사용시 항상 트랜잭션 안에서 데이터를 변경해준다.

트랜잭션 없이 데이터 변경시 예외 발생

트랜잭션 시작시 엔티티 메니저에서 트랜잭션 API를 받아온다.

 

비즈니스 로직 정상 작동 → commit

예외 발생 → rollback

EntityTransaction tx = em.getTransaction();
try {
	
    tx.begin();  // 트랜잭션 시작
    logic(em);   // 비즈니스 로직 실행
    tx.commit(); // 트랜잭션 커밋
    
} catch (Exception e) {
	tx.rollback(); // 예외 발생 시 트랜잭션 롤백
}

 

데이터베이스 커넥션

엔티티 매니저는 데이터베이스 연결이 꼭 필요한 시점까지 커넥션을 얻지 않는다.

보통 트랜잭션을 시작할 때 커넥션을 획득한다.

 

Java SE 환경

  • 하이버네이트를 포함한 JPA 구현체들은 EntityManagerFactory를 생성할 때 커넥션풀도 만든다.

Java EE 환경

  • (스프링 프레임 워크 포함) → 해당 컨테이너가 제공하는 데이터소스 사용

 

🤔 영속성 컨텍스트란?

영속성 컨텍스트 persistence context

논리적인 개념으로 엔티티를 영구 저장하는 환경

 

엔티티 매니저를 생성할 때 영속성 컨텍스트가 하나 만들어진다.

엔티티 매니저를 통해 영속성 컨텍스트에 접근할 수 있고, 영속성 컨텍스트를 관리한다. 

persist() 메소드는 엔티티 매니저를 사용해서 엔티티를 영속성 컨텍스트에 저장한다.

 

 

👩‍⚕️ 엔티티의 생명주기

  • 비영속, 영속, 준영속, 삭제

비영속 new/transient

영속성 컨텍스트와 전혀 관계가 없는 상태

엔티티 객체 생성후 저장하지 않은 순수한 객체 상태일 때

영속성 컨텍스트나 데이터베이스와 전혀 관련이 없다.

em.persist() 호출 전 비영속 상태

영속 managed

엔티티 매니저를 통해 영속성 컨텍스트에 저장된 상태

영속성 컨텍스트가 관리하는 엔티티영속 상태이다.

em.find()나 JPQL을 사용해 조회 가능한 엔티티는 영속 상태이다.

em.persist() 호출 후 영속 상태

 

준영속 detached

영속성 컨텍스트에 저장되었다가 분리된 상태

영속성 컨텍스트가 관리하지 않는다.

em.detach(), em.close(), em.clear() 호출 후 준영속 상태

삭제 removed

엔티티가 영속성 컨텍스트와 데이터베이스에서 삭제된 상태

em.remove() 호출 후 삭제한 상태

 

 

🧛‍♀️ 영속성 컨텍스트의 특징

영속성 컨텍스트와 식별자 값

식별자 값 = @Id로 테이블의 기본키와 매핑한 값

영속성 컨텍스트엔티티를 식별자 값으로 구분한다.

영속상태는 반드시 식별자 값이 있어야한다.

👻 식별자 값이 없으면 예외 발생!

 

영속성 컨텍스트와 데이터베이스 저장

플러시 flush

영속성 컨텍스트의 변경 내용을 데이터베이스에 동기화

쓰기 지연 SQL 저장소에 모인 쿼리를 데이터베이스에 보낸다.

JPA는 보통 트랜잭션을 커밋하는 순간, 영속성 컨텍스트에 새로 저장된 엔티티를 데이터베이스에 반영한다.

 

영속성 컨텍스트가 엔티티를 관리했을 때 장점

  • 1차 캐시
  • 동일성 보장
  • 트랜잭션을 지원하는 쓰기 지연
  • 변경 감지
  • 지연 로딩

 

엔티티를 CRUD 하면서 영속성 컨텍스트가 왜 필요한지 알아보자.

 

엔티티 조회

🤔 1차 캐시란?

영속성 컨택스트 내부의 캐시

영속 상태의 엔티티는 모두 이곳에 저장된다.

영속성 컨텍스트 내부에 Map이 하나 있다. (key: @Id로 매핑한 식별자 값, value: 인스턴스)

영속성 컨텍스트에 데이터를 저장하고 조회하는 모든 기준은 데이터베이스 기본 키 값이다.

em.find() 호출 → 1차 캐시에서 엔티티 조회 → 없으면 데이터베이스에서 조회

 

👻 성능성 보장

 

1차 캐시에서 조회

em.find() 호출시 1차 캐시에서 식별자 값으로 엔티티를 찾는다.

찾는 엔티티가 있으면 데이터베이스를 조회하지 않고 메모리에 있는 1차 캐시에서 엔티티를 조회한다.

 

데이터베이스에서 조회

em.find() 호출시 엔티티가 1차 캐시에 없으면,

엔티티 매니저는 데이터베이스 조회 → 엔티티를 생성 → 1차 캐시에 저장 → 영속 상태의 엔티티 반환

 

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

식별자가 같은 엔티티 인스턴스 조회후 비교

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

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

 

em.find(Member.class, "member1");를 반복해서 호출해도

영속성 컨텍스트는 1차 캐시에 있는 같은 인스턴스를 반환한다.

→ 둘은 같은 인스턴스, 결과는 true

 

👻 영속성 컨텍스트는 성능성 이점엔티티의 동일성 identity을 보장한다.

 

엔티티 등록

엔티티 매니저는 트랜잭션 커밋 전까지, 내부 쿼리 저장소에 INSERT SQL을 모아둔다. 🙆‍♀️

데이터베이스에 엔티티 저장 🙅‍♀️

 

트랜잭션을 지원하는 쓰기 지연  transactional write-behind

트랜잭션을 커밋할 때, 모아둔 쿼리를 데이터베이스에 보낸다.

 

1. persist(memberA) : memberA를 영속화

    → 영속성 컨텍스트는 1차 캐시에 회원 엔티티 저장 & 회원 엔티티 정보로 등록 쿼리 생성

    → 만들어진 등록 쿼리를 쓰기 지연 SQL 저장소에 보관 (현재 등록쿼리 1건)

 

2. persist(memberB) : memberB를 영속화

    → 영속성 컨텍스트는 1차 캐시에 회원 엔티티 저장 & 회원 엔티티 정보로 등록 쿼리 생성

    → 만들어진 등록 쿼리를 쓰기 지연 SQL 저장소에 보관 (현재 등록쿼리 2건)

 

3. commit() : 트랜잭션 커밋

    → 엔티티 맨니저는 영속성 컨텍스트를 flush

    → 영속성 컨텍스트의 변경내용을 데이터베이스에 동기화 후 실제 데이터베이스 트랜잭션을 commit

 

트랜잭션을 지원하는 쓰기 지연이 가능한 이유

트랜잭션 커밋 →  저장 🙆‍♀️

트랜잭션 롤백 → 저장 🙅‍♀️

 

등록 쿼리를 그때그때 데이터베이스에 전달해도 커밋하지 않으면 소용이 없다.

커밋 직전에만 데이터베이스에 SQL을 전달하면 된다.

 

👻 모아둔 등록 쿼리를 데이터베이스에 한번에 전달 → 성능 최적화 👍

 

엔티티 수정

변경 감지  dirty checking

엔티티의 변경사항을 데이터베이스에 자동으로 반영하는 기능

영속성 컨텍스트가 관리하는 영속 상태의 엔티티에만 적용된다.

 

스냅샷 : JPA는 엔티티를 영속성 컨텍스트에 보관할 때, 최초 상태를 복사해서 저장해둔다.

flush 시점에 스냅샷과 엔티티를 비교 → 변경된 엔티티를 찾는다.

 

  1. 트랜잭션 커밋 → 엔티티 매니저 내부에서 flush() 호출
  2. 엔티티와 스냅샷 비교 → 변경된 엔티티 찾는다.
  3. 변경된 엔티티 🙆‍♀️ → 수정 쿼리 생성 → 쓰기 지연 SQL 저장소에 보낸다.
  4. 쓰기 지연 저장소의 SQL을 데이터베이스에 보낸다.
  5. 데이터베이스 트랜잭션 커밋

 

 

JPA의 기본 전략 : 엔티티의 모든 필드를 업데이트

 

장점

  • 모든 필드를 사용하면 수정 쿼리가 항상 같다. (바인딩되는 업데이트는 다르다.)
  • 애플리케이션 로딩 시점에 수정쿼리를 미리 생성해두고 재사용 가능

단점

  • 데이터베이스에 보내는 데이터 전송량 증가

 

👻 컬럼이 대략 30개 되면 정적 수정 쿼리(기본 방법)보다 동적 수정 쿼리(@DynamicUpdate 사용)가 더 빠르다.

      @DynamicUpdate 사용시 수정된 데이터만 사용해서 동적으로 UPDATE SQL을 생성한다. → 변경 감지

      기본 전략을 사용하고, 최적화 할 정도로 느리면 그때 전략을 수정한다.

      한 테이블에 컬럼이 30개 이상 된다는 것은 테이블 설계상 책임이 적절히 분리되지 않았을 가능성이 높다.

 

 

엔티티 삭제

삭제 대상 엔티티를 조회 → em.remove()에 삭제 대상 엔티티 전달 → 엔티티 삭제

em.remove(memberA) 호출 → memberA가 영속성 컨텍스트에 제거

Member memberA = em.find(Member.class, "MemberA");
em.remove(memberA);

 

엔티티는 즉시 삭제되는 것이 아님

  1. 삭제 쿼리를 쓰기 지연 SQL 저장소에 등록
  2. 트랜잭션 commit
  3. flush 호출
  4. 데이터베이스에 삭제쿼리 전달

 

👻 삭제된 엔티티는 재사용하지 말고 자연스럽게 가비지 컬렉션의 대상이 되도록 두자.

 

 

🧜‍♂️ 플러시 flush

영속성 컨텍스트의 변경 내용을 데이터 베이스에 반영 🙆‍♀️

영속성 컨텍스트에 보관된 엔티티 삭제 🙅‍♀️ (헷갈리지 말자!)

 

flush 실행

1. 변경 감지 동작 → 영속성 컨텍스트에 있는 모든 엔티티를 스냅샷과 비교 → 수정된 엔티티 찾음

2. 수정된 엔티티는 수정 쿼리를 만들어 쓰기 지연 SQL 저장소에 등록

3. 쓰기 지연 SQL 저장소의 쿼리를 데이터베이스에 전송

 

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

1. 직접 호출

     엔티티 매니저의 flush() 메소드 직접 호출

     테스트, 다른 프레임워크와 JPA를 함께 사용할 때를 제외하고 거의 사용 🙅‍♀️

 

2. 트랜잭션 커밋 시, 플러시 자동 호출

    데이터베이스에 변경내용을 SQL로 전달하지 않고 트랜잭션만 커밋하면 어떤 데이터도 데이터베이스에 반영되지 않는다.

    이런 문제를 예방하기 위해 트랜잭션 커밋 시, JPA는 flush()를 자동으로 호출해준다.

 

3. JPQL 쿼리 실행 시, 플러시 자동 호출

    JPQL은 SQL로 변환되어 데이터베이스에서 엔티티를 조회한다.

    따라서 쿼리를 실행하기 직전에 영속성 컨텍스트를 flush해서 변경내용을 데이터베이스에 반영해야 한다.

    JPA는 JPQL 실행할 때도 flush() 를 자동으로 호출해준다.

   👻 식별자를 기준으로 조회하는 find() 메소드를 호출할 때는 flush()가 실행되지 않는다.

 

플러시 모드 옵션

FlushModeType.AUTO : 커밋이나 쿼리를 실행할 때 flush (기본) 

FlushModeType.COMMIT : 커밋할 때만 flush

👻 성능 최적화가 필요할 때 COMMIT 모드를 사용한다.

 

 

🧝‍♀️ 준영속 detached

영속성 컨텍스트가 관리하는 영속 상태의 엔티티가 영속성 컨텍스트에서 분리된 상태

영속성 컨텍스트가 관리하지 않는 상태

준영속 상태의 엔티티는 영속성 컨텍스트가 제공하는 기능을 사용할 수 없다.

(1차 캐시, 동일성 보장, 트랜잭션을 지원하는 쓰기 지연, 변경 감지, 지연 로딩)

 

영속 상태 → 준영속 상태로 만드는 방법

1. em.detach(entity) : 특정 엔티티만 준영속 상태로 전환

    메소드 호출시 1차 캐시 ~ 쓰기 지연 SQL 저장소까지 해당 엔티티를 관리하기 위한 보든 정보 제거

 

detach 실행 전

 

detach 실행 후

 

2. em.clear() : 영속성 컨텍스트 완전히 초기화

    영속성 컨텍스를 초기화 해서 모든 엔티티를 준영속 상태를 만든다.

 

영속성 컨텍스트 초기화 전

 

영속성 컨텍스트 초기화 후

 

3. em.close() : 영속성 컨텍스트 종료

    영속성 컨텍스트를 종료하면 해당 영속성 컨텍스트가 관리하던 영속 상태의 엔티티가 모두 준영속 상태가 된다.

    개발자가 직접 준영속 상태로 만드는 일은 드물다.

 

영속성 컨텍스트 제거 전

 

영속성 컨텍스트 제거 후

 

정리

👻 엔티티 매니저는 엔티티 매니저 팩토리에서 생성
       자바를 직접 다루는 Java SE환경에서는 엔티티 매니저를 만들면 내부에 영속성 컨텍스트도 함께 만들어짐

 

👻 영속성 컨텍스트는 애플리케이션과 데이터베이스 사이에서 객체를 보관하는 가상의 데이터베이스 같은 역할

       영속성 컨텍스트는 엔티티 매니저를 통해서 접근 가능

       영속성 컨텍스트 덕분에 1차 캐시, 동일성 보장, 트랜잭션을 지원하는 쓰기 지연, 변경 감지, 지연 로딩 기능 사용 🙆‍♀️

 

👻 영속성 컨텍스트에 저장한 엔티티 → flush 시점에 데이터베이스에 반영

       일반적으로 트랜잭션 커밋시 영속성 컨텍스트가 flush

 

👻 영속 상태의 엔티티 = 영속성 컨텍스트가 관리하는 엔티티

       준영속 상태의 엔티티 = 영속성 컨텍스트가 해당 엔티티를 관리 🙅‍♀️

       영속성 컨텍스트가 제공하는 1차 캐시, 동일성 보장, 트랜잭션을 지원하는 쓰기 지연, 변경 감지, 지연 로딩 기능 사용 🙅‍♀️

 

 

 

참조

김영한,  자바 ORM 표준 JPA 프로그래밍, 에이콘출판주식회사, 2015

자바 ORM 표준 JPA 프로그래밍 - 기본편

 

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

JPA를 처음 접하거나, 실무에서 JPA를 사용하지만 기본 이론이 부족하신 분들이 JPA의 기본 이론을 탄탄하게 학습해서 초보자도 실무에서 자신있게 JPA를 사용할 수 있습니다., - 강의 소개 | 인프런

www.inflearn.com