프로젝트/크루트

JPA를 쓰는데 Fk 제약조건 때문에 골머리 썩고 있다면 cascade를 사용해서 쉽게쉽게 갑시다

발전생 2022. 3. 10. 23:06

FK 제약조건으로 인해 객체를 삭제할 때는 제거하려는 객체를 참조하고 있는 객체들부터 먼저 제거해줘야 한다.

 

Question 객체를 삭제하고자 하지만 Notification에서 FK로 Question의 id를 참조하고 있는 상황이다. 그래서 Question 객체를 삭제하려고 할 때 연관된 Notification 객체들부터 전부 제거해줘야 했다.

jpa의 orpahnRemoval=true를 이용하고 Question 객체의 필드인 Notification 배열을 clear 해주는 방식으로 구현을 했었다. 그런데 FK 관계가 한두개가 아니다보니 이렇게 하는 건 중간에 실수가 발생하기 쉽겠다는 생각이 들었다. 

@Transactional
public void delete(Question question) {
    // FK 제약 조건 때문에 관련 Notification 먼저 제거 필요
    question.getNotifications().clear();
    questionRepository.delete(question);
}


설계한 엔티티의 일부인데 Project를 FK로 참조하고 있는 테이블이 3개나 된다. 3개 전부 getXXX를 한 뒤 clear() 해주는 방식은 깔끔하지 않다. 언젠가 몇개 clear 하는 걸 깜빡하고 원인을 모른 채 오랜 시간 디버깅을 하게 되는 원인이 될 수 있다. 

 

 

 

JPA의 CascadeType.REMOVE를 이용하면 메소드를 호출할 필요가 없이 연관된 객체까지 깔끔하게 제거가 가능하다. 그러므로 당연히 FK 제약 때문에 제거를 못 할 일은 없다. 물론, 이 방법은 연관된 객체까지 레코드에서 전부 제거하는 걸 원할 때만 유효하다.

 

 

CascadeType.REMOVE는 이해가 어려운 개념이었다.

지금 이해한 바로는 아래와 같은 코드일 때 Question 타입의 객체가 제거되면 children들(즉, 제거될 객체를 parent로 참조하고 있는 객체들)을 모두 제거한다고 이해했다.

몇 번의 Question 타입의 객체를 제거하는 시도의 결과 이해한 바가 맞다는 확신이 어느 정도 들고 있다.

@Entity
public class Question {
    ...

    @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
    private List<Question> children = new ArrayList<>();
}

 

일반화 시킨 예시를 보면 이해가 더 잘 될 거라 생각한다.

cascade = CascadeType.REMOVE  때문에 A 타입의 객체를 제거하면 해당 객체를 FK로 참조하고 있던 B 타입의 객체들 역시 모두 제거된다. JPA가 FK 제약에 안 걸리게 순서대로 레코드들을 테이블에서 잘 제거해준다.

@Entity
public class A {
    ...

    @OneToMany(mappedBy = "parent", cascade = CascadeType.REMOVE)
    private List<B> children = new ArrayList<>();
}

 

이제 풀고자 했던 문제로 돌아가보면, Question 테이블에서 레코드를 하나 제거하려 할 때에 해당 레코드를 FK로 참조하고 있던 레코드들 역시 Notification 테이블에서 모두 제거돼야 한다. 순서가 중요하다. Notification 테이블에서 먼저 제거하고나서 Question 테이블에서 제거해야 한다.  그래야 FK 제약조건에 위배되지 않는다. 

 

cascade = CascadeType.ALL은 cascade = CascadeType.REMOVE 를 포함한다.

그러므로 아래와 같이 cascade를 사용해주면 직접 getNotifications().clear()를 해줄 필요가 없다.

굉장히 코드가 깔끔해진다.

 

결론은 cascade 애용합시다.