JPA 서버에 대한 작업을 진행하던 도중, 이해가 되지 않는 오류를 발견하였다.
문제 상황과 유사한 예시는 다음과 같다.
@Entity
class Wife (
id: Int,
name: String,
){
@Default
constructor(name: String) : this(0, name)
@Id
@GeneratedValue
val id: Int = id
@Column(nullable = false)
var name: String = name
@OneToOne(cascade = [CascadeType.ALL])
@JoinColumn(name = "husband", nullable = false)
private var _husband: Husband = Husband(this)
val husband get() = _husband
@Column(nullable = false)
var isDeleted: Boolean = false
fun marriage(id: Int) {
this._husband = Husband(
id,
this,
)
}
}
@Entity
class Husband(
id: Int,
name: String,
wife: Wife,
) {
@Default
constructor(name: String, wife: Wife) : this(0, name, wife)
@Id
@GeneratedValue
var id = id
@Column(nullable = false)
var name: String = name
@OneToOne(mappedBy = "wife")
var wife: Wife = wife
var isDeleted: Boolean = false
}
여성쪽에서만 재혼이 가능하고, 남성쪽에선 배우자가 없다면 존재할 수 없는 이상한 상황은 오직 적절한 예시만을 위한것이니
여성기혼자들의 재혼상대를 등록하기 위한 것이라 가정하자.
fun DoSomething() {
wife = Wife(
name = "Dizzy",
)
husband = Husband(
name = "Sol",
wife = wife
)
husbandRepository.save(husband)
//...
}
간단하게 위와 같은 코드를 실행해보자.
save the transient instance before flushing
위와 같은 에러가 발생하며 바로 터져나간다.
원인을 찾아보니 wifeRepositry에서 ‘Dizzy’ 를 저장하지 않아 ‘Dizzy’는 현재 ‘transient’ 상태로 ID조차 부여되지 않은 상태이다.
그런데 Husband ‘Sol’ 에서, ‘transient’ 상태인 ‘Dizzy’ 의 ID를 사용하려하니 실행되지 않는다.
wife = Wife(
name = "Dizzy",
).run(wifeRepository::save)
곧장 수정 후 실행하였지만 전혀 예상치 못한 결과가 나왔다.

‘Dizzy’, ‘Dizzy와 @OneToOne으로 연결된 _husband’ 와 ‘Sol(husband)’ 의 레퍼런스 값이 다르다.
JPA에서 무언가 알 수 없는 일이 벌어지고 있다.
원인이 무엇인지 기나긴 삽질과 구글링의 시간을 통해 찾아보았다.
다행히도 나와 매우 비슷한 상황을 찾아낼 수 있었다.
결론부터 말하자면,
‘Dizzy’, 즉 wife 객체를 save할 때 cascade가 flush보다 먼저 발생해, 먼저 ‘Dizzy’에 연결된 ‘husband’ 객체가 생성된다.
이 타이밍부터 이미 wife 객체는 husband 객체를 들고 있는 상태가 된다.
fun DoSomething() {
wife = Wife(
name = "Dizzy",
).run(wifeRepository::save)
/*
wifeRepository.save를 실행한 시점부터, 'Dizzy'객체는 ID까지 부여된 husband객체를 들고있다.
이 husband 객체는 ID값과 wife(이 경우 Dizzy)객체를 들고있지만, 다른 값은 모두 default 값이다.
*/
husband = Husband(
name = "Sol",
wife = wife
)
/*
이 시점의 'Sol'객체는 'Dizzy'를 들고있지만, 'Dizzy'는 텅 빈 husband 객체와 연결되어있다.
*/
husbandRepository.save(husband)
//...
}
조금 더 깊이 파고들어가면,
해당 문제의 경우엔,
이와 같은 흐름으로 ‘Sol’ 객체와 다른 레퍼런스가 부여된것으로 이해된다.
내가 적용한 해결법은 다음과 같다.
결국 위의 7번에서, ‘Dizzy’쪽에서 자신의 husband 를 등록하지 않았기때문에 레퍼런스값이 다른것이니 등록해주면 해결이 가능하다.
fun DoSomething() {
wife = Wife(
name = "Dizzy",
).run(wifeRepository::save)
husband = Husband(
name = "Sol",
wife = wife
)
husbandRepository.save(husband)
wife.marrige(husband.id)
//...
}
해결방법은 물론 마음에 들지 않는 방식이라
여전히 조금 더 어메이징 미라클한 방법은 없을까 고민중이지만,
save 를 호출하는 타이밍에 따라서도 결과가 너무 달라지는 JPA 에 배울 것은 많이 남아있다 느끼고 있다.
2025.1.2
문제가 발생하는 이유는 서로가 서로를 들고 있기 때문이다. 한쪽만 들고 있는 형태로 구조를 바꾸는게 현재로서는 가장 현명한 방법으로 보인다.
해당 글을 쓰게 해준, 훨씬 자세하게 하나하나 뜯어봐주셔서 길을 알려주신 이 포스트를 써준 블로거분께 감사를 느끼며 출처를 남긴다.