Hibernate “object references an unsaved transient instance” 오류 해결 — 저장 전 flush 시점의 종속 엔티티 문제

검증 환경

본 절은 원문 사례·문서에 등장한 버전/도구를 정리한 것이다. 별도 실험실 재현이 명시되지 않은 항목은 일반화하지 않는다.

항목본문·원문에서 확인된 범위
런타임/도구Hibernate 3.
문서 정리일2026-09-01
  • OS/CI 세부 값은 프로젝트마다 다르므로, 적용 전 로컬에서 동일 오류 메시지를 재확인한다.

1. 문제 정의

Spring Boot / JPA(Hibernate)로 엔티티를 저장할 때 다음 에러가 발생하는 상황이다.

에러 원문: object references an unsaved transient instance - save the transient instance before flushing

1
2
org.hibernate.TransientPropertyValueException: object references an unsaved transient instance
  - save the transient instance before flushing

이 에러는 부모(parent) 엔티티 하나만 저장(save/persist) 하려고 하는데, 그 부모가 참조하는 **자식(child) 엔티티가 아직 DB에 없는 상태(transient)**일 때 발생한다. 질문 작성자는 “Hibernate로 객체를 저장했을 때 이 에러를 받는다"고만 설명했지만, 실제로는 연관된 컬렉션(또는 단일 연관)의 항목 중 하나 이상이 아직 데이터베이스에 존재하지 않는 것이 원인이다. (출처: StackOverflow 2302802)

이 글은 Root Cause Trace형으로, 왜 이 오류가 나는지를 엔티티 상태와 flush 시점 관점에서 추적하고, 상황별 해결책을 제시한다.

증상 fingerprint

항목
에러 메시지object references an unsaved transient instance - save the transient instance before flushing
예외 클래스org.hibernate.TransientPropertyValueException
발생 단계부모 엔티티 저장(save/persist) 후 flush 시점
관련 프레임워크Hibernate 3.x~6.x, JPA, Spring Data JPA
흔한 오해“부모만 저장하면 되지 않나?” → 아님. 참조된 자식이 모두 persistent여야 함

2. 원인 탐구 — 왜 이런 오류가 발생하는가

이 오류를 이해하려면 Hibernate가 엔티티를 4가지 상태로 관리한다는 점을 알아야 한다.

상태의미DB 행 존재 여부영속성 컨텍스트 관리
New / Transient새로 만들어진 객체, 세션에 연결된 적 없음없음아니요
Persistent / Managed세션에 연결되어 변경을 추적받음있음(또는 등록됨)
Detached세션이 닫혀 더 이상 추적되지 않음있음아니요
Removed삭제 예정있음삭제 처리 중

부모 엔티티에 @OneToMany 컬렉션이나 @OneToOne 연관이 있고, 그 컬렉션/연관의 자식 객체가 아직 persist되지 않은(New 상태) 상태라면 문제가 된다.

1
2
3
Parent parent = new Parent();
parent.getChildren().add(new Child());  // Child는 아직 transient!
session.save(parent);                    // flush 시점에 에러 발생

부모를 save하면 Hibernate는 flush 시점에 연관 객체들의 상태도 함께 동기화하려 한다. 이때 자식이 transient(New) 상태인 채로 남아 있으면, Hibernate는 “이 자식 객체를 아직 저장하지 않았는데 부모가 참조하고 있다"는 모순을 만나 TransientPropertyValueException을 던진다.

3. 근본 원인 분석 — 실제 근본 원인 파악

핵심 근본 원인은 연관 매핑에 cascade(영속성 전이) 설정이 없어서, 부모를 저장할 때 자식까지 저장될 경로가 없음에도 불구하고 transient 자식을 참조하면, flush 시점에 Hibernate가 자식을 자동으로 저장할 수 없기 때문이다.

정리하면:

  1. 부모 저장 시 flush 동기화가 연관 객체까지 도달한다.
  2. 그 연관 객체가 영속 컨텍스트와 무관한 New(transient) 상태다.
  3. cascade가 없으면 Hibernate는 transient 객체를 자동으로 DB에 기록하지 못하고 예외를 던진다.

즉, 단순히 “부모를 저장했다"는 것이 아니라 **“자식을 미리 저장하지 않은 채 부모만 저장하려 했고, 그 전이 경로(cascade)도 없었던 것”**이 근본 원인이다.

또한 cascade가 있더라도 양방향 연관에서는 부모와 자식의 참조가 서로 동기화되어 있어야 cascade가 올바르게 동작한다. 부모만 자식을 가리키고 자식은 부모를 가리키지 않으면 영속 전이가 누락될 수 있다.

4. 코드 해결책 — 단계별 해결 방법

4-1. 연관 매핑에 cascade 추가 (주요 해결)

부모 참조가 자식을 함께 저장하도록 연관 매핑에 cascade를 추가한다.

1
2
3
// @OneToMany 양방향 - 자식 목록
@OneToMany(mappedBy = "post", orphanRemoval = true, cascade = CascadeType.ALL)
private List<Comment> comments = new ArrayList<>();
1
2
3
// @OneToOne - 단일 연관
@OneToOne(mappedBy = "post", orphanRemoval = true, cascade = CascadeType.ALL)
private PostDetails details;

이렇게 하면 부모를 save하는 시점에 cascade에 따라 자식도 함께 INSERT 된다. StackOverflow 답변에서도 “@OneToOne과 @OneToMany에서 모두 Child 객체를 저장하기 전에 부모를 save하면 이 에러가 나타나며, 부모의 Child 참조에 cascade = CascadeType.ALL을 추가하면 문제가 해결된다"고 설명한다.

4-2. @ManyToMany는 특별 처리

@ManyToMany에서는 CascadeType.ALL을 쓰지 않는다. ALL 속에는 삭제(detach, remove)가 포함되어 있어, 한 부모에서 다른 부모로 delete가 전이될 수 있기 때문이다. 그래서 @ManyToMany는 PERSIST/MERGE만 전이하도록 하는 것이 안전하다.

1
2
@ManyToMany(mappedBy = "authors", cascade = { CascadeType.PERSIST, CascadeType.MERGE })
private List<Book> books = new ArrayList<>();

(일반적인 점검 기준) @ManyToMany에서 cascade delete 여파가 있을 수 있으므로 PERSIST/MERGE만 전이하는 설계가 안전하다. 환경에 따라 다를 수 있다.

4-3. cascade 없이 자식을 먼저 저장

cascade를 사용하지 않으려면 자식을 먼저 save/persist한 후 부모를 save하면 된다.

1
2
3
4
Child child = new Child();
parent.getChildren().add(child);
entityManager.persist(child);   // 1. 자식 먼저
entityManager.persist(parent);   // 2. 부모 나중 - 이제 자식은 persistent

질문 속 답변에서도 “각 객체를 생성한 후 올바른 순서로 저장하라"고 안내한다.

5. 향후 예방 조치 — 재발 방지

아래 검증 명령으로 저장 후 flush가 성공하는지를 확인한다.

검증 명령 (Spring Boot + JPA):

1
2
3
4
# 테스트 클래스에서 save 후 flush가 예외 없이 진행되는지 확인
mvn -q test -Dtest=YourEntityPersistenceTest
# 또는 Gradle
./gradlew test --tests "*YourEntityPersistenceTest*"

테스트 코드에서는 부모 저장 후 repository.saveAndFlush(parent)가 예외 없이 돌아가고, 자식이 조회되는지 단언한다.

재발 방지 체크리스트

  1. 연관 매핑마다 cascade가 필요한지 확인 — 전이가 꼭 필요한 곳에만 설정한다.
  2. cascade 없이 transient 참조를 만들지 말기 — 자식을 먼저 persist하거나 cascade를 부여한다.
  3. 양방향 연관 동기화 — 부모와 자식 모두 참조를 설정해야 cascade가 올바르게 동작한다.
  4. @ManyToMany는 PERSIST/MERGE만 — 삭제 여파에 유의한다.
  5. @JsonIgnore로 id가 누락될 때 주의 — 연관 객체 식별자가 빠지면 Hibernate는 그 객체를 새로운 것으로 오인하여 transient로 판정한다.

잘못된 해결책 (하지 말 것)

  • merge를 임시방편으로 사용 — 어떤 경우 동작할 것처럼 보이지만, transient 객체에 merge를 쓰면 사본 행이 생길 수 있고 올바른 영속화 방식이 아니다.
  • 모든 연관에 무조건 CascadeType.ALL 남발 — 특히 @ManyToMany에서 위험하며, 원하지 않는 자식 삭제(delete)가 전이될 수 있다.
  • id와 version 없이 “그냥 저장하면 되겠지” — version 필드가 NULL이면 이 예외로 표면화될 수 있는 등, 정상 경로(자식을 먼저 저장하거나 cascade)를 쓰는 것이 올바르다.

DevTrace verdict

이 문제의 핵심은 “부모만 저장하면 된다"는 오해가 아니라, Hibernate가 flush 시점에 참조된 transient 자식을 자동으로 저장할 경로(cascade 또는 직접 persist)를 갖지 못했기 때문이며, 따라서 올바른 해결은 연관 매핑에 cascade를 작성하거나 자식을 먼저 저장하는 것이다.

DevTrace 결론

Hibernate의 ‘unsaved transient instance’ 오류는 부모 저장 시 종속 엔티티가 DB에 저장되지 않아 발생하며, 연관 매핑에 cascade를 설정하거나 자식을 먼저 저장하면 해결되는 문제다.

원문 출처는 문제 발견의 단서이며, 위 판단과 점검 항목은 DevTrace의 독자 분석이다.

출처: