[Spring boot] JPA - 영속성 컨텍스트(엔티티 매니저 팩토리와 엔티티 매니저, 커넥션풀, 영속성 컨텍스트, 준영속)

영속성 컨텍스트 구조 && 장점

영속성 컨텍스트 구조 && 장점 미리 보기

 

  • JPA가 제공하는 기능은 크게 엔티티와 테이블을 매핑하는 설계 부분과 매핑한 엔티티를 실제 사용하는 부분으로 나눌 수 있다.
  • 엔티티를 엔티티 매니저를 통해 어떻게 사용하는지 한 번 알아보자

 

엔티티 매니저 → 엔티티를 저장, 수정, 삭제, 조회 등 엔티티와 관련된 모든 일을 처리

 

개발자 입장에서 엔티티 매니저는 엔티티를 저장하는 가상의 DB라고 생각해라

 

  • 밑 코드를 통해 다시 분석

https://github.com/whitecy01/codyssey/tree/main/springAndJPA/pureJPA/pureJPA

 

codyssey/springAndJPA/pureJPA/pureJPA at main · whitecy01/codyssey

나만의 백엔드 세계를 탐험하는 여정 기초부터 실전까지 한 걸음씩 쌓아가는 개발자의 기록. Contribute to whitecy01/codyssey development by creating an account on GitHub.

github.com

 

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

DB를 하나만 사용하는 애플리케이션 → 하나의 EntityManagerFactory를 하나만 생성

//엔티티 매니저 팩토리 생성
EntityManagerFactory emf = Persistence.createEntityManagerFactory("pure-jpa");
  • 한 개만 만들어서 애플리케이션에서 전체 공유하도록 설계됨

엔티티 매니저는 다음과 같다.

  //엔티티 매니저 - 생성
  EntityManager em = emf.createEntityManager();

→ 엔티티 매니저 팩토리는 여러 스레드가 동시에 접근해도 안전하여 서로 다른 스레드 간에 공유됨

→ 엔티티 매니저는 여러 스레드가 동시에 접근하면 동시성 문제가 발생하여 스레드 간에 절대 공유하면 안된다.

 

 

  • 엔티티 매니저는 DB 연결이 꼭 필요한 시점까지 커넥션을 얻지 않음 → 트랜잭션 시작할 때 커넥션 획득
  • 하이버네이트를 포함한 JPA 구현체들은 EntityManagerFactory를 생성할 때 커넥션 풀도 만드는데(persistence.xml에 보면 DB 접속 정보가 있음) 이건 J2SE 환경에서 사용하는 방법

 

커넥션풀에 대해 좀 더 알아보자

 

커넥션 풀

→ DB 연결(Conntection)을 미리 여러 개 만들어두고 재사용하는 창고

 

이게 왜 필요한가?

  • DB 커넥션 하나 만드는 과정은 비싸고 느림.

 

EntityManagerFactory → 설정 및 커넥션 풀 관리

  • JPA에서 커넥션풀은 JPA 구현체 + 풀 라이브러리 조합으로 관리
    • Hibernate + HikariCP (요즘 표준)
    • EclipseLink + 내부 풀
    • 옛날: C3P0, DBCP (요즘 잘 안 씀)

 

커넥션을 하나 만들 때 실제로 일어나는일

  1. TCP 연결
  2. 인증(ID/PW)
  3. 세션 생성
  4. DB 리소스 할당

→ 이걸 요청마다 매번하면 성능 폭망, DB 과부하, 서버는 놀고 DB는 죽음..

 

 

커넥션 풀의 동작 방식

1. 애플리케이션 시작

EntityManagerFactory 생성 → 커넥션 풀 생성 → DB 커넥션 여러 개 미리 확보

 

2. 트랜잭션 시작

em.getTransaction().begin();
  • 이 시점에 풀에서 커넥션 하나 빌림

3. SQL 실행

  • 빌린 커넥션으로 쿼리 수행

4. 트랜잭션 종료

em.getTransaction().commit();
  • 커넥션을 닫는 게 아니라 풀에 반납

 

엔티티 매니저 팩토리 && 엔티티 매니저 && 커넥션 풀을 Spring boot에서 보자

  • postgreSQL로 진행해봄

 

package com.persistenceContext.persistenceContextexam;

import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

import javax.sql.DataSource;

@Component
@RequiredArgsConstructor
public class JpaInspector implements CommandLineRunner {

    private final EntityManagerFactory emf;
    private final EntityManager em;
    private final DataSource dataSource;

    @Override
    public void run(String... args) {
        System.out.println("===== JPA Infra =====");
        System.out.println("EMF = " + emf.getClass());
        System.out.println("EM  = " + em.getClass());
        System.out.println("DS  = " + dataSource.getClass());
    }
}

 

서버 실행하면 콘솔에서 꼭 봐야 할 것

  • EntityManagerFactory → 스프링이 만든 구현체
  • EntityManager → 프록시 (SharedEntityManager)
  • DataSource → HikariDataSource

→ “Spring에서 주입되는 EM은 진짜가 아니다”가 눈으로 보임

import jakarta.persistence.*;

@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;
    private String name;

    protected Member() {}
    public Member(String name) { this.name = name; }
}
import jakarta.persistence.EntityManager;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
public class TestService {
    private final EntityManager em;

    @Transactional
    public void tx() {
        em.persist(new Member("재윤"));
        System.out.println("persist 완료");
    }
}
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
public class TestController {
    private final TestService testService;

    @GetMapping("/test")
    public String test() {
        testService.tx();
        return "ok";
    }
}

 

실행 로그 - /test 전

1. 커넥션 풀(Hikari) 생성 + 커넥션 미리 확보

com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...

HikariPool-1 - Pool stats (total=1/10, idle=1/10, active=0, waiting=0)
HikariPool-1 - Attempting to create/setup new connection (bbbddec1-de48-47ca-bb6f-e9867a0571cf)
HikariPool-1 - Established new connection (bbbddec1-de48-47ca-bb6f-e9867a0571cf)
HikariPool-1 - Added connection org.postgresql.jdbc.PgConnection@197d521a
HikariPool-1 - After adding stats (total=2/10, idle=2/10, active=0, waiting=0)
HikariPool-1 - Attempting to create/setup new connection (a723ee15-3011-4678-ad23-1a630289d4e5)
HikariPool-1 - Established new connection (a723ee15-3011-4678-ad23-1a630289d4e5)
HikariPool-1 - Added connection org.postgresql.jdbc.PgConnection@72e9f3b3
HikariPool-1 - After adding stats (total=3/10, idle=3/10, active=0, waiting=0)
HikariPool-1 - Attempting to create/setup new connection (eacaa76c-0527-4f11-9a51-7776116b2448)
HikariPool-1 - Established new connection (eacaa76c-0527-4f11-9a51-7776116b2448)
HikariPool-1 - Added connection org.postgresql.jdbc.PgConnection@f216598e
HikariPool-1 - After adding stats (total=4/10, idle=4/10, active=0, waiting=0)

HikariPool-1 - Attempting to create/setup new connection (b7e2f4df-33b4-496c-bd19-b4caa4f25c1e)
HikariPool-1 - Established new connection (b7e2f4df-33b4-496c-bd19-b4caa4f25c1e)
HikariPool-1 - Added connection org.postgresql.jdbc.PgConnection@d7abe26a
HikariPool-1 - After adding stats (total=5/10, idle=5/10, active=0, waiting=0)

HikariPool-1 - Attempting to create/setup new connection (479d0621-1cca-4179-948a-44300548ad82)
HikariPool-1 - Established new connection (479d0621-1cca-4179-948a-44300548ad82)
HikariPool-1 - Added connection org.postgresql.jdbc.PgConnection@a34fb132
HikariPool-1 - After adding stats (total=6/10, idle=6/10, active=0, waiting=0)

HikariPool-1 - Attempting to create/setup new connection (aa2c5a7b-4349-47ba-82db-098b9de454e3)
HikariPool-1 - Established new connection (aa2c5a7b-4349-47ba-82db-098b9de454e3)
HikariPool-1 - Added connection org.postgresql.jdbc.PgConnection@412daca2
HikariPool-1 - After adding stats (total=7/10, idle=7/10, active=0, waiting=0)

HikariPool-1 - Attempting to create/setup new connection (1497aea9-6884-4b83-aa50-fef3949f6847)
HikariPool-1 - Established new connection (1497aea9-6884-4b83-aa50-fef3949f6847)
HikariPool-1 - Added connection org.postgresql.jdbc.PgConnection@87bf4c81
HikariPool-1 - After adding stats (total=8/10, idle=8/10, active=0, waiting=0)

HikariPool-1 - Attempting to create/setup new connection (21d8fed5-8b72-47d1-9ee4-5bd678d8cb41)
HikariPool-1 - Established new connection (21d8fed5-8b72-47d1-9ee4-5bd678d8cb41)
HikariPool-1 - Added connection org.postgresql.jdbc.PgConnection@1a6d9311

HHH000489: No JTA platform available

HikariPool-1 - After adding stats (total=9/10, idle=8/10, active=1, waiting=0)

HikariPool-1 - Attempting to create/setup new connection (831d227c-0297-4fcb-a538-4158ff0ab53d)
HikariPool-1 - Established new connection (831d227c-0297-4fcb-a538-4158ff0ab53d)
HikariPool-1 - Added connection org.postgresql.jdbc.PgConnection@4fd97cee
  • properties에서 커넥션 풀을 만드는 개수를 지정하지 않으면 자동으로 maximumPoolSize=10, minimumIdle=10가 지정되어 만들어져서 위처럼 나옴

 

2. JpaInspector

EMF = class jdk.proxy2.$Proxy118
EM  = class jdk.proxy2.$Proxy119
DS  = class com.zaxxer.hikari.HikariDataSource
  • DS가 HikariDataSource → 커넥션 풀
  • EMF/EM이 jdk.proxy2.$Proxy..로 나온 이유는
    • 스프링이 트랜잭션/영속성 컨텍스트를 붙이기 위해 프록시로 감싸서 주입했기 때문
    • 즉 내가 주입 받은 EM은 진짜 “EntityManager 인스턴스”가 아니라 요청/트랜잭션 상황에 맞춰 진짜 EM을 찾아주는 대리인(프록시)

→ 스프링에서 주입되는 EntityManager는 프록시다.

 

실행 로그 - /test 후

1. OpenEntityManagerInViewIntercetpr(OSIV) 때문에 EM을 먼저 열어둠

Opening JPA EntityManager in OpenEntityManagerInViewInterceptor

의미

  • 웹요청 시작~응답 끝까지 EntityManager를 열어둠
  • 뷰 렌더링/컨트롤러 이후에도 지연로딩 가능
  • 대신 “트랜잭션 바깥에도 쿼리가 나갈 수 있음”

 

2. 트랜잭션 매니저가 “스레드에 바잉딘 된 EM”을 발견

Found thread-bound EntityManager [SessionImpl(...<open>)] for JPA transaction
Creating new transaction with name [TestService.tx]
  • OSIV가 이미 EM을 열어두고 스레드에 묶어둔 상태라서 트랜잭션 매니저가 “어?, 이 스레드에 열린 EM이 있네” 하고 재사용

그 뒤 로그가

Not closing pre-bound JPA EntityManager after transaction
  • 트랜잭션 끝났다고 EM 닫지 않는다는 뜻 → 요청이 끝날 때 (OSIV가) 닫아야 하니

 

3. 실제로 트랜잭션이 시작됨(@Transactional)

Getting transaction for [TestService.tx]
Exposing JPA transaction as JDBC [HibernateConnectionHandle...]
  • 여기서 드디어 JPA 트랜잭션이 JDBC 커넥션과 연결됨
  • “커넥션을 얻는 시점”은 정확히 이 타이밍 근처

→ 여기서 HNikari가 10개 만들어놓았으니 풀에서 1개 대여

 

4. persist 시점에 시퀀스 값 먼저 뽑음

select nextval('member_seq')
persist 완료
insert into member ...
  • @GeneratedValue + sequence 전략 때문에
    • insert 전에 PK를 만들려고 nextval 먼저 호출
  • 그 다음 insert 실행

5. 커밋 + 트랜잭션 종료

Initiating transaction commit
Committing JPA transaction ...
Completing transaction ...
  • commit 시점에 flush가 일어나면서 insert SQL 확정
  • 커넥션은 “닫는 게 아니라 풀에 반납”

6. 요청이 끝나면 OSIV가 EM 닫음

Closing JPA EntityManager in OpenEntityManagerInViewInterceptor
  • 트랜잭션은 이미 끝났지만 EM 자체는 요청 끝나면서 닫힘(= 영속성 컨텍스트 종료)

 

결과

  • 실제 해보았을 때 커넥션 풀을 애플리케이션 시작 때 만들어지고, 커넥션을 미리 확보한다라는 것을 알 수 있었음
  • 스프링이 주입하는 EntityManager/EntityManagerFactory는 프록시임.
  • OSIV가 켜져 있어서 요청 전체 동안 EM이 열려있고 트랜잭션은 그 안에서 잠깐 시작/종료됨.

https://github.com/whitecy01/codyssey/tree/main/springAndJPA/persistenceContextexam/src/main/java/com/persistenceContext/persistenceContextexam

 

codyssey/springAndJPA/persistenceContextexam/src/main/java/com/persistenceContext/persistenceContextexam at main · whitecy01/co

나만의 백엔드 세계를 탐험하는 여정 기초부터 실전까지 한 걸음씩 쌓아가는 개발자의 기록. Contribute to whitecy01/codyssey development by creating an account on GitHub.

github.com

 

영속성 컨텍스트(persistence context)

  • 엔티티를 영구 저장하는 환경
  • 엔티티 매니저 → 엔티티를 저장하거나 조회하면 엔티티 매니저는 영속성 컨텍스트에 엔티티를 보관하고 관리함.
em.persist(member);

이 메소드는 엔티티 매니저를 사용해서 회원 엔티티를 영속성 컨텍스트에 저장함.

 

→ 영속성 컨텍스트는 논리적인 개념에 가깝고 눈에 보이지도 않음. 영속성 컨텍스트는 엔티티 매니저를 생성할 때 하나 만들어짐 And 엔티티 매니저를 통해 여기에 접근 및 관리할 수 있음

 

엔티티의 생명주기

엔티티는 4가지 상태가 존재

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

비영속

  • 엔티티 객체를 생성함 but 순수한 객체 상태임 아직 저장하지 않음
  • 영속성 컨텍스트와 DB와는 전혀 관련이 없음
  Member member = new Member();
  member.setId(id);
  member.setUsername("재윤");

 

 

영속

  • 엔티티 매니저를 통해서 엔티티를 영속성 컨텍스트에 저장 == 영속 상태 == 영속성 컨텍스트에 의해 관리된다는 것.
  • em.find나 JPQL을 사용해서 조회한 엔티티도 영속성 컨텍스트가 관리하는 영속 상태
em.persist(member);

 

삭제

  • 엔티티를 영속성 컨텍스트와 DB에서 삭제
em.remove(member);

 

 

영속성 컨텍스트의 특징

  1. 영속성 컨텍스트와 식별자 값 → 영속성 컨텍스트는 엔티티를 식별자 값으로 구분함. 식별자 값 없으면 예외 발생
  2. 영속성 컨텍스트와 DB 저장 → JPA는 보통 트랜잭션을 커밋하는 순간 영속성 컨텍스트에 새로 저장된 엔티티를 DB에 반영함 == flush(플러시)

 

영속성 컨텍스트가 엔티티를 관리하는 것에서 장점을 이제 한 본 보려고 함

 

엔티티 조회 - 1차 캐시

  • 영속성 컨텍스트는 내부에 캐시를 가지고 있음 == 1차 캐시 → 영속 상태의 엔티티 이곳에 저장됨
  • 이 내부에 Map이 있음 키는 @Id 값이고 value는 엔티티 인스턴스

  • em.find(엔티티 클래스의 타입, 식별자 값)를 호출하였다고 했을 때 1차 캐시에서 엔티티를 찾고 1차 캐시에 없으면 DB에서 조회

  • em.persist를 하면 1차 캐시에 저장됨 == 메모리에 있는 1차 캐시

→ 성능상 이점

 

추가적으로

  • 영속성 컨텍스트는 엔티티의 동일성을 보장 → 객체 조회에서 같은 객체를 반환

 

엔티티 등록 - 트랜잭션을 지원하는 쓰기 지연

//엔티티 매니저 팩토리 생성
EntityManagerFactory emf = Persistence.createEntityManagerFactory("pure-jpa");

//엔티티 매니저 - 생성
EntityManager em = emf.createEntityManager();

//트랜잭션 - 획득
EntityTransaction tx = em.getTransaction();
tx.begin(); // 트랜잭션 시작
em.persist(memberA);
em.persist(memberB);
tx.commit();// 트랜잭션 - 커밋 -> 커밋하는 순간 SQL을 DB에 보냄
  • 엔티티 매니저 → 트랜잭션을 커밋하기 직전까지 DB에 엔티티를 저장하지 않고 내부 쿼리 저장소에 SQL을 모아놓음
  • 트랜잭션을 커밋할 때 모아둔 쿼리를 DB에 보냄 → 트랜잭션을 지원하는 쓰기 지연(transactional write-behind)

 

  • 트랜잭션 커밋 → 엔티티 매니저는 영속성 컨텍스트를 flush(영속성 컨텍스트의 변경 내용을 DB에 동기화하는 작업 이때 등록, 수정, 삭제한 엔티티를 DB에 반영) → 동기화한 후 실제 DB 트랜잭션을 커밋
    • 동기화 == 영속성 컨텍스트에 쌓아둔 변경 사항을 SQL로 만들어 DB에 반영
    • DB 트랜잭션을 커밋 == JDBC Connection 단위, BEGIN/COMMIT/ROLLBACK, SQL 결과를 실제로 저장할지 말지 결정 즉, commit 전까지는 DB가 확정하지 않음

 

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

  • 데이터를 저장하는 즉시 등록 쿼리 DB에 보낸 후 마지막에 트랜잭션을 커밋
  • 데이터를 저장하면 등록 쿼리를 메모리아 모아놓고 트랜잭션 커밋할 때 쿼리를 DB에 보낸 후 커밋

이 2개가 어차피 결과가 같음 → 트랜잭션을 커밋하면 함꼐 저장되고 롤백하면 함께 저장되지 않음 즉, 커밋 직전에만 DB에 SQL을 전달하면 됨

 

엔티티 수정 - 변경 감지(dirty checking)

  • 요구사항 변경으로 인해 막 수정한다고 하자 그러면 JPA 사용 안 하면 비즈니스 로직이 SQL에 의존함. → 자동으로 변경되는 걸 좀 해주면 좋겠음 ㅠㅠ라는 거임
//엔티티 매니저 팩토리 생성
EntityManagerFactory emf = Persistence.createEntityManagerFactory("pure-jpa");

//엔티티 매니저 - 생성
EntityManager em = emf.createEntityManager();

//트랜잭션 - 획득
EntityTransaction tx = em.getTransaction();

tx.begin();
Member findMember = em.find(Member.class, "memberA");
//영속 엔티티 데이터 수정
member.setUsername("재윤");
member.setAge(20);
tx.commit();

 

  • JPA로 엔티티 수정하게 된다면 → 엔티티 조회해서 데이터만 변경하면 됨
  • 엔티티의 변경사항을 DB에 자동으로 반영하는 기능 변경 감지(dirty checking)라고 함

 

밑 그림을 보자

 

  • JPA는 엔티티를 영속성 컨텍스트에 보관할 때 최초의 상태를 복사해서 저장해둠 == 스냅샷
  • 플러시 시점에 스냅샷과 엔티티를 비교해서 변경된 엔티티 찾음 → 찾으면 수정 쿼리를 쓰기 지연 SQL 저장소에 보내놓음

 

→ 변경 감지는 영속성 컨텍스트가 관리하는 영속 상태의 엔티티에만 적용됨, 비영속, 준영속은 값을 변경해도 DB에 반영되지 않음

 

 

JPA 기본 전략

  • 재미있는 거는 수정된 것만 SQL을 반영하도록 하는 게 아니라 엔티티의 모든 필드 업데이트data 전송량이 증가하는 단점 but 장점을 본다면
    • 수정을 했을 때 Hibernate가 어떤 SQL을 쓸지 매번 판단을 안 해도 됨 → 애플리케이션 시작 시점에 미리 수정 쿼리를 만들어 놓아서 캐싱 해놓고 이거 바인딩해서 재사용하면 됨
    • DB는 SQL을 바로 실행하지 않음 순서가 SQL 문자열 파싱 → 문법/권한 검사 → 실행 계획 생성 → 실행인데 1~3번이 좀 비쌈 SQL이 매번 다르면 DB는 재사용 안함 SQL이 항상 같으면 DB가 봤던 쿼리라 파싱/계획 재사용을 함
  • 뭐 필드가 많거나 저장되는 내용이 크면 수정된 데이터만 사용해서 동적으로 UPDAT SQL 생성 전략 가능 → 하이버네이트 확장 기능 → DynamicUpdate 어노테이션 사용
    • 이건 뭐 컬럼이 30개 이상이면 이게 더 빠르다고함

 

엔티티 삭제

em.remove(memberA)
  • 엔티티 등록과 비슷하게 삭제 쿼리를 쓰기 지연 SQL에 등록 후에 → 트랜잭션을 커밋해서 플러시를 호출하면 실제 DB에 삭제 쿼리 전달
    • em.remove를 호출하는 순간 영속성 컨텍스트에서 제거됨

 

지연로딩

  • 영속성 컨텍스트에서 위에서 말한 것처럼 식별자를 가지고 있고, 상태 관리(변경 감지), 트랜잭션 범위 내에서 객체 동일성 보장가 있었다.

 

우선 지연로딩이 뭔지부터 보자

@ManyToOne(fetch = FetchType.LAZY)
private Team team;
  • 이 상태에서 Member를 조회하면 Team 엔티티를 즉시 조회하지 않음 즉, DB에는 TeamSQL이 안나감

 

영속성 컨텍스트가 여기서 핵심인 걸 본다면

  1. 프록시가 아직 DB를 안 갔다라는 걸 기억함
  • 이 프록시는 실제 Team 데이터는 없고 식별자만 알고 있음, 영속성 컨텍스트 참조를 들고 있음

 

밑 코드가 실행된다고 하자

member.getTeam().getName();
  • 이 순간 내부에서는
프록시 -> 영속성 컨텍스트에 질문 "Team(id=1) 이미 관리 중임?:"

영속성 컨텍스트에서 Team이 있으면 객체 반환하고 없으면 DB에 SQL 날려서 Team 조회함 그리고 영속성 컨텍스트에 등록하고 → 프록시 → 실제 엔티티로 변환

 

즉, 이점은 DB 접근을 끝까지 미룰 수 있다는 거, 이미 있으면 DB를 안 가도 됨

 

준영속

위에서는 비영속 → 영속 → 삭제를 봄 이제는 영속 → 준영속을 한 번 봐보자

  • 준영속 상태의 엔티티는 영속성 컨텍스트가 제공하는 기능 사용 불가능.
    • 변경 감지 → 값 변경해도 DB에 반영안됨

 

만드는 방법 → em.detach, em.clear(), em.close()

  • em.deatch == 1차 캐시부터 쓰기 지연 SQL 저장소까지 해당 엔티티를 관리하기 위한 모든 정보가 제거됨
  • em.clear == 영속성 컨텍스트의 모든 엔티티를 준영속 상태로 만듦
  • em.close() == 영속성 컨텍스트를 종료 엔티티 전부 준영속 상태가 됨.

 

준영속 상태의 특징

  • 거의 비영속에 가까움
  • 식별자 값을 가지고 있음
    • 비영속 상태는 식별자 값이 없을 수 있지만 이건 이미 한 번 영속 상태여서 반드시 식별자 값을 가지고 있음
  • 지연로딩 불가능
    • 실제 객체 대신 프록시 개체를 로딩해두고 해당 객체를 실제 사용할 때 영속성 컨텍스트를 통해 불러오는데 이 상태는 영속성 컨텍스트가 관리하지 않아서 지연 로딩 시 문제 발생

 

병합 : merge()

  • 준영속 상태에서 이름을 바꿔보는 걸 해보자.
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.EntityTransaction;
import jakarta.persistence.Persistence;

public class App
{
    static EntityManagerFactory emf = Persistence.createEntityManagerFactory("pure-jpa");
    public static void main( String[] args )
    {
        Member member = createMember("memberA", "회원1");

        member.setUsername("회원명변경"); // 준영속 상태에서 변경

        mergeMember(member);

    }

    static Member createMember(String id, String username){
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();

        tx.begin();
        Member member = new Member();
        member.setId(id);
        member.setUsername("재윤");
        member.setAge(20);

        em.persist(member);
        tx.commit();

        em.close();

        return member;
    }

    static void mergeMember(Member member){
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();

        tx.begin();
        Member mergeMember = em.merge(member);
        tx.commit();
        //준영속 상태
        System.out.println("member = " + member.getUsername());

        //영속 상태
        System.out.println("mergeMember = " + mergeMember.getUsername());

        System.out.println("em contains member = " + em.contains(member));
        System.out.println("em contains mergeMember = " + em.contains(mergeMember));
        em.close();

    }
}

 

결과

member = 회원명변경
mergeMember = 회원명변경
em contains member = false
em contains mergeMember = true

 

순서는 다음과 같다.

  • membvver.setUsername(”회원명변경”)을 호출해서 회원 이름을 변경했지만 준영속 상태인 member 엔티티를 관리하는 영속성 컨텍스트가 존재하지 않아서 수정 사항을 DB에 반영 불가
  • 준영속 상태의 엔티티 수정하려면 → 준영속 상태를 다시 영속 상태로 변경(merge) → 트랜잭션을 커밋할 때 수정했던 회원명이 DB에 반영됨(member 엔티티가 영속 상태가 된 것은 아님 mergeMember라는 새로운 영속 상태 엔티티 반환)

 

즉, 중요한 것은

  1. 준영속 엔티티(member)를 그대로 다시 붙이지 않음
  2. 영속성 컨텍스트 안에 새로운 영속 엔티티를 만든다
  3. 준영속 엔티티의 값을 복사한다.
  4. 그 새 영속 엔티티를 반환

member는 계속 준영속 상태임. 단지 member

member가 객체로는 있지만 DB에 반영이 안 된 것

 

https://github.com/whitecy01/codyssey/tree/main/springAndJPA/persistenceContextexam/src/main/java/com/persistenceContext/persistenceContextexam

 

codyssey/springAndJPA/persistenceContextexam/src/main/java/com/persistenceContext/persistenceContextexam at main · whitecy01/co

나만의 백엔드 세계를 탐험하는 여정 기초부터 실전까지 한 걸음씩 쌓아가는 개발자의 기록. Contribute to whitecy01/codyssey development by creating an account on GitHub.

github.com