DataJPA) Soft delete

Wonjun Lee·2025년 7월 25일

Soft delete

삭제했음을 표시만 하고, 실제 삭제는 나중에 하기 위해 사용한다.
회원 데이터의 경우 삭제 후에도 정보를 유지하여야 하며, 어떤 데이터는 삭제 후에도 복구 가능해야 하기 때문에 관련 데이터를 유지하기 위해서 사용한다.

나의 경우.

이번에 수행중인 프로젝트에서 유저 정보를 삭제하더라도 1년 간은 보관하도록 만들고 싶었다.

특히 연관된 데이터가 많아서 회원데이터를 바로 삭제하는 것이 너무 많은 영향을 끼친다고 생각했다.

users 테이블에 회원 데이터를 저장하고 있었는데, 이 테이블에 is_activated 컬럼과 deactivated_at 컬럼을 추가했다.

is_activated 컬럼은 활성화 여부를, deactivated_at은 비활성화 일시를 기록한다.

@Entity(name = "users")
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@EntityListeners(AuditingEntityListener.class)
@SQLRestriction("is_activated <> false")
@SQLDelete(sql = "update users set is_activated = false, deactivated_at = now() where id = ?")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
	
    ...
    
    @Column(name = "is_activated")
    private Boolean isActivated;

    @Column(name = "deactivated_at")
    private LocalDateTime deactivatedAt;
}

위 코드예시에서 사용한 2가지 애너테이션을 적용하면 Soft delete를 쉽게 구현할 수 있다.

SQLRestriction

예전에는 @Where 애너테이션을 사용했다고 한다. 근데 이 deprecated 되었다. 검색해보니 @SQLRestriction을 사용하면 특정 엔티티 클래스에 대한 SQL에 제약을 걸 수가 있다.

나의 경우 @SQLRestriction("is_activated <> false")를 달아서 활성화 X인 데이터는 필터링이 자동으로 포함되도록 작성하였다.

실제 쿼리가 수행될 때, 자동으로 where 조건절이 추가된다.

간단하게 논리적 삭제된 엔티티를 거를 수 있어 효과적이지만, 동적인 쿼리를 작성하기에 좋지 않다. 또한 복잡한 필터링이 어렵다.

@SQLDelete(sql = "sql")

이 엔티티에 대한 delete가 동작할 때, delete문 대신 명시된 sql문이 동작한다.
이 덕분에 delete 쿼리 메서드를 수행해도 update로 변경하여 soft delete를 쉽게 구현할 수 있다.

내부에 작성한 sql문은 다음과 같다.

update users set is_activated = false where id = ?

위와 같이 작성하면 id 기반의 삭제 로직이 update로 변경된다.

 @Test
    public void 유저를_논리적으로_삭제한다() {
        User user = userRepository.save(User.builder().email("test@mail.com").isActivated(true).role("USER").name("wonjun").build());

        userRepository.delete(user);

        System.out.println(user.getIsActivated());
        System.out.println(user.getDeactivatedAt());
    }

위 테스트 결과, true, null이 반환된다.
update users set is_activated = false, deactivated_at = now() where id = ?

예상된결과는 위 쿼리가 수행되는 것이고, 이후 isActivated는 false, deactivatedAt은 현재 날짜가 입력되어 있어야 했다.

디버거를 통해 확인한 결과, delete 연산으로 논리적 삭제가 수행된 후에는 영속성 컨텍스트에서 해당 엔티티를 read할 수가 없었다. 또한, 기존 user 엔티티에 대해 update가 발생하지 않은 것은 내부적으로 논리적 삭제된 데이터를 처리하여 그 객체로 관리되지 않도록 만들어진 것 같다. (패러다임 불일치 해소를 위한 동일성 유지가 해제 됨.)

실제 삭제 구현.

테이블들이 외래키 관계에 on delete cascade를 설정하고 있다면, 유저 정보 삭제로 연관된 정보들이 재귀적으로 삭제될 수 있다.

다음과 같은 이벤트 핸들러를 등록하고 일주일에 한 번씩 삭제 처리를 수행하기로 했다.

create event if not exists soft_delete_users on schedule every 7 day starts current_timestamp
do delete from users where TIMESTAMPDIFF(DAY, deactivated_at, NOW()) >= 365;

0개의 댓글