Web/Spring
Hibernate "unsaved transient instance" 오류 해결 방법 : CascadeType.ALL
zyin
2025. 3. 16. 16:04
1. 오류 상황
Spring Boot + JPA/Hibernate 환경에서 아래와 같은 오류를 만났다.
org.springframework.dao.InvalidDataAccessApiUsageException:
org.hibernate.TransientObjectException: object references an unsaved transient instance -
save the transient instance before flushing: com.example.model.AnswerHistoryEntity
이 오류는 영속화되지 않은(transient) 엔티티를 다른 엔티티와 함께 저장하려고 할 때 발생한다.
예제 코드
@Transactional
fun createQuizChallenge(req: ChallengeQuizReq, authReq: AuthReq): CreateChallengeResp {
val quizList = req.answers.map { it.quizId }.distinct()
val quizMap = quizRepository.findAllById(quizList).associateBy { it.quizId }
val challenge = Challenge(
status = ChallengeStatus.UNDER_REVIEW,
questId = req.questId,
customerId = authReq.userId,
answers = req.answers.map {
val quiz = quizMap[it.quizId] ?: throw IllegalArgumentException("퀴즈를 찾을 수 없습니다.")
AnswerHistoryEntity(content = it.answer, quiz = quiz)
}.toMutableList()
)
val savedChallenge = challengeRepository.save(challenge)
return CreateChallengeResp(savedChallenge)
}
이 코드에서 AnswerHistoryEntity는 아직 DB에 저장되지 않은 상태에서 Challenge에 추가되었기 때문에 Hibernate에서 오류가 발생한다.
2. Hibernate의 엔티티 생명주기
JPA에서 엔티티는 다음 4가지 상태를 가진다.
- Transient(비영속): 아직 영속성 컨텍스트에 저장되지 않은 상태
- Persistent(영속): EntityManager.persist() 또는 save()를 호출하여 저장된 상태
- Detached(준영속): 영속성 컨텍스트에서 분리된 상태
- Removed(삭제됨): delete() 등을 호출하여 삭제된 상태
오류의 원인은 AnswerHistoryEntity가 Transient 상태이기 때문이다.
Hibernate는 save(challenge)를 실행할 때 answers에 있는 AnswerHistoryEntity도 저장하려 하지만, 이 엔티티가 아직 영속 상태가 아니므로 오류가 발생한다.
3. 해결 방법
방법 1: AnswerHistoryEntity를 먼저 저장 후 Challenge에 추가
@Transactional
fun createQuizChallenge(req: ChallengeQuizReq, authReq: AuthReq): CreateChallengeResp {
val quizList = req.answers.map { it.quizId }.distinct()
val quizMap = quizRepository.findAllById(quizList).associateBy { it.quizId }
val challenge = Challenge(
status = ChallengeStatus.UNDER_REVIEW,
questId = req.questId,
customerId = authReq.userId,
answers = mutableListOf()
)
req.answers.forEach {
val quiz = quizMap[it.quizId] ?: throw IllegalArgumentException("퀴즈를 찾을 수 없습니다.")
val answerHistory = AnswerHistoryEntity(content = it.answer, quiz = quiz)
// 먼저 answerHistoryEntity를 저장
val savedAnswerHistory = answerRepository.save(answerHistory)
challenge.answers.add(savedAnswerHistory)
}
val savedChallenge = challengeRepository.save(challenge)
return CreateChallengeResp(savedChallenge)
}
✅ 변경 사항
- AnswerHistoryEntity를 먼저 저장한 후, Challenge에 추가하여 영속화.
- 이렇게 하면 Challenge 저장 시 answers가 이미 DB에 존재하기 때문에 오류가 발생하지 않는다.
방법 2: CascadeType.ALL을 설정하여 Challenge 저장 시 AnswerHistoryEntity도 자동 저장
@Entity
@Table(name = "tb_challenge_history")
data class Challenge(
@Id
@UuidGenerator
var challengeId: String? = null,
@Enumerated(EnumType.STRING)
var status: ChallengeStatus = ChallengeStatus.UNDER_REVIEW,
var questId: String? = null,
var customerId: String? = null,
@OneToMany(mappedBy = "challenge", cascade = [CascadeType.ALL], orphanRemoval = true)
var answers: MutableList<AnswerHistoryEntity> = mutableListOf()
)
이제 CascadeType.ALL을 추가했기 때문에 Challenge를 저장하면 answers도 함께 저장된다.
✅ 변경 사항
- CascadeType.ALL을 추가하여 Challenge 저장 시 AnswerHistoryEntity도 자동으로 저장되도록 변경.
- answerRepository.save()를 따로 호출하지 않아도 됨.
4. 해결 방법 비교
해결 방법 설명 장점 단점
방법 1: 명시적 저장 | AnswerHistoryEntity를 먼저 save() 후 Challenge에 추가 | 안정적, 필요한 경우만 저장 | 추가적인 save() 호출 필요 |
방법 2: Cascade 설정 | Challenge 저장 시 answers도 자동 저장 | 코드가 간결함 | 불필요한 곳에서도 자동 저장될 가능성 있음 |
✅ 추천 방법
- AnswerHistoryEntity가 독립적으로 저장될 필요가 있다면 방법 1 추천
- AnswerHistoryEntity가 Challenge의 일부로만 사용된다면 방법 2 추천
5. 결론
- Hibernate의 "unsaved transient instance" 오류는 영속화되지 않은 객체가 다른 엔티티와 함께 저장될 때 발생한다.
- 해결 방법은 객체를 먼저 저장하거나, Cascade 설정을 추가하는 것이다.
- 애플리케이션의 요구사항에 따라 적절한 방법을 선택하자.