1. 개요
데이터를 실제 물리적으로 삭제하지 않고, 논리적 삭제(soft delete) 해야하는 상황이 자주 발생한다.
이러한 상황에 대처하기 위해 JPA에서는 여러 soft delete 적용 방식이 사용된다.
각 방식들에 대해 장단점을 비교하고 어떤 상황에 어떤 방식으로 적용해야 좋은지 살펴보자
2. Soft Delete 방식
2-1. 더티 체킹을 이용한 방식
엔티티의 상태를 변경하여, 트랜잭션 종료 시점에 JPA의 더티 체킹에 의해 UPDATE 쿼리를 발생시키는 방법이다.
예제
@Table
@Entity
@Getter
public class Review extends SoftDeleteBaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "is_deleted", nullable = false, columnDefinition = "tinyint")
private boolean isDeleted = false;
public void delete() {
this.isDeleted = true;
}
}
review.delete(); // 삭제 처리
실행 SQL
UPDATE review SET is_deleted = true WHERE id = ?
2-2. @SqlDelete를 이용한 방식
@SqlDelete 애너테이션과 jpa repository의 delete() 메서드를 호출하면 delete 대신 update sql이 실행된다.
{h-schema}, {h-table}을 사용 시 사용시 상속 구조에서도 재사용 가능하다.
개발자의 실수로 물리 삭제하는 사고를 방지할 수 있다.
@Table
@Entity
@SQLDelete(sql = "UPDATE {h-schema}{h-table} SET is_deleted = true WHERE id = ?")
@Getter
public class Review {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "is_deleted", nullable = false, columnDefinition = "tinyint")
private boolean isDeleted = false;
}
reviewRepository.delete(review);
3. 조회 문제와 자동 필터링
soft delete 정책 사용 시 가장 큰 고민은 데이터를 조회할 때이다.
삭제되지 않은 데이터를 조회하려면 where is_deleted = false 와 같은 조건을 항상 붙혀줘야 한다.
이를 해결하기 위해 hibernate는 자동 필터링 기능을 제공한다.
3-1. @Where, @SQLRestriction을 통한 자동 필터링
해당 애너테이션을 사용하면 자동으로 필터링이 가능하다.
@Where은 hibernate 6.3부터 deprecated 되었고, 이를 대체하는 애너테이션이 @SQLRestriction이다.
@Entity
// @Where(clause = "is_deleted = false") // deprecated
@SQLRestriction("is_deleted = false")
public class Review {
}
조회 SQL
SELECT * FROM review WHERE is_deleted = false
3-2. @SoftDelete 애너테이션
hibernate 6.4에 도입된 soft delete 통합 애너테이션으로 @SqlDelete + @SQLRestriction 동일한 효과를 볼 수 있다.
@Entity
@SoftDelete
public class Review {
}
해당 애너테이션도 주의할 점이 있다.
xxxToOne 관계에서 패치 전략을 Lazy로 사용하면 아래와 같은 예외가 발생한다.
이는 LAZY 로딩은 프록시 객체로 전제로 하지만, @SoftDelete는 프록시 객체로는 엔티티가 유효한지 (is_deleted = false) 검증된 객체가 필요하기 때문이다.
UnsupportedMappingException: To-one attribute (com.bbangle.bbangle.seller.domain.Seller.store) cannot be mapped as LAZY as its associated entity is defined with @SoftDelete
@NotFound를 사용하여 조건에 부합하지 않는 객체라면 예외를 던지지 말고 null로 처리하도록 해결할 수 있지만, 근본적인 해결책은 아니다.
@ManyToOne(fetch = FetchType.LAZY)
@NotFound(action = NotFoundAction.IGNORE)
private Store store;
4. 자동 필터링 방식이 항상 좋은가?
결론은 그렇지 않다.
@Where, @SQLRestriction, @SoftDelete와 같은 자동 필터링 방식은 분명히 편리하긴하다.
하지만, 이 방식에 치명적인 한계가 존재하다.
1. 삭제되지 않은 데이터를 조회하기 어려움
자동 필터링이 적용된 엔티티는 모든 조회 쿼리에 is_deleted = false 조건이 강제로 적용된다.
이로 인해 삭제된 데이터를 조회하기에 매우 까다로워진다.
그래서 자동 필터링 방식이 필요한지 결정을 신중하게 가져가야 한다.
백오피스 기능, 통계, 이력 분석이 필요한 서비스에는 적합하지 않다.
또한, 현실적으로 삭제된 데이터만 100% 조회하는 서비스는 거의 존재하지 않는다.
그러한 경우라면 soft delete 자체를 사용할 필요가 없으며, hard delete가 더 명확한 선택이 될 수 있다.
5. 결론
단순 조회 위주의 사이트 프로젝트의 생산성이 중요하다면 자동 필터링 방식을 적용할 수 있지만, 어드민, 통계, 이력 관리가 중요한 서비스에는 명시적인 조회 조건이 더 적합하다.
무조건 좋은 방식은 없고, 서비스의 성격과 요구사항에 맞는 soft delete 전략을 선택하는 것이 중요하다.
'Programming > Spring' 카테고리의 다른 글
| [Spring] Filter vs Interceptor 차이 (0) | 2025.10.20 |
|---|---|
| [Spring] Spring MVC에서 Pageable 파라미터가 동작하는 방식 (1) | 2025.09.16 |
| [Spring Security] SecurityFilterChain 동작 분석 - Logout 요청 시 AuthenticationEntryPoint 미호출 문제 (3) | 2025.08.13 |
| [Spring 테스트] Mockito ArguementCaptor란? (1) | 2025.08.10 |
| [Spring 테스트] 테스트 간 데이터 충돌 문제 해결(@Sql 이해) (0) | 2025.07.11 |