[생각정리] @OnDelete(action = OnDeleteAction.CASCADE)

jeyong·2024년 1월 24일
1

공부 / 생각 정리  

목록 보기
10/121


스프링에 대해서 여러가지 공부를 하던중, 정리 해놓으면 좋을 것 같은 주제를 정리하려고 한다.
이번에 다를 주제는 @OnDelete(action = OnDeleteAction.CASCADE)이다. 해당 주제를 공부하게 된 이유는 어떠한 의문에서 부터 시작했다. Cascade 설정을 해주고 싶은데 casecade=CascadeType.REMOVE를 사용하려면 양방향 매핑이 강제적이다. 하지만 불필요한 양방향 매핑은 오류의 가능성을 야기하기 때문에 해줄 이유가 없다고 생각한다. 그래서 해당 문제를 해결하기 위해 공부하였고 @OnDelete(action = OnDeleteAction.CASCADE)라는 것을 발견하였다. 해당 내용에 대해 공부하였고, 흥미러운 주제가 생겨 공유하고 나의 생각을 정리할 것 이다.

1. cascade = CascadeType.REMOVE vs @OnDelete(action = OnDeleteAction.CASCADE)

해당 내용은 다른 게시글에 정말 잘 정리 되어있다. 해당 게시글은 간단하게만 설명하고 넘어가겠다.

1-1. casecade=CascadeType.REMOVE

  • JPA에 의해 외래 키를 찾아가며 참조하는 레코드를 제거해주는 기능으로 JPA 상에서는 참조하고 있는 레코드의 개수만큼 delete 쿼리가 생성된다.
  • 부모 테이블의 자식 컬럼에 작성해야하므로 양방향 매핑이 아닌 이상 적용하기 어렵다.

1-2. @OnDelete(action = OnDeleteAction.CASCADE)

  • DB 자체에서 on delete cascade 제약조건이 걸려 이를 통해 참조하는 레코드가 모두 제거되는 기능으로
    JPA 상에서는 한 개의 delete 쿼리가 생성된다.
  • 단방향 매핑에서도 적용이 가능하다.

2. @OnDelete(action = OnDeleteAction.CASCADE)의 문제점

앞에 내용만을 봤을때는 @OnDelete(action = OnDeleteAction.CASCADE)은 정말 좋은 기능인 것 같다. 하지만 문제점이 있다.

  • JPA에서 기본 제공하는 cascade 기능들이 왜 자식 객체들을 하나하나 삭제할까를 생각해보면, 위와 같이 DB단계에서 직접적으로 관여하는 것은 DB의 무결성을 훼손할 가능성이 생긴다.
  • 즉, ondelete 설정으로 인해 DB에 있는 테이블의 참조 부분까지 모조리 삭제 할 가능성이 생긴다는 것이다.
  • 위와 같은 형태는 즉 설계자가 삭제의 모든 과정을 신경쓰고 관여해야 한다는 것이고, 제어 관계의 역전(IoC) / 객체 지향의 다형성 등의 원리에 부합되지 않는다

    OndeleteCascade vs deleteAllInBatch - 어느것을 택해야 할까

반박할 여지가 없다..

3. 발생한 문제점

앞에 내용만으로는 게시글을 작성해서 정리해아한다는 생각을 하지못하였다. 하지만 해당 문제를 직면하고서 기록하고자 마음먹었다. 발생한 문제는 아래와 같다.

Board.java

@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를 해준 모습이다.

FileEntity.java

@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

board와 file은 서로 다른 테이블에 위치한다.
CascadeType.REMOVE는 JPA 레벨에서 동작하지만, 데이터베이스 무결성 제약 조건 때문에 board가 삭제되었을 때 연관된 file은 자동으로 삭제되지 않는다.
하지만 @OnDelete(action = OnDeleteAction.CASCADE)는 데이터베이스 레벨의 옵션으로, 외래 키 제약 조건을 사용하여 board가 삭제될 때 연관된 file들을 데이터베이스에서 직접 삭제합니다. 이는 JPA가 아니라 데이터베이스가 처리하는 작업이기 때문이다.

사실 이렇게 생각하는 근거는 하나 더 있다. 대댓글에 해당 내용을 적용해 보았는데 대댓글은 CascadeType.REMOVE 만을 이용해서도 잘 삭제가 되는 모습을 볼 수 있었다. 해당 내용도 정리하자면 아래와 같다.

Comment와 자식 Comment

Comment 테이블 내에서 부모-자식 관계가 설정되어 있다.
CascadeType.REMOVE 또는 orphanRemoval = true는 JPA 레벨에서 동작하며, 부모 Comment가 삭제되면 연관된 자식 Comment들도 같은 테이블 내에서 삭제된다. 즉, 같은 데이터 베이스에 있기 때문에 잘 삭제가 되는 것이다.

사실 해당 내용의 결론은 정해져있다. cascade = CascadeType.REMOVE와 @OnDelete(action = OnDeleteAction.CASCADE)의 둘중 하나만 사용하라 인 것 같다. 하나만 사용해도 조심하여 사용하여야 하는데 둘다 적용하려고 하니, 이상한 오류가 발생하는 것이다!

4. 마무리

해당 내용을 정리하면서 결론이 뭐지라고 계속 생각해보았는데, 일단 cascade = CascadeType.REMOVE vs @OnDelete(action = OnDeleteAction.CASCADE) 중에서는 하나만 선택하여 사용하는 것이 맞는 것 같다. 둘중 하나만 사용해야한다면 무엇을 선택할까는 정말 어려운 고민이다. 두개의 장단점이 명확하다. 얼마나 행복한 고민인가... 계속해서 고민을 이어가던중 결론에 도달하였다. 나의 생각은 아래와 같다.

애매하면 Casecade를 사용하지마!

갑자기 이상한 결론인데, 이게 정답인 것 같다. 정말 확실한 부모, 자식 관계에서만 Casecade를 사용하고 확실하지 않은 경우에는 사용하지 않는게 맞는 것 같다. (근데 Casecade는 정말 편하다...)

profile
노를 젓다 보면 언젠가는 물이 들어오겠지.

0개의 댓글