CasCade를 적용/학습하며 작성한 글입니다. 학습 과정에서 작성된 글이기 때문에 잘못된 내용이 있을 수 있으며, 이에 대한 지적이나 피드백은 언제든 환영입니다.
1. CasCade
Cascade는 데이터베이스 외래 키에 대한 작업을 수행할 때 사용할 수 있는 옵션으로, 외래 키 값에 대한 업데이트나 삭제 작업이 발생했을 때 관련된 다른 테이블의 행에도 해당 작업을 자동으로 전파시키는 기능을 말합니다.
JPA에서도 비슷한데요, CasCade 옵션을 통해 엔티티가 변경될 때 상태 변화를 전파 시킬 수 있습니다. 엔티티의 상태는 크게 네 가지가 존재하며 아래와 같습니다. 이는 참조 무결성과도 연관이 있는데 CasCade를 사용한다고 참조 무결성이 반드시 지켜지지는 않지만 CasCade는 참조 무결성을 지키기 위한 한 가지 방법으로 사용될 수 있습니다.
Transient: 객체를 생성하거나 값을 부여해도 JPA/Hibernate가 객체에 대해 아무것도 모르는 상태.
Persistent: 엔티티를 저장한 순간 JPA가 이를 감지하는 상태. 이때 주의해야 할 점은 save 메서드를 호출 했다고 해서 곧 바로 DB에 저장되지는 않습니다. JPA가 영속 상태로 관리하고 있다가 후에 데이터를 저장합니다. Write Behind
Detached: JPA가 더이상 관리하지 않는 상태. JPA가 제공해주는 기능들을 사용하려면 다시 영속 상태로 만들어야 합니다.
Removed: JPA가 관리하는 상태이긴 하지만, 실제 커밋이 일어날 때 삭제되는 상태.
이는 엔티티 라이프 사이클(Life Cycle) 이라고 합니다.
CasCade 옵션은 아래와 같이 사용할 수 있는데요, CascadeType은 Enum으로 제공되며 아래와 같은 설정이 있습니다. 사용법에 대해서는 관련된 설명/자료가 많으므로 헷갈리는 상황에 대해서만 살펴보겠습니다.
1
2
3
4
5
6
7
@Entity
class City(
......
) {
@OneToMany(mappedBy = "city", cascade = [CascadeType.PERSIST, CascadeType.REMOVE], orphanRemoval = true)
private var _districts: MutableList<District> = ArrayList()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public enum CascadeType {
/** Cascade all operations */
ALL,
/** Cascade persist operation */
PERSIST,
/**
* - 엔티티 상태를 병합 할 때, 연관된 엔티티도 함께 병합
* - 트랜잭션이 종료되고 DETACHED 상태에서 엔티티가 merge( )를 수행하게 되면 연관 엔티티의 추가 및 수정사항도 함께 적용
*/
MERGE,
/**
* 엔티티를 삭제할 때, 연관된 엔티도 함께 삭제
*/
REMOVE,
/**
* 상위 엔티티를 REFRESH 할 때, 연관된 엔티티도 모두 REFRESH
*/
REFRESH,
/**
* 부모 엔티티가 detach()를 수행하게 되면, 연관된 엔티티도 DETACHED 상태가 되어 변경사항이 반영 X
*/
DETACH
}
2. CasCadeType.REMOVE vs orphanRemoval=true
결론부터 말하면 CascadeType.REMOVE와 orphanRemoval = true는 부모 엔티티를 삭제하면 자식 엔티티도 함께 삭제합니다. 하지만 부모 엔티티에서 자식 엔티티 제거할 때 CascadeType.REMOVE는 자식 엔티티가 그대로 남아있는 반면, orphanRemoval = true는 자식 엔티티를 제거합니다. 테스트를 통해 이를 알아보겠습니다.
우선 팀(Team)과 회원(Member)는 일대 다의 관계입니다. 회원 쪽에는 CasCadeType이 REMOVE로 지정되어 있고 orphanRemoval에 대한 설정은 없습니다.
1
2
3
4
5
6
7
8
9
@Entity
class Member(
......
) {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "team_id")
private var team: Team? = null
......
1
2
3
4
5
6
7
8
@Entity
class Team(
......
) {
@OneToMany(mappedBy = "team", cascade = [CascadeType.PERSIST, CascadeType.REMOVE])
private var _members: MutableList<Member> = ArrayList()
......
다음은 도시(City)와 구역(District)의 관계입니다. 마찬가지로 일대 다의 관계입니다.
1
2
3
4
5
6
7
8
@Entity
class City(
......
) {
@OneToMany(mappedBy = "city", cascade = [CascadeType.PERSIST, CascadeType.REMOVE], orphanRemoval = true)
private var _districts: MutableList<District> = ArrayList()
......
1
2
3
4
5
6
7
8
9
@Entity
class District(
......
) {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "city_id")
private var city: City? = null
......
먼저 단순 CasCadeType.REMOVE를 테스트 해보겠습니다. 팀에서 회원을 등록하고 영속화한 후 다시 회원들을 제거합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class MemberCascadeTest {
......
@Test
@DisplayName("orphanRemoval 옵션 없이 CascadeType만 Remove라면 연관관계를 끊어도 자식이 삭제되지 않는다.")
fun cascade_remove_test() {
val messi = Member(null, "Messi")
val xavi = Member(null, "Xavi")
val team = Team(null, "FC Barcelona")
team.register(mutableListOf(messi, xavi))
teamJpaRepository.save(team)
// 모두 제거해도 남아있음
team.removeAll()
// true
assertEquals(2, memberJpaRepository.findAll().size)
}
}
결과를 보면 여전히 두 명인 것을 알 수 있는데요, 즉 연관관계를 제거해도 이를 삭제하지 않습니다.
반면 orphanRemoval = true 옵션을 준 경우는 진짜로 자식이 삭제됩니다. 정리해 보면 양쪽 모두 부모가 삭제될 때 함께 삭제되는 것은 맞지만 orphanRemoval 옵션을 주지 않는다면 단순히 연관관계를 끊었다고 해서 자식 엔티티가 삭제되지는 않는다. 입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class CityCascadeTest {
......
@Test
@DisplayName("orphanRemoval=true 라면 부모의 객체 라이프사이클을 따르게 된다.")
fun orphan_removal_life_cycle_test() {
val city = City(null, "서울")
val gangseo = District(null, "강서구")
val hwagok = District(null, "화곡")
city.register(mutableListOf(gangseo, hwagok))
cityRepository.save(city)
// 자식 하나 제거
city.remove(hwagok)
cityRepository.save(city)
// true
assertEquals(1, cityRepository.findAll().size)
assertEquals(1, districtRepository.findAll().size)
}
}
3. 정리
CasCade는 한 엔티티의 상태를 전이할 때 사용합니다. 이는 참조 무결성을 지키기 위한 한 가지 방법으로 사용할 수 있습니다. CascadeType.REMOVE와 orphanRemoval = true는 부모 엔티티를 삭제하면 자식 엔티티도 함께 삭제합니다. 하지만 부모 엔티티에서 자식 엔티티 제거할 때 CascadeType.REMOVE는 자식 엔티티가 그대로 남아있는 반면, orphanRemoval = true는 자식 엔티티를 제거합니다.