Soft Delete를 운영 가능한 상태 관리로 바꾸기

삭제 상태를 보존하면서 기본 조회에서는 제외하고 관리자 복구 경로를 남기는 Soft Delete 흐름

삭제 버튼을 눌렀다고 해서 데이터를 즉시 지워야 하는 것은 아니다. 신고 대상 댓글을 확인하거나, 탈퇴한 계정을 복구하거나, 삭제된 데이터를 통계에 활용해야 한다면 원본을 남기는 편이 안전하다. Soft Delete는 데이터를 보존한 채 일반 사용자 화면에서 숨기는 방식이다.

물리 삭제와 논리 삭제

  • Hard Delete: DELETE로 행 자체를 제거한다.
  • Soft Delete: 삭제 여부나 삭제 시각을 기록하고 기본 조회에서 제외한다.

Soft Delete의 장점은 복원과 감사 추적이다. 반면 삭제된 행도 데이터베이스에는 계속 남으므로 저장 공간, 인덱스 크기, 개인정보 보존 기간을 함께 관리해야 한다. “삭제”라는 이름만 붙이고 영구 보존하면 개인정보 삭제 요청과 충돌할 수 있다.

deleted_at과 상태 플래그 비교

처음에는 deleted_at IS NULL 조건을 모든 조회에 넣는 방식을 사용할 수 있다. 삭제 시각을 남길 수 있어 운영 기록에는 유리하지만, 조건을 빠뜨린 조회가 삭제된 데이터를 노출하는 문제가 생긴다. Hibernate의 @SQLRestriction처럼 전역 조건을 걸면 실수를 줄일 수 있지만 특정 관리자 조회까지 막히거나 SQL 문법에 결합될 수 있다.

대안으로 명시적인 상태 필드를 두고 조회 규칙을 저장소 계층에 모으는 방법이 있다.

@Entity
public class User {
    @Enumerated(EnumType.STRING)
    private DeletionStatus deletionStatus = DeletionStatus.ACTIVE;

    public void delete() {
        this.deletionStatus = DeletionStatus.DELETED;
    }

    public void restore() {
        this.deletionStatus = DeletionStatus.ACTIVE;
    }
}

YN 같은 문자열도 동작하지만, enum이나 별도 상태 타입을 사용하면 오타를 컴파일 단계에서 줄일 수 있다. 삭제 시각과 삭제 주체가 필요하다면 deletedAt, deletedBy를 상태와 함께 기록한다.

기본 조회와 관리자 조회를 분리하기

논리 삭제의 핵심은 “저장된 데이터”와 “사용자에게 보여 줄 데이터”를 분리하는 것이다. 일반 저장소의 메서드 이름이나 공통 조건으로 활성 상태를 명확히 하고, 관리자 화면에서는 삭제 상태를 조건으로 선택할 수 있게 한다.

public Optional<User> findActiveById(Long id) {
    return Optional.ofNullable(queryFactory
        .selectFrom(user)
        .where(user.id.eq(id), user.deletionStatus.eq(ACTIVE))
        .fetchOne());
}

public Optional<User> findIncludingDeletedById(Long id) {
    return Optional.ofNullable(queryFactory
        .selectFrom(user)
        .where(user.id.eq(id))
        .fetchOne());
}

관리자 조회를 별도 메서드로 두면 일반 화면에서 삭제 조건을 빼먹을 가능성이 낮아진다. 반대로 모든 개발자가 각자 조건을 붙이는 구조는 시간이 지날수록 누락이 생긴다.

인덱스와 유일성 제약

삭제 행이 쌓이면 deletion_status 단일 인덱스가 항상 좋은 답은 아니다. 실제 검색 조건과 데이터 분포를 확인해 (deletion_status, created_at)처럼 자주 함께 쓰는 컬럼을 검토한다. 이메일 같은 값에 유일성 제약이 있다면 삭제된 계정이 같은 이메일을 계속 점유할 수 있다. 재가입을 허용할지, 삭제된 계정도 유일성 대상에서 제외할지 정책을 먼저 정해야 한다.

보존 기간과 개인정보 처리

Soft Delete는 무기한 보관 정책이 아니다. 삭제 후 30일이 지나면 익명화하거나 완전 삭제하는 배치 작업을 둘 수 있다. 로그, 백업, 검색 인덱스에도 같은 보존 기준을 적용해야 한다. 사용자가 삭제를 요청했는데 애플리케이션 테이블만 숨기고 백업에 평문이 남아 있으면 처리 완료로 보기 어렵다.

적용 전 체크리스트

  1. 일반 조회와 관리자 조회의 범위를 분리했는가?
  2. 삭제 상태와 삭제 시각, 삭제 주체 중 무엇을 남길지 정했는가?
  3. 유일성 제약과 재가입 정책이 충돌하지 않는가?
  4. 삭제 데이터의 보존 기간과 익명화·영구 삭제 절차가 있는가?
  5. 삭제 전후의 API 응답과 캐시를 함께 무효화하는가?

Soft Delete는 어노테이션 하나로 끝나는 기능이 아니라 데이터 수명주기 정책이다. 상태를 명시하고 조회 경계를 저장소에 모은 뒤, 보존 기간과 복구 권한까지 함께 문서화해야 운영 가능한 삭제가 된다.

함께 읽기:

원문 기록: Spring Day 31: Soft Delete v2