QueryDsl을 이용해서 한 엔티티를 삭제하는 기능을 구현했다. 삭제하려는 엔티티는 외부에서 참조를 하고있었고, 참조하고있는 엔티티가 삭제되지 않은 상황에서 참조되는 엔티티를 삭제하려 하자 SQLIntegrityConstraintViolationException
예외가 발생하였다.
엔티티에 대해서 좀더 구체적으로 설명하면 퀴즈에 대한 정보를 담은 Quiz
엔티티와 퀴즈의 썸네일 정보를 담은 QuizThumbnail
엔티티간의 관계에서 발생한 문제였다. 하나의 퀴즈당 하나의 썸네일만을 가질수 있다고 설정되어 있었기 때문에 둘간의 연관관계는 @OneToOne
양방향관계로 매핑되어 있었다.
cascade 전략이 설정되어있지 않아 부모의 엔티티가 삭제되었을 때 자식엔티티가 삭제되지 못했기 때문에 발생하는 문제였다고 추측하고 엔티티의 필드에 Cascade 옵션을 추가해주었다. 물론 QuizThumbnail을 먼저 삭제해준 후 Quiz엔티티를 삭제하면 문제를 해결해 줄 수 있다. 하지만 이러한 방법은 추후에 Quiz엔티티를 참조하는 다른 엔티티가 생겼을 때 삭제코드를 추가해주어야한다는 점 때문에 채택하지 않앗다.
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@ToString(of = {"title", "description", "password", "likeCount", "playTime"})
public class Quiz extends CreatedDateEntity {
//...
@OneToOne(mappedBy = "quiz", cascade = CascadeType.ALL, orphanRemoval = true)
private QuizThumbnail thumbnail;
//...
}
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class QuizThumbnail {
//...
@OneToOne
@JoinColumn(name = "quiz_id")
private Quiz quiz;
//...
}
그러나 필드의 cascade 영속성 전이 옵션을 설정해주어도 여전히 QuizThumbnail이 Quiz를 참조하고있기 때문에 Quiz를 삭제할 수 없다는 에러가 발생하였다.
그 원인은 레포지토리에서 커스텀한 DTO로 결과를 바로 반환받고자 했기에 Spring Data JPA를 통해 기본적으로 제공되는 delete 메서드를 사용하지 않고 QueryDsl로 작성한 쿼리, 다르게말하면 JPQL을 사용해서 DELETE 쿼리를 날리는 상황이였다.
JPA에서는 JPQL을 실행하면 바로 쿼리가 날아가기 때문에 cascade 옵션이 적용되지 않고 바로 부모의 DELETE 쿼리가 DB에 전달되었다가 발생한 에러라고 추정된다. JPQL을 사용하지 않고 EntityManager의 remove나 Spring Data JPA를 통해서 접근할 수 있는 delete 메서드를 사용하면 쿼리가 바로 날아가는 것이 아니라 flush되는 시점에 쓰기지연 SQL저장소에 delete 쿼리가 저장되어 문제를 해결할 수 있었다.