Java & Spring/JPA

Spring Data JPA는 내부적으로 어떻게 동작할까?

승주우에요 2026. 4. 9. 08:01

1. JPA vs Spring Data JPA

둘을 혼용해서 쓰는 경우가 많지만 역할이 다릅니다.

**JPA (Java Persistence API)**는 자바 ORM 표준 명세입니다. 인터페이스만 정의하고, 실제 구현체는 Hibernate가 담당합니다.

Spring Data JPA는 JPA 위에 얹힌 추상화 레이어입니다. JpaRepository를 상속하는 것만으로 findById(), save(), delete() 같은 기본 CRUD를 자동으로 제공합니다.

Spring Data JPA
      ↓
    JPA (인터페이스)
      ↓
  Hibernate (구현체)
      ↓
    JDBC
      ↓
    DB

즉, 우리가 userRepository.save(user)를 호출하면 내부적으로 Hibernate가 실제 SQL을 생성하고 실행합니다.


2. 영속성 컨텍스트란

영속성 컨텍스트(Persistence Context)는 JPA가 엔티티를 관리하는 메모리 공간입니다.

-> JVM 힙 메모리에 생성되고, 트랜잭션 단위로 생성됨!

DB에서 엔티티를 조회하면 결과를 바로 반환하는 것이 아니라, 이 공간에 올려두고 관리합니다. 영속성 컨텍스트에 올라간 엔티티를 영속 상태(managed) 라고 합니다.

@Transactional
public void example(Long userId) {
    User user = userRepository.findById(userId).orElseThrow();
    // 이 시점에 user는 영속성 컨텍스트에 올라간 영속 상태
}

영속성 컨텍스트는 EntityManager가 관리하며, Spring Data JPA에서는 트랜잭션 단위로 생성되고 종료됩니다.

트랜잭션이 끝나면 영속성 컨텍스트도 함께 종료됩니다. 이후 엔티티는 더 이상 관리되지 않는 준영속 상태가 됩니다.


3. 엔티티 생명주기

엔티티는 영속성 컨텍스트와의 관계에 따라 네 가지 상태를 가집니다.

비영속 (new)
    ↓  persist()
영속 (managed)         ← 영속성 컨텍스트가 관리
    ↓  detach() / 트랜잭션 종료
준영속 (detached)      ← 한때 관리했지만 끊긴 상태
    ↓  merge()
영속 (managed)         ← 다시 관리 상태로
    ↓  remove()
삭제 (removed)

비영속 (new)

영속성 컨텍스트와 전혀 관계없는 상태입니다. new 키워드로 막 생성한 엔티티가 여기 해당합니다.

User user = new User("승주");  // 비영속 상태

영속 (managed)

영속성 컨텍스트에 의해 관리되는 상태입니다. 이 상태에서 발생한 변경은 트랜잭션 커밋 시 자동으로 DB에 반영됩니다.

em.persist(user);                          // 비영속 → 영속
User user = userRepository.findById(id);   // 조회 시 자동으로 영속 상태

준영속 (detached)

한때 영속 상태였지만 영속성 컨텍스트와의 연결이 끊긴 상태입니다. 트랜잭션이 종료되면 영속 상태였던 엔티티는 모두 준영속 상태가 됩니다.

em.detach(user);  // 명시적으로 준영속 전환
// 또는 트랜잭션 종료 시 자동으로 준영속 전환

준영속 상태의 엔티티를 수정해도 DB에 반영되지 않습니다. 다시 반영하려면 merge()나 save()를 통해 영속 상태로 되돌려야 합니다.

삭제 (removed)

remove()를 호출한 상태입니다. 트랜잭션 커밋 시 DELETE SQL이 실행됩니다.


4. 1차 캐시

영속성 컨텍스트는 내부적으로 1차 캐시를 가지고 있습니다. 엔티티를 조회하면 DB에 바로 접근하는 것이 아니라 1차 캐시를 먼저 확인합니다.

@Transactional
public void example(Long userId) {
    User user1 = userRepository.findById(userId).orElseThrow();
    // → DB 조회 후 1차 캐시에 저장

    User user2 = userRepository.findById(userId).orElseThrow();
    // → 1차 캐시에서 바로 반환 (DB 조회 없음)

    System.out.println(user1 == user2);  // true (같은 인스턴스)
}

같은 트랜잭션 내에서 같은 엔티티를 여러 번 조회해도 SQL이 한 번만 실행되는 이유가 여기 있습니다.

1차 캐시는 트랜잭션 단위로 생존합니다. 트랜잭션이 종료되면 캐시도 함께 사라집니다. Redis나 Ehcache 같은 2차 캐시와는 다릅니다.


5. flush와 commit

스냅샷

영속성 컨텍스트는 엔티티를 처음 영속 상태로 만들 때 스냅샷을 함께 보관합니다. 스냅샷은 해당 시점의 엔티티 상태를 그대로 복사한 것입니다.

영속성 컨텍스트
├── 1차 캐시
│     └── User { id: 1, name: "승주" }  ← 현재 상태
└── 스냅샷
      └── User { id: 1, name: "승주" }  ← 최초 조회 시점 상태

flush

flush는 영속성 컨텍스트의 변경 사항을 DB에 반영하는 동작입니다. 이때 현재 엔티티 상태와 스냅샷을 필드 단위로 비교해 변경이 있으면 SQL을 생성합니다. 이것이 Dirty Checking입니다.

dirty checking (현재 상태 vs 스냅샷 비교)
    ↓
변경 감지 → UPDATE SQL 생성
    ↓
  flush (DB에 SQL 전송)
    ↓
트랜잭션 커밋 (영구 반영)

 

flush는 세 가지 시점에 발생합니다.

// 1. 트랜잭션 커밋 시 자동 flush
@Transactional
public void update() { ... }  // 메서드 종료 시 커밋(flush 트리거) → flush -> 커밋

// 2. JPQL 쿼리 실행 전 자동 flush
// (DB와 영속성 컨텍스트 동기화를 위해)

// 3. 명시적 flush 호출
em.flush();

commit

flush는 SQL을 DB에 전송하지만 아직 트랜잭션이 끝난 것은 아닙니다. commit이 되어야 비로소 변경 사항이 DB에 영구 반영됩니다.

flush  → SQL을 DB에 전송 (아직 롤백 가능)
commit → 변경 사항 영구 반영 (롤백 불가)

flush ≠ commit입니다. flush는 SQL 전송, commit은 트랜잭션 확정입니다.


마무리

Spring Data JPA의 동작을 이해하는 데 핵심은 영속성 컨텍스트이다.

  • 엔티티는 영속성 컨텍스트에 의해 관리되며 생명주기를 갖는다.
  • 1차 캐시로 같은 트랜잭션 내 중복 쿼리를 방지합니다.
  • flush 시점에 스냅샷과 비교해 변경된 엔티티를 자동으로 감지합니다.