[회고록] Spring Boot에서 JPA의 Soft Delete와 Cascade 연관관계

2024. 12. 9. 01:49·Spring/프로젝트 리팩토링 회고록
728x90
반응형
SMALL

내가 사용한 SQL Delete방식에서 Cascade속성을 함께 사용하려 했을 때 발생했던 아래의 문제에 대해 다시는 발생하지 않도록 개념을 다잡기 위해 정리한다! 이전에는 Cascade를 왜 사용하는지 잘 몰랐으나 SoftDelete와 함께 무턱대고 사용하려고 하니 충돌이 나서 각각의 방식과 전략을 확실히하고 어떤 방법으로 SoftDelete를 사용하며 Cascade는 왜 피해야 하는지 알아보자.

[발생한 예외]

org.hibernate.ObjectDeletedException: deleted instance passed to merge

[Soft Delete의 적합성]

Soft Delete는 데이터베이스에서 데이터를 물리적으로 삭제하지 않고, "삭제됨" 상태를 표시하기 위해 추가적인 컬럼(e.g., deleted 또는 deletedAt)을 활용하는 방법이다. 이를 통해 다음과 같은 장점을 얻을 수 있다

  • 삭제된 데이터를 복구 가능
  • 데이터 삭제 이력 추적
  • 삭제된 데이터를 제외한 결과만 조회 가능

하지만 완벽한 전략은 아니기에 단점 또한 존재한다.

  • 삭제된 데이터를 제외하려면 추가적인 필터링 조건이 필요하여 쿼리 성능이 저하된다.
  • 삭제된 데이터를 복구하거나 잘못된 상태로 관리할 경우 데이터의 정합성이 깨질 가능성이 있다.
  • 물리적으로 삭제하지 않기 때문에 데이터베이스 용량이 불필요하게 증가할 수 있다.

[Soft Delete 구현]

1. 엔티티에 Soft Delete 필드 추가

Soft Delete를 구현하기 위해 엔티티에 @Column 어노테이션과 함께 deleted 또는 deletedAt 필드를 추가한다.

2. Soft Delete 필드를 활용한 삭제 메서드

JPA에서 데이터를 삭제하는 대신, 해당 필드 값을 변경하는 커스텀 메서드를 정의한다.

3. @Where 어노테이션으로 삭제된 데이터 제외

Hibernate에서 제공하는 @Where 어노테이션을 활용하면, 삭제된 데이터를 자동으로 제외한 결과를 반환할 수 있다.

내 코드에는 SoftDelete방식을 구현하였지만 Cascade의 Type을 ALL로 하였으며 orphanRemoval까지 적용되었다.

이 부분은 JPA의 속성들에 대해 무지했고 이제서야 수정을 한다는게 부끄러운 일이지만 하나하나 알아가며 수정하는 재미도 있기에 반성과 뿌듯함을 함께 느끼는 중이다...

@Entity(name = "users")
@Getter
@Setter
@Builder(toBuilder = true)
@NoArgsConstructor
@AllArgsConstructor
@SQLDelete(sql = "UPDATE users SET account_status = 'DELETED' WHERE uuid = ?")
@SQLRestriction("account_status = 'ACTIVE'")
public class User {

    @Id
    @GeneratedValue(strategy = IDENTITY)
    private Long uuid;

    ... 필드들

    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    @ColumnDefault("'ACTIVE'")
    @Builder.Default
    public AccountStatus accountStatus = AccountStatus.ACTIVE;

    @OneToOne(fetch = FetchType.LAZY,cascade = CascadeType.ALL, orphanRemoval = true)
    @JoinColumn(name = "owner_uuid")
    private Owner owner;

    @OneToOne(fetch = FetchType.LAZY,cascade = CascadeType.ALL, orphanRemoval = true)
    private Mate mate;

    @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Dog> dog;

    @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Board> board;

    private String fcmToken;

    public Collection<? extends GrantedAuthority> getAuthorities() {
        return Collections.singletonList(new SimpleGrantedAuthority(this.role.name()));
    }

}

[문제점 - Cascade 연관관계와 Soft Delete의 상호작용]

JPA에서는 @OneToMany, @ManyToOne 등의 연관관계에서 Cascade 옵션을 설정하여 관련 엔티티에 대한 작업을 자동으로 전파할 수 있다. 하지만 Soft Delete를 사용하는 경우, Cascade 옵션이 예상치 못한 결과를 초래할 수 있다. 내 코드의 두가지 문제점을 먼저 파악해보자.

1. CascadeType.ALL은 연관 엔티티에 대한 모든 작업을 전파하므로, Soft Delete를 적용할 때 실제로 데이터가 물리적으로 삭제될 수 있는 문제를 초래할 수 있다.

1-1. @Where 어노테이션과 함께 설정되어 있고, 연관된 엔티티에 CascadeType.ALL이 지정된 경우, 부모 엔티티를 삭제하려고 할 때 org.hibernate.ObjectDeletedException과 같은 예외가 발생할 수 있다. 이는 Hibernate가 삭제된 상태로 간주된 엔티티를 Cascade 동작을 통해 다시 접근하려고 하기 때문이다.

2. orphanRemoval = true는 부모 엔티티에서 연관 엔티티를 제거할 경우 물리적 삭제를 발생시키므로, Soft Delete와 충돌한다.

 

[해결 방안]

  1. CascadeType 및 orphanRemoval 제거: CascadeType을 제거하고 Soft Delete 메서드에서 연관된 엔티티를 명시적으로 처리한다.
public void softDelete() {
    this.posts.forEach(참조 엔티티::softDelete);
}
  1. delete() 대신 커스텀 쿼리 사용: 엔티티 삭제 시 Soft Delete 상태를 업데이트하도록 커스텀 쿼리를 사용한다.
@Modifying
@Query("UPDATE User u SET u.deleted = true WHERE u.id = :userId")
void softDeleteUser(@Param("userId") Long userId);
  1. Lifecycle Callback 활용: @PreRemove를 사용하여 삭제 전에 연관된 엔티티를 Soft Delete 처리한다.
@Entity
public class User {

    @PreRemove
    public void onPreRemove() {
        this.posts.forEach(참조 엔티티::softDelete);
    }
}
  1. 연관 엔티티의 명시적 관리: 연관 엔티티를 직접 관리하여 Soft Delete와 Cascade 간의 충돌을 방지한다.

[적용 방안]

LifeCycle Callback을 활용해서 연관된 엔티티의 컬럼을 변경하기로 하였다.

각각의 Entity에는 SoftDelete가 명시되어 있고, 각 태그 엔티티들은 다른 엔티티를 참조하고 있지 않는 그래프의 마지막 엔티티이기 때문에 직접 삭제할 수 있다.

그래서 Callback 메서드를 활용하여 각 엔티티의 컬럼을 true로 변경하여 조회 시 반환되지 않도록 하였다.

 @PreRemove
    public void onPreRemove() {
        deleteOwner();
        deleteMate();
        deleteDogs();
        deleteBoards();
    }

    private void deleteOwner() {
        if (owner != null) {
            owner.setIsDeleted(true);
        }
    }

    private void deleteMate() {
        if (mate != null) {
            mate.setDeleted(true);
        }
    }

    private void deleteDogs() {
        if (dog != null && !dog.isEmpty()) {
            dog.forEach(d -> d.setDeleted(true));
        }
    }

    private void deleteBoards() {
        if (board != null && !board.isEmpty()) {
            board.forEach(b -> b.setDeleted(true));
        }
    }

 

 

JPA의 다양한 속성들은 확실하게 알지 못하면 어디서 문제가 발생하는지 모르기 때문에 러닝커브가 지속적으로 발생할 수 있다. 이후 N+1 문제, 순환 참조, 영속성 컨텍스트와 1차 캐시 사이에서 발생하는 무결성 문제 등에 대해 추가로 다뤄보고 지속적으로 프로젝트를 개선해 나가고자 한다.

728x90
반응형
SMALL

'Spring > 프로젝트 리팩토링 회고록' 카테고리의 다른 글

키워드 초성 검색 기능 개선 과정  (0) 2024.12.16
레이어 아키텍처 구조 개선하기  (1) 2024.11.30
레이어드 아키텍처의 문제점과 해결방안  (0) 2024.11.17
'Spring/프로젝트 리팩토링 회고록' 카테고리의 다른 글
  • 키워드 초성 검색 기능 개선 과정
  • 레이어 아키텍처 구조 개선하기
  • 레이어드 아키텍처의 문제점과 해결방안
공부하고 기억하는 공간
공부하고 기억하는 공간
IT 비전공자로 시작하여 훌륭한 개발자가 되기 위해 공부하고 있는 공간입니다. 틀린 내용이나 부족한 부분이 있으면 댓글로 알려주세요 바로 수정하겠습니다.
    250x250
  • 공부하고 기억하는 공간
    IT - railroad
    공부하고 기억하는 공간
  • 전체
    오늘
    어제
    • 분류 전체보기 (325)
      • 면접 준비 (22)
        • OS (6)
        • Spring Security (0)
        • Java (3)
        • DB (11)
        • Network (3)
      • ElasticSearch (2)
      • Kafka (4)
      • Spring (22)
        • Spring Cloud (7)
        • Security6 (5)
        • JPA (12)
        • 프로젝트 리팩토링 회고록 (4)
        • Logging (8)
        • Batch (2)
      • Redis (17)
        • Redis 개념 (8)
        • Redis 채팅 (5)
        • Redis 읽기쓰기 전략 (1)
      • AWS (11)
      • 리눅스 (29)
        • 리눅스 마스터 2급 (5)
        • 네트워크(기초) (7)
        • 리눅스의 이해 (6)
        • 리눅스의 설치 (2)
        • 리눅스 운영 및 관리 (6)
      • JAVA-기초 (16)
        • JAVA기본 (11)
        • Design Pattern (5)
      • JSP (27)
        • JSP 기본 개념 (10)
        • JSP (1)
      • SQL (1)
      • TIL (36)
      • 문제 풀이 (2)
        • Programmers (9)
        • 백준 문제풀이 (28)
      • JavaScript (10)
      • HTML (17)
      • Ngrinder (1)
        • Ngrinder 문서 정리 (1)
  • 블로그 메뉴

    • 링크

    • 공지사항

    • 인기 글

    • 태그

      jsp기초
      Spring
      JS
      스프링프레임워크
      jsp request
      자바 면접질문
      JSP
      redis 채팅
      프로그래머스
      자바 면접
      자바 알고리즘
      자바 반복문
      Spring Data Redis
      redis
      spring redis
      java
      자바기초
      CSS
      리눅스마스터2급정리
      Til
      레디스
      백준
      JavaScript
      리눅스
      리눅스마스터2급
      자바스크립트
      Springframework
      자바
      HTML
      springsecurity
    • 최근 댓글

    • 최근 글

    • hELLO· Designed By정상우.v4.10.3
    공부하고 기억하는 공간
    [회고록] Spring Boot에서 JPA의 Soft Delete와 Cascade 연관관계
    상단으로

    티스토리툴바