3. 영속성 관리
2025. 6. 12. 13:27ㆍSpring/JPA
[목차]
1. JPA에서 중요한 개념
2. Entity Manager Factory와 Entity Manager
1) EntityManagerFactory (엔티티 매니저 팩토리)
2) EntityManager (엔티티 매니저)
3) 예시 흐름
3. 영속성 컨텍스트와 엔티티 매니저
1) 영속성 컨텍스트란?
2) EntityManager와의 관계
4. 엔티티 생명주기
5. 영속성 컨텍스트의 이점
5-1. 1차 캐시
1) 캐시란?
2) JPA의 1차캐시
3) 1차 캐시 사용 예시
4) 1차 캐시의 장점
5-2. 쓰기 지연
1) 쓰기 지연
2) 쓰기 지연 SQL 저장소
5-3. 변경 감지
1) 변경 감지
2) 변경 감지 동작 원리: 스냅샷 (Snapshot)
6. 플러시
6-1. 플러시란?
6-2. 플러시 발생 상황
1) em.flush() - 직접 호출
2) 트랜잭션 commit() - 플러시 자동 호출
3) JPQL 실행 - 플러시 자동 호출 (AUTO 모드 설정시)
7. 영속/준영속 상태로 만들기
1) 영속 상태로 만들기
2) 준영속 상태로 만들기
1. JPA에서 중요한 개념
1) 영속성 컨텍스트 (JPA 내부 동작 원리)
2) 객체 - 관계형 DB 매핑 (Object Relational Mapping)
2. Entity Manager Factory와 Entity Manager
EntityManagerFactory는 공장,
EntityManager는 작업자,
그리고 작업자가 관리하는 공간이 영속성 컨텍스트
1) EntityManagerFactory (엔티티 매니저 팩토리)
- 앱이 실행될 때 딱 한 번 생성해서 사용하는 공장(factory) 역할
- EntityManager를 생성한다.
- 무겁고 비용이 큼 (DB 연결, 메타정보 로딩 등)
- 따라서 애플리케이션 전체에서 하나만 만들어서 공유하는 게 원칙 (싱글톤처럼 사용)
2) EntityManager (엔티티 매니저)
- 실제로 DB 작업(등록, 수정, 조회, 삭제 등)을 직접 실행하는 객체
- 내부적으로 영속성 컨텍스트를 관리한다.
- 가벼운 객체이며 보통 요청 단위나 트랜잭션 단위로 생성해서 사용
- 스레드 간 공유 불가, 사용 후 반드시 닫아야 함
3) 예시 흐름
// 애플리케이션 실행 시 1회
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
// 요청마다 새로운 EntityManager 생성
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
Member member = new Member();
em.persist(member); // 저장
em.getTransaction().commit();
em.close(); // 꼭 닫아줘야 함
3. 영속성 컨텍스트와 엔티티 매니저
1) 영속성 컨텍스트란?
- "엔티티를 영구 저장하는 환경" 이라는 뜻으로 JPA를 이해하는데 가장 중요한 용어
- 영속성 컨텍스트라는 것은 논리적인 개념 (눈에 보이지 않음)
- 엔티티 매니저를 통해서 영속성 컨텍스트에 접근한다.
- EntityManager.persist(entity) 로 접근
- 이때 엔티티 매니저는 DB에 저장하는 것이 아니라 영속성 컨텍스트에 저장한다.
2) EntityManager와의 관계
- J2SE 환경: EntityManager : 영속성 컨텍스트 = 1:1
- J2EE/스프링 환경: EntityManager 여러 개 → 하나의 영속성 컨텍스트 공유 (N:1)
4. 엔티티 생명주기
상태 | 설명 |
비영속 (new/transient) | 새로 생성된 엔티티. 영속성 컨텍스트와 무관 |
영속 (managed) | 영속성 컨텍스트에 의해 관리되는 상태 변경 감지, 쓰기 지연, 1차 캐시, 지연 로딩 같은 혜택을 받음 |
준영속 (detached) | 영속성 컨텍스트에 저장되었다가 분리된 상태 DB와 연결이 끊김 |
삭제 (removed) | DB에서 삭제된 상태 |
// 1. 비영속: 객체를 생성한 상태
Member member = new Member();
member.setId("member1");
member.setUsername("회원1");
// 2. 영속: 객체를 저장한 상태
// 엔티티 매니저 안에 있는 영속석 컨텍스트에 멤버 객체가 들어가면서 영속 상태가 됨
// 지금 DB에 저장되는 것이 아님, commit 될 때 저장됨
em.persist(member);
// 3. 준영속
em.detach(member);
// 4. 삭제: DB에서 삭제
em.remove(member);
5. 영속성 컨텍스트의 이점
1. 1차 캐시
2. 트랜잭션을 지원하는 쓰기 지연
3. 변경 감지
4. 지연 로딩
5-1. 1차 캐시
1) 캐시(Cache)란?
- 캐시란 자주 쓰는 데이터를 빠르게 꺼내 쓰기 위해 임시로 저장해두는 공간
- DB나 파일처럼 느린 저장소에 계속 접근하면 성능이 떨어지기 때문에
속도가 빠른 메모리(RAM)에 복사본을 잠깐 저장해두는 것
2) JPA의 1차 캐시
- 물리적: JVM의 힙 메모리에 있는 Map<PK, Entity> 객체
- 논리적: 영속성 컨텍스트 내부 저장소
- EntityManager가 내부에 영속성 컨텍스트를 가지고 있고
- 영속성 컨텍스트는 Map<id, entity> 형태의 1차 캐시를 들고 있음
3) 1차 캐시 사용 예시
- 처음엔 DB에서 조회하고 → 1차 캐시에 저장
다음에 또 같은 ID로 조회하면 → DB 안 가고 1차 캐시에서 바로 가져옴! - 동일 ID 엔티티는 DB 조회 없이 메모리에서 재사용
Member m1 = em.find(Member.class, "member1"); //DB에서 조회
Member m2 = em.find(Member.class, "member1"); //1차 캐시에서 조회
4) 1차 캐시의 장점
- 성능 향상
- DB에 불필요한 쿼리를 줄일 수 있다
- 같은 데이터를 여러 번 조회할 때 빠름
- 단, 영속 컨텍스트는 요청 단위나 트랜잭션 단위로 생성해서 사용하므로
해당 요청 안에서 메모리 재사용이 있었을 경우에만 도움이 된다.
- 동일성 보장
- 같은 트랜잭션 안에서는 완전히 같은 인스턴스가 리턴되기 때문에 객체의 동일성이 보장된다.
- 또한 객체 값을 변경 해도 하나만 바꾸면 자동으로 반영됨
Member a = em.find(Member.class, "member1");
Member b = em.find(Member.class, "member1");
System.out.println(a == b); // true
5-2. 트랜잭션을 지원하는 쓰기 지연
1) 쓰기 지연
- 엔티티를 persist() 했을 때 SQL을 바로 DB에 보내지 않고 모아뒀다가 커밋 시점에 한꺼번에 보내는 방식
- persist()는 DB에 즉시 반영되지 않음 → commit() 시점에 INSERT 발생
- 따라서 엔티티 매니저는 데이터 변경시 반드시 트랜잭션을 시작해야한다.
2) 쓰기 지연 SQL 저장소
- DB로 보낼 SQL을 임시로 모아두는 공간, 영속성 컨텍스트 안에 존재한다.
- 예를 들어 em.persist(member) 호출 시 바로 INSERT SQL을 DB에 날리지 않고
- 쓰기 지연 SQL 저장소에 “이 member를 DB에 insert 해야 한다"는 정보를 임시로 보관
- 커밋 시점에 저장소에 모아두었던 정보를 SQL문으로 만들어서 DB에 한번에 전달한다.
3) 쓰기 지연의 목적: 성능 최적화
- DB와의 통신은 비용이 크기 때문에 SQL문을 한 번에 보내면 더 효율적
- 따라서 여러 개를 모아놓고 commit() 시점에 한꺼번에 SQL을 날린다.
5-3. 변경 감지 (Dirty Checking)
1) 변경 감지
- JPA는 영속 상태의 엔티티가 트랜잭션 안에서 변경되었는지를 감지해서, 자동으로 UPDATE SQL을 생성한다
- 즉, em.update()를 호출하고 자바 객체의 값만 바꿔도 트랜잭션 commit 시점에 알아서 UPDATE 쿼리가 날아감
Member memberA = em.find(Member.class, "memberA");
memberA.setUsername("hi");
memberA.setAge(10);
//em.update(member); //이런 코드는 필요하지 않다.
transaction.commit(); // 변경 감지 후 UPDATE 실행
2) 변경 감지 동작 원리: 스냅샷 (Snapshot)
- 엔티티를 em.find() 또는 persist()로 영속화할 때, JPA는 해당 객체의 원본 상태를 내부에 복사해둠 → 스냅샷
- 이후 트랜잭션 커밋 시점에 현재 객체 상태 vs 스냅샷 상태를 비교해서 달라진 부분이 있으면 UPDATE SQL을 생성
- 스냅샷 역시 영속성 컨텍스트 내부에 존재하지만 하지만 1차 캐시와 별도 구조로 관리
구분 | 목적 | 구조 | 위치 |
1차 캐시 | 엔티티를 캐싱해서 DB 접근 최소화 | Map<PK, Entity> | 영속성 컨텍스트 내부 |
스냅샷 | 변경 여부 감지 (Dirty Checking) | Map<FieldName, Value> | 영속성 컨텍스트 내부, 엔티티 단위로 별도 관리 |
6. 플러시 (flush)
6-1. 플러시란?
- 영속성 컨텍스트의 변경사항을 DB에 반영하는 작업
- 컨텍스트를 비우지 않음, 동기화만 수행
- 플러시 모드: em.setFlushMode(FlushModeType.XXXX);
- AUTO (기본): JPQL 실행 또는 트랜잭션 커밋 시 flush 자동 발생
- COMMIT: 커밋할 때만 flush
6-2. 플러시 발생 상황
1) em.flush() - 직접 호출
- 플러시는 영속성 컨텍스트 변경 내용을 DB에 동기화하는 작업
1. 변경 감지 (Dirty Checking)로 현재 엔티티 상태와 스냅샷 비교
→ 2. 수정된 엔티티가 있으면 '쓰기 지연 SQL 저장소'에 작업 정보 등록 (INSERT/UPDATE/DELETE)
→ 3. 저장소에 있는 작업들을 실제 SQL로 변환
→ 4. 변환된 SQL을 DB에 전송 (flush)
- flush는 SQL을 DB에 전송하지만 트랜잭션이 커밋되지 않았기 때문에, DB는 아직 변경 사항을 확정(commit)하지 않은 상태다.
→ 즉, 트랜잭션은 계속 살아 있고 rollback 시 변경 내용을 취소할 수 있다.
em.flush(); // SQL 날림 (DB에 반영됨)
em.getTransaction().rollback(); // 롤백하면 DB에서 반영 안 된 것처럼 처리됨
2) 트랜잭션 commit() - 플러시 자동 호출
- 내부적으로 먼저 flush() 호출해서 SQL 전송
- 이후 DB 트랜잭션을 commit해서 변경 내용을 확정
- 트랜잭션도 종료됨
em.persist(member);
em.getTransaction().commit();
// 1. flush() 자동 실행 → SQL 실행
// 2. commit → 진짜 DB 반영 확정, 트랜잭션 종료
3) JPQL 실행 - 플러시 자동 호출 (AUTO 모드 설정시)
- JPQL 실행 전에 DB와의 정합성 보장을 위해 flush가 자동 호출됨
- 만약 JPQL 실행 전 flush가 자동 발생하지 않는다면,
영속성 컨텍스트에만 있는 데이터는 DB에 반영되지 않기 때문에JPQL 결과에 반영되지 않는다. - JPQL 실행 시에도 commit 일어나지 않고 트랜잭션은 계속 살아있음
//저장 -> DB에 변경내용 반영되지 않음
em.persist(memberA);
em.persist(memberB);
em.persist(memberC);
//이후 JPQL 실행
//이때 JPQL 실행 전 플러시가 호출되지 않으면! 위의 저장 사항이 DB에 반영되지 않아 부정확한 결과가 나온다
query = em.createQuery("select m from Member m", Member.class);
List<Member> members= query.getResultList();
7. 영속/준영속 상태로 만들기
1) 영속 상태로 만들기
- 영속 상태란 영속성 컨텍스트에 의해 관리되는 상태
- 변경 감지, 쓰기 지연, 1차 캐시, 지연 로딩 같은 혜택을 받음
방법 | 설명 | 객체가 비영속 상태인 경우 | 객체가 이미 영속 상태인 경우 |
1. persist() | 명시적 등록 | 1차 캐시에 Map<id, entity=""> 형태로 저장 + 쓰기 지연 SQL 저장소에 INSERT 예약 |
IllegalArgumentException |
2. find() / JPQL 조회 |
DB 조회 시 자동 등록 | 1차 캐시에 저장 + INSERT 예약 없음 (이미 DB에 존재하므로) |
1차 캐시에 꺼내서 반환, 영속상태 유지 |
3. merge() | 병합 시 새로운 객체 생성 | 복사본이 1차 캐시에 저장됨 + 병합 대상이 신규면 INSERT, 기존이면 UPDATE 예약 |
UPDATE 예약 |
4. cascade | 부모 persist 시 연관 객체도 자동 등록 | 1차 캐시에 저장 + INSERT 예약 발생 (cascade 조건 만족 시) |
다시 persist 안 하고 무시됨 |
//1. persist()
Member member = new Member("member1", "현지");
em.persist(member); // 영속 상태로 전환됨
//2. find()
Member member1 = em.find(Member.class, "member1");
//1차 캐시에 있는지 확인 → 있으면 거기서 꺼내서 반환 (영속 상태 유지)
//없으면 DB 접근 → 조회 후 1차 캐시에 등록 → 영속 상태가 됨
//JPQL select 실행시 결과 리스트의 각 객체도 영속 상태 등록됨
query = em.createQuery("select m from Member m", Member.class);
List<Member> members= query.getResultList();
//3. merge() 호출 후 반환된 객체
Member detachedMember = new Member("member1", "현지");
detachedMember.setUsername("hyunji");
Member merged = em.merge(detachedMember); // merged는 영속 상태!
//단, detachedMember는 여전히 준영속 상태임
//4. @Transactional 환경에서 cascade = CascadeType.PERSIST
Team team = new Team("Dev Team");
Member member = new Member("member1");
team.addMember(member); // 연관관계 설정
em.persist(team); // cascade 설정돼 있으면 member도 같이 영속됨
2) 준영속 상태로 만들기
- 준영속이란 JPA 관리 밖으로 내보내는 것으로 JPA는 준영속 상태의 객체를 절대 반영하지 않는다
- 즉, detach된 엔티티는 DB와 연결이 끊긴 객체가 됨
- 또한 준영속 상태가 되면, 더 이상 컨텍스트의 기능(캐시, 쓰기 지연, 변경 감지 등)을 사용하지 못함
방법 | 설명 |
em.detach(entity) | 특정 엔티티만 분리 |
em.clear() | 모든 영속성 컨텍스트 초기화 현재 관리 중인 모든 엔티티가 준 영속 상태로 전환됨 |
em.close() | 영속성 컨텍스트 종료 em 자체도 쓸 수 없게 됨 |
Member member = em.find(Member.class, "member1");
em.detach(member); // member 객체 관리 중단
member.setUsername("Modified");
em.getTransaction().commit(); // ❌ DB에 UPDATE 안 됨!
- merge()를 쓰지 않고는 다시 영속 상태로 못 돌림
- 지연 로딩 (프록시 호출) 시 LazyInitializationException 같은 예외 날 수 있다
- 객체를 준영속 상태로 만드는 경우
- 주로 불필요한 감지/SQL 생성을 막고 싶을 때,
- 혹은 트랜잭션 외부에서 객체를 다루고 싶을 때