데이터를 삭제할 때, 즉시 데이터베이스에서 제거하는 방식(DELETE)은 간단하지만,
데이터 복구가 어렵고, 감사(audit) 및 트랜잭션 이력을 유지할 수 없다는 단점이 있다.
이에 따라, 많은 애플리케이션에서는 논리 삭제(Soft Delete)를 적용하고,
일정 시간이 지난 후 배치 작업을 통해 데이터베이스에서 실제로 삭제하는 방법을 사용한다.
이번 글에서는 JPA를 활용하여 논리 삭제 후 배치 작업으로 데이터를 완전 삭제하는 구현 방법을 설명한다.
논리 삭제 (Soft Delete) 란?
데이터 베이스에서 데이터를 즉시 제거하지 않고, 삭제된 상태를 나태는 플래그(컬럼)을 추가한다.
위 방식을 이용한다면, 데이터를 유지하면서 삭제된 것처럼 처리할 수 있다.
1️⃣ 논리 삭제 구현 (@SQLDelete & @Where)
(1) 엔티티 설정(SoftDelete)
@Getter
@Setter
@Entity
@Table(name = "user")
@SQLDelete(sql = "UPDATE user SET is_deleted = true, deleted_at = now() WHERE id = ?")
@Where(clause = "is_deleted = false") // 조회 시 자동으로 필터링
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
@Column(name = "is_deleted")
private boolean isDeleted = false; // 기본값 false (삭제 안 됨)
@Column(name = "deleted_at")
private LocalDateTime deletedAt; // 삭제 시각
// 삭제 메서드
public void softDelete() {
this.isDeleted = true;
this.deletedAt = LocalDateTime.now();
}
}
🔹 @SQLDelete로 DELETE 대신 UPDATE 실행
@SQLDelete(sql = "UPDATE users SET is_deleted = true, deleted_at = now() WHERE id = ?")
@SQLDelete란?
Hibernate가 DELETE 대신 UPDATE 쿼리를 실행하도록 설정하는 애노테이션입니다.
즉, `DELETE FROM users WHERE id = ?` 명령이 실행될 때, 다음과 같은 쿼리로 대체됩니다.
UPDATE users SET is_deleted = true, deleted_at = now() WHERE id = ?
🔹 @SQLDelete의 장점
✅ 데이터가 물리적으로 삭제되지 않아 복구 가능
✅ 연관 테이블(FK)과의 무결성을 유지할 수 있음
✅ 실수로 데이터를 삭제하는 문제를 방지
🔹 Soft Delete된 데이터 자동 필터링
@Where(clause = "is_deleted = false") // 조회 시 자동 필터링
@Where란?
JPA 엔터티를 조회할 때, Hibernate가 자동으로 특정 조건을 추가하도록 설정하는 애노테이션입니다.
`clause` 는 SQL에서 `절(Clause)` 를 의미하며, Hibernate가 자동으로 추가할 SQL `WHERE 조건`을 지정하는 부분이다.
findAll() 같은 메서드를 실행하면,
`@Where(clause = "is_deleted = false")`의 코드로인해 Hibernate가 자동으로 sql문을 변환합니다.
SELECT * FROM users WHERE is_deleted = false;
즉, 논리적으로 삭제된 데이터(is_deleted = true)는 자동으로 제외됩니다.
2️⃣ 일정 시간이 지나면 물리 삭제 (Batch Job)
(1) 레포지토리 (Repository)
import org.springframework.data.jpa.repository.JpaRepository;
import java.time.LocalDateTime;
import java.util.List;
public interface UserRepository extends JpaRepository<User, Long> {
// 논리 삭제된 데이터 중 6개월 이상 지난 것 찾기
List<User> findAllByIsDeletedTrueAndDeletedAtBefore(LocalDateTime dateTime);
}
(2) 서비스 설정 : 배치 스케줄링 (Hard Delete)
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
// 자동 삭제
@Scheduled(cron = "0 0 3 * * ?")
@Transactional
public void deleteOldSoftDeletedUsers() {
LocalDateTime sixMonthsAgo = LocalDateTime.now().minusMonths(6);
List<User> usersToDelete = userRepository.findAllByIsDeletedTrueAndDeletedAtBefore(sixMonthsAgo);
if (!usersToDelete.isEmpty()) {
userRepository.deleteAll(usersToDelete);
System.out.println(usersToDelete.size() + " users permanently deleted.");
}
}
}
🔹 특정 시간에 메서드를 자동 실행
`@Scheduled` 애노테이션을 사용하면 특정 시간에 메서드를 자동 실행할 수 있습니다.
cron = "0 0 3 * * ?" → 매일 새벽 3시 정각에 실행된다.
데이터베이스 성능을 고려하여, 시스템 부하가 적은 시간에 실행하는 것이 좋다.
🔹 오류 발생 시 롤백
`@Transactional ` 어노테이션을 사용하여 오류 발생 시 롤백할 수 있다.
이를 통해 데이터가 일관성 있게 유지된다.
🔹 특정 기간이 지난 데이터는 삭제
LocalDateTime sixMonthsAgo = LocalDateTime.now().minusMonths(6);
List<User> usersToDelete = userRepository.findAllByIsDeletedTrueAndDeletedAtBefore(sixMonthsAgo);
`LocalDateTime.now().minusMonths(6)` → 현재 날짜 기준으로 6개월 전의 날짜를 계산한다.
`findAllByIsDeletedTrueAndDeletedAtBefore(sixMonthsAgo)`는 Spring Data JPA의 메소드 이름 기반 쿼리로,
is_deleted = true (논리 삭제된 유저) 이고, deleted_at이 6개월 이전인 데이터들을 조회한다.
🔹 조회된 유저를 물리적으로 삭제
if (!usersToDelete.isEmpty()) {
userRepository.deleteAll(usersToDelete);
System.out.println(usersToDelete.size() + " users permanently deleted.");
}
조회된 데이터가 존재하면 물리적으로 삭제 (DELETE FROM user) 한다.
데이터 관리에서 논리 삭제(Soft Delete)는 데이터의 무결성과 감사(Audit) 기능을 유지하면서도
실수로 인한 데이터 손실을 방지할 수 있는 효과적인 방법입니다.
JPA의 `@SQLDelete`와 `@Where`를 활용하면 논리 삭제된 데이터를 자동으로 필터링할 수 있으며,
Spring Batch나 `@Scheduled`를 사용해 특정 시점에 물리적으로 데이터를 정리할 수 있습니다.
이러한 구현 방식을 적용할 때는 데이터의 삭제 주기, 데이터베이스 성능, 대량 데이터 삭제 시 부하 관리 등을 종합적으로 고려해야 합니다. 특히 대량 데이터 삭제 시 배치 단위 삭제나 페이징 처리를 통한 삭제 최적화가 중요합니다.
프로젝트의 특성과 요구사항에 맞춰 논리 삭제와 물리 삭제 전략을 병행한다면,
데이터 보존과 성능 최적화라는 두 마리 토끼를 모두 잡을 수 있을 것입니다.