JPA를 사용하다 보면 이런 의문이 생깁니다.
“save() 직후 ID가 생기는데, DB에 저장 된건가?”
이 질문의 답은 트랜잭션의 동작 흐름 안에 있습니다.
persist(), flush(), commit()은 각각 “메모리에 저장 -> SQL 실행 -> 트랜잭션 확정” 단계를 의미하며, ID는 이 과정 속에서 점진적으로 생성되고 확정됩니다.
이 글에서는 트랜잭션 내부에서 ID가 생성되고, DB에 반영되고, 다른 트랜잭션에서 보이게 되는 전 과정을 순서대로 살펴보겠습니다.
1. 비영속 상태
ScheduleEntity schedule = new ScheduleEntity();
System.out.println(schedule.getId()); // 대부분 null (직접 세팅 안 했다면)
단순히 new 한 상태는 JPA가 아직 관여하지 않은 비영속(Transient) 상태입니다.
즉, Hibernate가 관리하지 않기 때문에 ID가 세팅되지 않습니다.
UUID나 @GeneratedValue(strategy = GenerationType.UUID)라면 생성자에서 바로 값이 들어올 수 있지만, 일반적으로 AUTO_INCREMENT, IDENTITY, SEQUENCE 전략이라면 이 시점에는 ID = null 입니다.
2. save 호출 시점 (영속 상태)
scheduleRepository.save(schedule);
이때 비로소 Hibernate가 엔티티를 영속성 컨텍스트에 등록하고,
@GeneratedValue(strategy = …) 전략에 따라 ID를 생성하거나 조회합니다.
| 전략 | ID 생성 타이밍 | 설명 |
| IDENTITY | INSERT SQL 실행 시 | Hibernate가 즉시 INSERT를 날리고 DB의 AUTO_INCREMENT 결과를 getGeneratedKeys()로 받아옴 |
| SEQUENCE | save() 직후 | DB에서 nextval을 미리 조회하여 ID를 세팅 (INSERT는 flush 때 실행) |
| TABLE | save() 직후 | ID 테이블을 조회해 새 번호를 가져와 세팅 (INSERT는 flush 때 실행) |
| UUID | new 시점 또는 엔티티 생성자 내부 | save 전에도 이미 존재 |
3. save() 이후의 상태
System.out.println(schedule.getId()); // 이제는 값이 있음
save() 시점에 이미 Hibernate가 ID를 생성하거나 조회하므로 이제부터 ID는 항상 존재합니다.
다만, 아직 flush() 전이라 SQL이 DB에 반영되었을 수도 있고 아닐 수도 있습니다.
(IDENTITY면 즉시 반영, SEQUENCE면 flush 때 반영)
이렇게 id는 생성되는 방식을 알았는데 해당 id를 어느 영역까지 사용할 수 있을까?
@Service
@RequiredArgsConstructor
public class IdLifecycleExampleService {
private final EntityManager em;
@Transactional
public void idLifecycleExample() {
// 1️⃣ new — 비영속 상태
ScheduleRepeatEntity repeat = new ScheduleRepeatEntity();
repeat.setTitle("before save");
System.out.println("new 이후 → id = " + repeat.getId()); // ❌ null
// 2️⃣ persist/save — 영속성 컨텍스트 등록
em.persist(repeat);
System.out.println("persist 이후 → id = " + repeat.getId()); // ✅ 존재 (IDENTITY면 즉시, SEQUENCE면 미리 할당)
// 하지만 아직 flush 전이므로 DB에는 반영되지 않았을 수도 있음
// 3️⃣ flush — SQL 실제 전송(DB에 반영)
em.flush(); // 이 시점에 INSERT SQL 발생
System.out.println("flush 이후 → id = " + repeat.getId()); // ✅ 동일한 id (변화 없음)
// DB 직접 확인
Long count = em.createQuery("SELECT COUNT(r) FROM ScheduleRepeatEntity r", Long.class).getSingleResult();
System.out.println("flush 이후 DB 조회 count = " + count); // ✅ 1 (DB에 반영됨)
// 4️⃣ commit — 트랜잭션 종료 후
// commit은 @Transactional이 끝날 때 자동으로 발생하므로,
// 여기서 별도 출력은 로그 상에서 확인 가능
}
}
시점에 따른 id의 존재 여부
| 시점 | 상태 | ID 존재 여부 | DB 반영 여부 | JDBC 접근 가능 여부/ FK 검증 |
| new | 비영속 상태 | ❌ 없음 | ❌ | ❌ |
| persist/save | 영속 상태 (1차 캐시) | ✅ 생성됨 | ❌ (flush 전) | ❌ |
| flush | DB 반영됨 (트랜잭션 미커밋) | ✅ 그대로 유지 | ✅ INSERT 반영 | ✅ 가능 |
| commit | DB 확정 (영구 적용) | ✅ 유지 | ✅ 영구 반영 | ✅ 가능 (다른 트랜잭션에서도 조회 가능) |
JPA -> DB 전체 데이터 저장 경로
| 단계 | 논리적 영역 | 물리적 저장 위치 | 주요 역할 |
| persist() 이후 | 영속성 컨텍스트 (1차 캐시 포함) |
Spring JVM 힙 메모리 (RAM) | 엔티티 객체를 HashMap(EntityKey → Entity) 형태로 관리 |
| flush() 이후 | DB 버퍼 풀 (InnoDB Buffer Pool) |
DB 서버의 메인 메모리 (RAM) | SQL 실행 결과가 메모리 버퍼에 반영 (아직 디스크 기록 X) |
| commit() 시점 | Redo / Undo Log (트랜잭션 로그 영역) |
DB 서버 디스크 (스토리지의 로그 파일 영역) | 트랜잭션 커밋 시 Redo Log를 디스크로 flush(fsync)하여 복구 가능 상태로 만듦 |
| Checkpoint / Lazy Write | 데이터 파일 (물리적 테이블 저장소) |
DB 서버 디스크 (HDD / SSD의 .ibd / .frm 파일) | Buffer Pool의 Dirty Page가 디스크 파일로 기록됨 |
| 이후 (다른 트랜잭션 접근 가능) | 커밋된 데이터 가시화 | DB 디스크 + OS 캐시 메모리 | 모든 트랜잭션에서 읽을 수 있는 상태 |
persist와 save의 차이
| 상황 | persist | save |
| 이미 DB에 존재하는 엔티티에 호출 시 | ❌ 예외(EntityExistsException) 발생 가능 | ✅ merge() 동작으로 UPDATE |
마무리
JPA에서 ID는 단순히 “DB에 INSERT하면 생기는 값”이 아닙니다.
Hibernate는 @GeneratedValue 전략과 영속성 컨텍스트를 활용해, DB와 JVM 사이에서 식별자 값을 미리 관리하고 동기화합니다.
'Backend Programming' 카테고리의 다른 글
| 정말 HTTPS에 대해서 알고 있는가? (1) | 2025.10.28 |
|---|---|
| 데이터베이스 구조 리팩토링 및 마이그레이션 경험 공유 (3) | 2025.07.30 |
| 빌드 실패 디버깅 (1) | 2025.03.07 |
| CI/CD? 배포 자동화를 해보자 (0) | 2024.12.13 |
| EC2를 활용한 HTTPS 및 도메인 설정 (3) | 2024.12.12 |