스프링에 대해서 여러가지 공부를 하던중, 정리 해놓으면 좋을 것 같은 주제를 정리하려고 한다.
이번에 다를 주제는 @OnDelete(action = OnDeleteAction.CASCADE)이다. 해당 주제를 공부하게 된 이유는 어떠한 의문에서 부터 시작했다. Cascade 설정을 해주고 싶은데 casecade=CascadeType.REMOVE를 사용하려면 양방향 매핑이 강제적이다. 하지만 불필요한 양방향 매핑은 오류의 가능성을 야기하기 때문에 해줄 이유가 없다고 생각한다. 그래서 해당 문제를 해결하기 위해 공부하였고 @OnDelete(action = OnDeleteAction.CASCADE)라는 것을 발견하였다. 해당 내용에 대해 공부하였고, 흥미러운 주제가 생겨 공유하고 나의 생각을 정리할 것 이다.
해당 내용은 다른 게시글에 정말 잘 정리 되어있다. 해당 게시글은 간단하게만 설명하고 넘어가겠다.
앞에 내용만을 봤을때는 @OnDelete(action = OnDeleteAction.CASCADE)은 정말 좋은 기능인 것 같다. 하지만 문제점이 있다.
반박할 여지가 없다..
앞에 내용만으로는 게시글을 작성해서 정리해아한다는 생각을 하지못하였다. 하지만 해당 문제를 직면하고서 기록하고자 마음먹었다. 발생한 문제는 아래와 같다.
@Entity
@Getter
@NoArgsConstructor
public class Board extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "board_id")
private Long id;
@Column(nullable = false)
private String title;
private String content;
@Column(nullable = false)
private int viewCount;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "memeber_id")
@OnDelete(action = OnDeleteAction.CASCADE)
public Member member;
@OneToMany(mappedBy = "board", cascade = CascadeType.REMOVE, fetch = FetchType.LAZY)
public List<FileEntity> files = new ArrayList<>();
@Builder
public Board(Long id, String title, String content, Member member) {
this.id = id;
this.title = title;
this.content = content;
this.viewCount = 0;
this.member = member;
}
public void upViewCount() {
this.viewCount++;
}
public void update(String title, String content) {
this.title = title;
this.content = content;
}
}
게시글 Entity이다. 코드에서 볼 수 있듯이, 부모로 member를 가지고 자식으로 file을 가진다. member에 대해서는 @OnDelete(action = OnDeleteAction.CASCADE) file에 대해서는 cascade = CascadeType.REMOVE를 해준 모습이다.
@Entity
@Table(name = "file")
@Getter
@NoArgsConstructor
public class FileEntity extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "file_id")
private Long id;
@Column(nullable = false)
private String originFileName;
@Column(nullable = false)
private String fileType;
@Column(nullable = false)
private String filePath;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "board_id")
public Board board;
@Builder
public FileEntity(Long id, String originFileName, String filePath, String fileType) {
this.id = id;
this.originFileName = originFileName;
this.filePath = filePath;
this.fileType = fileType;
}
public void setBoard(Board board) {
this.board = board;
board.getFiles().add(this);
}
public void delBoard(Board board) {
this.board = null;
board.getFiles().remove(this);
}
}
파일 Entity이다. 코드에서 볼 수 있듯이, 부모로 board 를 가지고 있다.
겉보기에는 문제가 없어 보인다. 하지만 문제는 board를 삭제했을 때가 아닌 member를 삭제했을 때 발생한다. 바로 member를 삭제하였을 경우 board에 대한 자식은 file을 삭제한 뒤, board를 삭제해야하는데 바로 board를 삭제하려고 해서 board의 자식이 있다는 오류가 발생하는 것이다. 하지만 @OnDelete(action = OnDeleteAction.CASCADE)로 설정할 경우 제대로 삭제하는 모습을 볼 수 있었다. 해당 오류에 대해서 여러가지 검색을 해보았지만 자료가 없었다. (당연하게도 두개 다 사용하는 바보는 없을 것이기 때문이다.) 그래서 나의 생각을 아래에 정리해서 기록해두고, 나중에 더 성장해서 다시 보려고 한다.
board와 file은 서로 다른 테이블에 위치한다.
CascadeType.REMOVE는 JPA 레벨에서 동작하지만, 데이터베이스 무결성 제약 조건 때문에 board가 삭제되었을 때 연관된 file은 자동으로 삭제되지 않는다.
하지만 @OnDelete(action = OnDeleteAction.CASCADE)는 데이터베이스 레벨의 옵션으로, 외래 키 제약 조건을 사용하여 board가 삭제될 때 연관된 file들을 데이터베이스에서 직접 삭제합니다. 이는 JPA가 아니라 데이터베이스가 처리하는 작업이기 때문이다.
사실 이렇게 생각하는 근거는 하나 더 있다. 대댓글에 해당 내용을 적용해 보았는데 대댓글은 CascadeType.REMOVE 만을 이용해서도 잘 삭제가 되는 모습을 볼 수 있었다. 해당 내용도 정리하자면 아래와 같다.
Comment 테이블 내에서 부모-자식 관계가 설정되어 있다.
CascadeType.REMOVE 또는 orphanRemoval = true는 JPA 레벨에서 동작하며, 부모 Comment가 삭제되면 연관된 자식 Comment들도 같은 테이블 내에서 삭제된다. 즉, 같은 데이터 베이스에 있기 때문에 잘 삭제가 되는 것이다.
사실 해당 내용의 결론은 정해져있다. cascade = CascadeType.REMOVE와 @OnDelete(action = OnDeleteAction.CASCADE)의 둘중 하나만 사용하라 인 것 같다. 하나만 사용해도 조심하여 사용하여야 하는데 둘다 적용하려고 하니, 이상한 오류가 발생하는 것이다!
해당 내용을 정리하면서 결론이 뭐지라고 계속 생각해보았는데, 일단 cascade = CascadeType.REMOVE vs @OnDelete(action = OnDeleteAction.CASCADE) 중에서는 하나만 선택하여 사용하는 것이 맞는 것 같다. 둘중 하나만 사용해야한다면 무엇을 선택할까는 정말 어려운 고민이다. 두개의 장단점이 명확하다. 얼마나 행복한 고민인가... 계속해서 고민을 이어가던중 결론에 도달하였다. 나의 생각은 아래와 같다.
갑자기 이상한 결론인데, 이게 정답인 것 같다. 정말 확실한 부모, 자식 관계에서만 Casecade를 사용하고 확실하지 않은 경우에는 사용하지 않는게 맞는 것 같다. (근데 Casecade는 정말 편하다...)