티스토리 뷰
Cascade란
Cascade란 영속성 전이를 의미한다. 엔티티A에 작업한 내용이 이를 참조하는 엔티티B까지 영향이 전이된다.
Cascade에는 여러가지 옵션이 존재한다.
- ALL // 모두 적용
- PERSIST // 영속
- REMOVE // 삭제
- MERGE // 병합
- REFRESH
- DETACH
위 옵션 중에서 REMOVE에 대해서 알아보겠다. Cascade.REMOVE는 엔티티A를 삭제하면 이를 참조하는 엔티티B도 같이 삭제된다.
ex. 부모 엔티티를 삭제하면 모든 자식 엔티티도 같이 삭제된다.
이부분은 사실 직접 경험하지 않는 이상 감이 와닿지 않는다. 나 또한 cascade란 옵션이 있구나? 에서 그쳤었는데, 이번에 무결성 문제가 발생하면서 cascade를 사용해야겠다고 다짐했다.
Cascade.REMOVE 사용 X
영속성 전이를 사용하지 않았을 때 발생하는 무결성 문제를 알아보자.
설명을 위해 User, Team, Likes 엔티티를 사용하겠다. Likes 엔티티는 게시물의 좋아요 기록을 저장한다.
User.class
먼저 User엔티티는 여러 Team에 속할 수 있다. UserTeam 엔티티를 사용하여 User와 Team이 @ManyToMany를 사용하지 않고 @OneToMany + @ManyToOne 관계로 풀어서 사용했다.
아래 필드 중에 teams와 likes를 기억하고 있자! 두 필드 때문에 무결성 문제가 발생한다.
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class User extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "user_id")
private Long id;
private String email; // 사용자 이메일
private String password; // 사용자 비밀번호
private String username; // 사용자 이름
private String phone; // 전화번호
@OneToMany(mappedBy = "user")
private List<UserTeam> teams = new ArrayList<>();
@OneToMany(mappedBy = "user")
private List<Bookmark> bookmarks = new ArrayList<>();
@OneToMany(mappedBy = "user")
private List<Likes> likes = new ArrayList<>();
@Builder
public User(String email, String password, String username, String phone) {
this.email = email;
this.password = password;
this.username = username;
this.phone = phone;
}
}
UserTeam.class
User-Team이 @ManyToMany를 사용하지 않도록 하기 위해 만든 중간 엔티티이다.
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class UserTeam {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "team_id")
private Team team;
@Builder
public UserTeam(User user, Team team) {
this.user = user;
this.team = team;
}
}
Likes.class
다음으로 Likes엔티티는 좋아요를 관리하는 엔티티이다. 좋아요를 누른 user와 section(게시물)을 필드로 갖는다.
@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Likes extends BaseEntity{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "like_id")
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "section_id")
private Section section;
@Builder
public Likes(User user, Section section) {
this.user = user;
this.section = section;
}
}
위 코드를 기반으로 무결성 문제를 설명하겠다. 웹사이트를 사용하던 User가 회원탈퇴를 하게 되었다. User의 기록을 지우기 위해서 기존에 속해있던 Team에서도 제외가 되어야 하고, 좋아요 기록 또한 지워져야 한다.
Id로 2를 갖는 User를 지우려고 하니 foreign key constraint 에러가 뜬다. 이것을 무결성 문제라고 한다. 자세히 읽어보면 user_team과 문제가 있음을 알 수 있다. user_team 테이블에 value 값으로 2를 갖는 user_id 필드(외래키)가 있기 때문에 해당 row를 먼저 제거하고 User를 삭제할 수 있다는 에러이다.
Cascade.REMOVE 사용 O
Id가 2인 User를 삭제할 수 없었던 이유는 해당 id를 참조하고 있는 user_team 테이블의 row가 존재하기 때문이다. JPA에서는 이러한 문제를 해결하기 위해 Cascasde 옵션을 제공한다.
User.class
@OneToMany를 보면 cascade = CascadeType.REMOVE 옵션이 추가되었다. 이 의미는 User 엔티티를 제거할 때, 해당 User 엔티티를 참조하는 UserTeam 엔티티도 같이 제거한다는 의미이다. 순서는 다음과 같다.
- DELETE FROM User WHERE user_id = 2; 호출
- Id가 2인 User를 삭제하기 전에, Cascade 옵션이 걸린 테이블(user_team)을 먼저 확인한다.
- user_team 테이블에서 Id가 2인 User를 참조(외래키)하고 있는지 확인한다.
- Id가 2인 User를 참조하고 있다면 해당 row를 제거한다.
- 2번을 먼저 수행하고 User를 제거한다.
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class User extends BaseEntity {
.. 생략
@OneToMany(mappedBy = "user", cascade = CascadeType.REMOVE)
private List<UserTeam> teams = new ArrayList<>();
}
다시 Id가 2인 User를 삭제하는 쿼리문을 발생시키니 또 에러가 발생한다. 이번에는 likes 테이블에서 foreign key constraint 문제가 발생했다.
User.class를 보면 likes 필드의 @OneToMany 옵션에 CascadeType.REMOVE 옵션이 없기에 발생하는 문제이다. teams 필드에 사용한 것처럼 다음과 같이 수정한다.
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class User extends BaseEntity {
.. 생략
@OneToMany(mappedBy = "user", cascade = CascadeType.REMOVE)
private List<Likes> likes = new ArrayList<>();
}
teams와 likes 필드에 CascadeType.REMOVE를 추가한 후에 Id가 2인 User가 정상적으로 삭제된다.
Cascade 옵션은 언제 사용할까?
- Cascade를 사용하는, Cascade가 적용되는 두 엔티티의 life-cycle이 거의 일치해야 한다.
- Cascade 되는 엔티티가 Cascade를 설정하는 엔티티에서만 사용해야 한다.
좋아요 개수의 경우 게시물에서만 사용되지 다른 곳에서 사용되지 않는다. 다른 말로, 게시물이 존재하기 때문에 좋아요 기능 또한 존재한다. 즉, 게시물이 삭제되면 좋아요 기록 또한 같이 삭제되어야 한다.
Cascade와 @OneToMany
결론부터 말하자면 @OneToMany에만 Cascade가 사용 가능하다. 이 말은 @OneToMany를 사용하는 필드에 모두 Cascade를 사용하라는 의미가 아닌다. 일단 Cascade를 쓰기 위해서는 최소한의 조건으로 @OneToMany이어야 한다는 의미이다.
@OneToMany에만 사용해야 하는 이유를 쉽게 생각하자.
자식 엔티티에는 좋아요를 누른 사용자가 있다. 여기서 게시물이 삭제되면 해당 게시물과 관련된 좋아요 기록은 필요가 없어진다. 따라서 부모가 삭제되었을 때, 부모를 참조하는 자식들의 연쇄적인 삭제가 발생해야 한다.
부모(게시물 관리) {
@OneToMany(CascadeType.REMOVE)
List<자식>
}
자식(게시물의 좋아요 관리) {
@ManyToOne
부모
}
참고
https://hongchangsub.com/jpa-cascade-2/