반응형

문제 상황

  • Jpa로 값을 삭제하려 했지만 SQLIntegrityConstraintViolationException 에러가 발생
  • DB에서 확인해봐도 값이 정상적으로 삭제되지 않았음

원인

  • 연관관계 매핑이 되어있는 상황에서는 부모 객체 삭제 시 자식 객체에서 부모 객체를 참조하고 있으면 에러가 발생하고 값이 삭제되지 않음
    • Parent와 Child가 1:N 관계를 맺고 있는 상황이고, Child가 연관관계 주인이라면 Child는 쉽게 삭제가 가능하지만, Parent를 삭제하려면 따로 처리가 필요함
    • [Spring Boot] 연관관계 매핑 참고

해결 방법

  1. Cascade 사용
  2. OrphanRemoval 사용
  3. Soft Delete(논리 삭제) 방식 사용
  • 각각의 방법을 예제를 통해 정리

예제 설명

  • Child, Parent, GrandParent 객체 존재
  • Child는 Parent를 한 명만 가질 수 있지만, Parent는 Child를 여러 명 가질 수 있음
    • Child : Parent = 1 : N 관계
  • Parent는 GrandParent를 한 명만 가질 수 있지만, GrandParent는 Parent를 여러 명 가질 수 있음
    • Parent : GrandParent = 1 : N 관계
@Entity
public class Child {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
    private int age;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "parent_id")
    private Parent parent;
}

@Entity
public class Parent {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
    private int age;

    @OneToMany(mappedBy = "parent")
    List<Child> children = new ArrayList<>();

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "grand_parent_id")
    private GrandParent grandParent;
}

@Entity
public class GrandParent {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
    private int age;

    @OneToMany(mappedBy = "grandParent")
    List<Parent> parents = new ArrayList<>();
}

Cascade (영속성 전이)

Cascade란?

  • 부모 Entity를 영속 상태로 만들 때 자식 Entity도 함께 영속 상태로 만들고 싶은 경우에 사용
    • ex) 부모 Entity 삭제 시 자식 Entity도 삭제
  • @ManyToOne, @OneToMany와 같은 어노테이션에 사용 가능

Cascade 종류

  • ALL : 모든 Cascade 적용
  • PERSIST : Entity를 영속화 할 때, 연관된 Entity도 함께 영속화
  • REMOVE : Entity를 제거할 때, 연관된 Entity도 삭제
  • MERGE : Entity를 병합할 때, 연관된 Entity도 병합
  • REFRESH : Entity를 새로고침 할 때, 연관된 Entity도 새로고침
  • DETACH : Entity를 detach 할 때, 연관된 Entity도 detach

CascadeType.REMOVE 사용 방법

  • 위 예제에서는 GrandParent, Parent가 부모 객체
  • 아래와 같이 GrandParent-Parent의 관계, Parent-Child의 관계에서 부모쪽에 CascadeType.REMOVE를 적용하면 부모 객체도 삭제가 가능해지고, 부모 객체 삭제 시 모든 자식 객체 삭제
  • 만약 Parent-Child의 관계에만 적용한다면 Parent는 삭제가 가능하지만, Grand-Parent를 삭제하면 에러 발생
  • 만약 GrandParent-Parent의 관계에만 적용한다면 GrandParent, Parent 삭제 시 에러 발생
    • Parent가 삭제되는 경우에 에러가 발생하기 때문에, GrandParent도 삭제할 수 없음
@Entity
public class GrandParent {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
    private int age;

    @OneToMany(mappedBy = "grandParent", cascade = CascadeType.REMOVE)
    List<Parent> parents = new ArrayList<>();

}

@Entity
public class Parent {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
    private int age;

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

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "grand_parent_id")
    private GrandParent grandParent;
}

OrphanRemoval

  • orpahnRemoval이란 고아 객체(Orphan)을 제거한다는 뜻으로, 부모 Entity와의 연관관계가 끊어진 자식 Entity를 자동으로 삭제하는 기능

orphanRemoval 사용 방법

  • cascade와 마찬가지로 GrandParent와 Parent Class를 아래와 같이 수정하면 삭제가 정상적으로 가능해짐
  • 만약 Parent-Child의 관계에만 적용한다면 Parent는 삭제가 가능하지만, Grand-Parent를 삭제하면 에러 발생
  • 만약 GrandParent-Parent의 관계에만 적용한다면 GrandParent, Parent 삭제 시 에러 발생
    • Parent가 삭제되는 경우에 에러가 발생하기 때문에, GrandParent도 삭제할 수 없음
@Entity
public class GrandParent {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
    private int age;

    @OneToMany(mappedBy = "grandParent", cascade = CascadeType.REMOVE)
    List<Parent> parents = new ArrayList<>();

}

@Entity
public class Parent {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
    private int age;

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

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "grand_parent_id")
    private GrandParent grandParent;
}

CascadeType.REMOVE, OrphanRemoval 차이

  • OrphanRemoval은 부모-자식 관계가 끊어지면 자식을 삭제
  • CascadeType.REMOVE는 부모-자식 관계가 끊어져도 자식을 삭제하지 않음
  • 이를 테스트해보기 위해서 CascadeType.REMOVE, OrphanRemoval과 CascadeType.PERSIST를 같이 사용해서 테스트 진행
  • 자식을 삭제하지 않고 아래와 같이 부모-자식 관계만 제거
@DeleteMapping("/child/{childId}")
public String deleteChild(@PathVariable Long childId) {
    Child child = childRepository.findById(childId).get();
    Parent parent = child.getParent();

    parent.getChildren().remove(child);
    child.setParent(null);

    childRepository.save(child);
    parentRepository.save(parent);

    return childId + "번 자식 삭제 완료";
}
  • Parent Entity에서 아래와 같이 orphanRemoval을 적용한 경우에는 자식이 삭제됨
@OneToMany(mappedBy = "parent", orphanRemoval = true, cascade = CascadeType.PERSIST)
List<Child> children = new ArrayList<>();

  • 하지만 Parent Entity에서 아래와 같이 CascadeType.REMOVE를 적용한 경우에는 자식이 삭제되지 않음
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
List<Child> children = new ArrayList<>();

Soft Delete

  • 위에서 설명한 방법들은 부모 객체를 삭제하면 자식 객체들을 삭제함
  • Soft Delete(논리 삭제)를 하면 부모 객체 삭제 시 부모 객체가 실제로는 지워지지 않고, 지워졌다고 표시를 해둔다고 생각하면 됨 + 자식 객체는 삭제되지 않음
    • 만약 Cascade나 OrphanRemoval을 같이 사용한다면 삭제하게 할 수 있음
  • Soft Delete를 한다면, 데이터가 실제 DB에는 남아있어 복구하기 쉽다는 장점이 있지만, 데이터가 남아있기 때문에 용량에 관한 문제가 발생할 수 있음

Soft Delete 사용 방법

  • 만약 Parent 객체에만 Soft Delete를 적용한다고 한다면 아래와 같이 Parent 클래스만 수정
@Entity
@SQLDelete(sql = "UPDATE parent SET deleted_at = current_timestamp WHERE id = ?")
@Where(clause = "deleted_at is null")
public class Parent {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
    private int age;

    private LocalDateTime deletedAt;

    @OneToMany(mappedBy = "parent")
    List<Child> children = new ArrayList<>();

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "grand_parent_id")
    private GrandParent grandParent;
}
  • 위와 같이 수정 후 parent를 삭제한다면(deleteById) 아래와 같이 데이터는 남아있지만 deletedAt이 채워짐

  • 이 상황에서는 Parent의 삭제는 정상적으로 가능하지만, Child, GrandParent에서 Parent를 참조하거나, id가 2인 Parent를 조회하면 에러가 발생함
반응형

↓ 클릭시 이동

복사했습니다!