기록

Hibernate "unsaved transient instance" 오류 해결 방법 : CascadeType.ALL 본문

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 설정을 추가하는 것이다.
  • 애플리케이션의 요구사항에 따라 적절한 방법을 선택하자.
 
Comments