[Spring] JPA 영속성 컨텍스트

2025. 11. 10. 08:02Dev./Spring

728x90
반응형

영속성 컨텍스트

데이터베이스와 애플리케이션 사이에 JPA가 만든 임시 저장소(캐시)

영속성 컨텍스트의 역할

역할 설명 이해를 위한 비유
① 1차 캐시 같은 엔티티를 두 번 조회해도 DB에 다시 안 감 한 번 불러온 책은 책상 위에 둔다
② 변경 감지 (Dirty Checking) 엔티티의 필드가 바뀌면 자동으로 UPDATE 감지 책을 수정하면 포스트잇으로 표시해둔다
③ 쓰기 지연 (Flush 시점에 DB 반영) INSERT, UPDATE, DELETE가 트랜잭션 끝날 때 실행됨 숙제는 한 번에 제출한다
  • DB 접근을 최소화한 ORM이 효율적인 ORM

생명주기에 따른 4가지 상태

상태 설명 예시
비영속 (new) 아직 JPA에 저장 안 됨 new Todo("공부")
영속 (managed) EntityManager에 저장되어 관리 중 em.persist(todo) or repository.save(todo)
준영속 (detached) 한때 관리됐지만 지금은 연결 끊김 트랜잭션 끝나거나 em.detach()
삭제 (removed) 삭제 예약됨 em.remove(todo) 후 flush 시 DB 반영

생명주기 흐름

  • 트랜잭션이 시작되면, 영속성 컨텍스트가 새로 만들어짐
  • find()나 save()로 엔티티를 등록하면, 컨텍스트에 저장
  • 같은 엔티티를 또 조회해도, DB 대신 캐시(1차 캐시)에서 반환
  • 엔티티의 필드가 바뀌면 JPA가 변경을 감지하고 기록
  • 커밋 시점(flush)에 변경사항을 모아 SQL로 한 번에 DB에 반영

왜 스프링 프로젝트의 대부분의 트러블 슈팅의 원인이 영속성 컨텍스트일까?

  • 영속성 컨텍스트는 JPA의 모든 자동화가 일어나는 중심축이기 때문 , 데이터 변경·조회·삭제, 캐싱, 트랜잭션, flush 등 굉장히 많은 기능을 다루는 만큼 문제도 가장 많이 생김
  • JPA의 장점은 SQL을 몰라도 개발자가 CRUD가 가능하다는 것 → 문제는 눈에보이지 않기 때문에 영속성 컨텍스트 동작 타이밍과 연관이 깊음
    • 쿼리 타이밍 : 분명 save()가 없는데 왜 저장될까?, update안했는데 왜 값이 바뀌어? → 다 영속성 컨텍스트 동작 타이밍문제
    • 1차 캐시로 데이터 불일치 : DB 바꿨는데 반영이 안되네 → 영속성 컨텍스트의 특징 캐시 때문
    • N+1 : 하나 조회하는데 쿼리가 두번이 나가네 → 언제 DB를 접근할지 모르기 때문, 컨텍스트 fetch 시점 문제

영속성 컨텍스트와 트랜잭션의 관계

⭐️ 영속성 컨텍스트는 트랜잭션 단위로 만들고 사라짐

  • 트랜잭션 시작 → 영속성 컨텍스트 생성 ↓ CRUD (엔티티 상태 관리) ↓ flush → commit → close()

JPA 내부의 흐름

  1. @Transactional 트랜잭션 시작
    • 새로운 영속성 컨텍스트를 생성한다. ( 생성의 단위가 트랜잭션)
  2. 엔티티 조회 or 등록
    • DB에서 데이터를 가져와 영속성 컨텍스트 내부 1차 캐시에 저장
  3. 엔티티 변경
    • DB 쿼리 안날림. 영속성 컨텍스트 내부에 Dirty로 구분
  4. 커밋 시점
    • JPA가 Dirty 들을 찾아 flush() → SQL 생성 및 실행 → 커밋
  5. 트랜잭션 종료
    • 영속성 컨텍스트 즉 임시저장소가 닫히며 캐시 삭제, 엔티티도 준 영속 상태가 됨

그래서 왜 트랜잭션이 중요한걸까?

  • JPA는 단독으로 존재 할 수 없음. 항상 트랜잭션 안에서 생성되고 소멸
  • ⭐️⭐️⭐️트랜잭션이 영속성 컨텍스트의 수명임⭐️⭐️⭐️
    • JPA 내부 흐름이 생명주기 흐름과 같은 이유
상황 영속성 컨텍스트 상태
트랜잭션 안 활성 (1차 캐시 유지, 변경 감지 작동)
트랜잭션 끝남 소멸 (준영속 상태 전환, 캐시 초기화)

요약

트랜잭션 동안만 임시저장소에 있다가 커밋할때 한번에 DB 반영

  • 트랜잭션 = 영속성 컨텍스트의 수명
  • flush = DB로 실제 반영
  • commit = flush + 트랜잭션 종료
@Transactional
public void example() {
Schedule schedule = new Schedule("공부");
scheduleRepository.save(schedule);  // INSERT 안 나감
schedule.setTitle("복습");      // UPDATE 안 나감
} // ✅ 트랜잭션 끝날 때 INSERT + UPDATE 실행됨 (flush + commit)

그럼 영속성 컨텍스트가 왜 그렇게 중요한걸까?

영속성 컨텍스트를 이해하지 못하면 JPA 사용할때 raw SQL 쓰는 것만 못하다는 것과 훨씬 복잡하고 어려울거라는 이야기를 들었다.

영속성 컨텍스트 이해 못하고 JPA 사용 == 나 너가 통제 불가능한 버그 발생할께! 나 찾아바! 야근 좋아!

1. 데이터 일관성의 중심

DB단과 앱단 사이의 중간이라 영속성 컨텍스트가 없으면 JPA는 그저 SQL 실행 도구

영속성 컨텍스트의 흐름(생명주기 흐름) 덕분에 단순 ORM이 아니라 데이터 일관성 시스템으로서 사용할 수 있음

2. SQL로는 불가능한 객체 단위 개발 가능

이전에는 개발자가 raw SQL 직접 날려야했었음, 객체 변환도 직접 작성

ResultSet rs = stmt.executeQuery("SELECT * FROM schedule");
Schedule schedule = new Schedule(rs.getString("title"));

JPA는

Schedule schedule = scheduleRepository.findById(1L).get();
schedule.setTitle("할일 완료");

이게 가능한 이유가 영속성 컨텍스트가 Schedule 객체의 생명주기를 추적해 커밋시점에 알아서 update 쿼리를 날려주기 때문

즉, 개발자는 객체만 다룰 수 있게 됨 DB 상태를 알아서 맞춰줌

하지만 알아서 맞춰주기때문에 영속성 컨텍스트를 모르면 SQL 실행 타이밍을 예측 할 수 없음. 우리가 설계한 대로 코드가 실행되지 않을 수 있음 → 그래서 JPA를 모르면 명확하게 SQL날리는게 더 쉬울거같다는 이야기가 나온거같음

3. 성능과 안정성의 핵심 최적화

DB접근을 최소화해야 효율적임

4. 캐시,트랜잭션,성능 최적화등 전략이 꼬임

JPA의 대부분 성능 최적화 == 영속성 컨텍스트의 캐시를 얼마나 잘 활용할 수 있는가

예시

  • 1차 캐시 → 중복 조회 방지
  • Dirty Checking → 자동 변경 감지
  • Flush 타이밍 → SQL 배치 최적화
  • Fetch 전략 → N+1 방지

이전 N+1문제가 발생했던것도 영속성 컨텍스트 개념을 제대로 잡지 못했기 때문


영속성 컨텍스트 실습

package com.todolist;

import com.todolist.entity.Todo;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

@Component
public class JpaConsoleTest implements CommandLineRunner {

    @PersistenceContext
    private EntityManager em;

    @Override
    @Transactional
    public void run(String... args) throws Exception {
        System.out.println("\n================= JPA 영속성 컨텍스트 실험 =================");

        // 1.비영속 상태
        Todo todo = new Todo("영속성 컨텍스트 테스트", "persist 전 상태", "diy0ung", "1234");
        System.out.println("[비영속] em.contains(todo)? → " + em.contains(todo)); 

        // 2.영속 상태로 전환
        em.persist(todo);
        System.out.println("[영속] em.contains(todo)? → " + em.contains(todo)); 
        System.out.println("[영속] 아직 INSERT SQL 안 나감 (flush 전)");

        // 3.flush 실행 (쓰기 지연 SQL 실행)
        em.flush();
        System.out.println("[flush 이후] INSERT SQL 실행됨");

        //4.1차 캐시 확인 (같은 ID로 조회 시, DB 접근 안 함)
        Todo find1 = em.find(Todo.class, todo.getId());
        Todo find2 = em.find(Todo.class, todo.getId());
        System.out.println("[1차 캐시] find1 == find2 ? → " + (find1 == find2));

        // 5.변경 감지 (Dirty Checking)
        find1.updateTitle("제목 변경됨");
        System.out.println("[Dirty Checking] 엔티티 수정했지만 flush 전이라 아직 UPDATE 없음");
        em.flush(); // UPDATE SQL 실행
        System.out.println("[Dirty Checking] flush 시점에 UPDATE SQL 나감");

        // 6.clear로 영속성 컨텍스트 초기화
        em.clear();
        System.out.println("[clear 이후] em.contains(find1)? → " + em.contains(find1)); 

        // 7.다시 조회 → DB에서 SELECT 발생 (1차 캐시 날아갔으므로)
        Todo find3 = em.find(Todo.class, todo.getId());
        System.out.println("[DB 조회] find1 == find3 ? → " + (find1 == find3)); 

        System.out.println("================= 실험 종료 =================\n");
    }
}

예상

  1. 비영속 상태이니 false
  2. 영속 상태에선 true
  3. clear 이후에는 캐시가 없으니 false
  4. 다시 조회 해도 1차 캐시가 없으니 false

콘솔

================= JPA 영속성 컨텍스트 실험 =================
[비영속] em.contains(todo)? → false
[영속] em.contains(todo)? → true
[영속] 아직 INSERT SQL 안 나감 (flush 전)
Hibernate:
    insert into todos (...) values (...)
[flush 이후] INSERT SQL 실행됨
[1차 캐시] find1 == find2 ? → true
[Dirty Checking] 엔티티 수정했지만 flush 전이라 아직 UPDATE 없음
Hibernate:
    update todos set title=? where id=?
[Dirty Checking] flush 시점에 UPDATE SQL 나감
[clear 이후] em.contains(find1)? → false
[DB 조회] find1 == find3 ? → false
================= 실험 종료 =================

실습 포인트 정리

  영속성 컨텍스트 개념 핵심 키워드
em.persist() 엔티티를 영속 상태로 등록 1차 캐시 등록
em.flush() SQL 실행 시점 강제 쓰기 지연 저장소
em.find() 1차 캐시 조회 DB 접근 최소화
엔티티 수정 + flush() 변경 감지 (Dirty Checking) 자동 UPDATE
em.clear() 캐시 초기화 detached 상태
다시 find() DB 접근 발생 1차 캐시와 DB 동기화

개념 정리 후 실습을 통해 영속성 컨텍스트가 눈에 보이지 않는 캐시구나 라고 체감이 되었다.

728x90
반응형

'Dev. > Spring' 카테고리의 다른 글

[Spring] JPA N+1 문제  (0) 2025.11.07
[Spring] JPA 관계 설정(1:N/N:1)  (0) 2025.11.05
[Spring] 수정 사항 발생시 자동 리빌드 Auto Reload  (0) 2025.11.04
[내배캠] 한달 회고  (0) 2025.10.31