사이드 프로젝트 진행 중에 특정 데이터에 대해 Soft Delete 를 적용해달라는 요청을 받았었다.
Soft Delete 란?
데이터를 영구적으로 삭제하지 않고 데이터가 삭제된 것처럼 처리하는 방법이다. 이렇게하면 데이터를 복구할 때 유용하며 Soft Delete 를 구현할 때는 데이터 항목에 isDeleted 등의 필드를 추가하여 삭제된 것처럼 처리한다.
예를 들어, Soft Delete 가 아닌 Hard Delete ( 물리적 삭제 ) 상태라면 user_id 가 1인 사용자를 삭제하는 경우 데이터베이스에서 해당 사용자의 레코드를 완전히 제거하게 된다. 반대로 Soft Delete 를 사용하였을 때는 아래의 is_deleted 필드가 'N' 에서 'Y' 로 바꿔 사용자가 삭제된 것처럼 처리하게 된다.
Soft Delete with JPA
JPA 를 사용하면 Soft Delete 를 간단하게 적용할 수 있다.
1. isDeleted 필드 추가 및 @SQLDelete 어노테이션 사용
먼저 아래 코드와 같이 id, email, password 가 있는 User Entity 를 만들어 isDeleted 필드를 추가한다. 그리고 @SQLDelete 어노테이션을 사용하여 엔티티에 대한 delete 쿼리를 재정의하면 간단하게 Soft Delete 를 적용할 수 있다.
@Entity @Table(name = "users")
@NoArgsConstructor(access = AccessLevel.PROTECTED) @Getter
@SQLDelete(sql = "UPDATE users SET is_deleted = 'Y' WHERE id = ?") // delete 쿼리 재정의
public class User {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String email;
private String password;
private String isDeleted; // 삭제 여부 필드 추가
@Builder
public User(Long id, String email, String password) {
this.id = id;
this.email = email;
this.password = password;
this.isDeleted = "N"; // 디폴트 값 설정
}
}
그 결과를 확인해보기 위해 테스트 코드를 작성하였고 이를 통해 User 데이터를 저장할 때는 'N' 으로 저장하고 삭제할 때는 delete 쿼리가 아닌 내가 재정의한 update 쿼리로 isDeleted 필드가 'Y' 로 바뀜을 볼 수 있었다.
@Test
@DisplayName("User Soft Delete 테스트")
public void userSoftDeleteTest(){
// given
userRepository.save(
User.builder()
.id(1L)
.email("limnj@test.com")
.password("1234").build()
);
// when
userRepository.deleteById(1L);
// then
User findUser = userRepository.findById(1L).orElseThrow();
assertEquals(findUser.getIsDeleted(),"Y");
}
- 실행결과
2. @Where 혹은 @Filter 사용하여 검색하기
삭제를 필드 값으로 구분했기 때문에 검색 조건에서 삭제되지않은 엔티티만 조회한다는 조건이 필요해졌다.
간단한 방법으로 @Where 을 사용하면 모든 쿼리에 @Where 로 정의한 쿼리가 같이 실행되어 isDeleted="N" 인 엔티티만 조회할 수 있게 된다.
@Where(clause = "is_deleted = 'N'")
public class User { .. }
그런데 이렇게 설정했을 때의 문제점은 삭제된 데이터를 조회할 수 없다는 것이다.
만약, 데이터가 사용자 입장에서 복구가 가능해야하는 데이터라면 위의 쿼리로는 이 요구사항을 만족시킬 수 없게 된다.
이에 대한 해결방안으로 우리는 @Filter 와 @FilterDef 를 사용할 수 있다.
- @FilterDef : 필터의 정의를 선언하는 데 사용. 필터의 이름과 필터링에 사용될 매개변수의 타입 정보가 포함된다.
- @Filter : @FilterDef 에 의해 정의된 필터를 실제 엔티티나 컬렉션에 적용될 때 사용. SQL 조건 구문으로 표현되며 필터 매개변수를 포함할 수 있다.
@FilterDef(name = "deletedUserFilter", parameters = @ParamDef(name = "isDeleted", type = String.class))
@Filter(name = "deletedUserFilter", condition = "is_deleted = :isDeleted")
public class User { ... }
만약에 findAll() 을 호출할 때에 삭제되지않은 ( isDeleted = 'N' ) 회원만 모두 조회하고 싶다면 아래와 같이 filter 를 구현할 수 있다.
public class UserServiceImpl implements UserService{
private final UserRepository userRepository;
private final EntityManager entityManager;
..
@Override
public List<User> enableDeletedUsersFilter(String isDeleted) {
Session session = entityManager.unwrap(Session.class);
session.enableFilter("deletedUserFilter").setParameter("isDeleted", isDeleted);
List<User> users = userRepository.findAll();
session.disableFilter("deletedUserFilter");
return users;
}
}
테스트를 위한 코드는 아래와 같다.
@Test
@DisplayName("User Soft Delete - Filter 테스트")
public void userSoftDeleteFilterTest(){
// given
for (int i = 0; i < 10; i++) {
userRepository.save(
User.builder()
.email("limnj"+i+"@test.com")
.password("1234")
.build()
);
}
userRepository.deleteById(2L);
userRepository.deleteById(3L);
userRepository.deleteById(7L);
// when
userServiceImpl.enableDeletedUsersFilter("N").forEach(System.out::println);
}
- 실행결과
정리
그렇다고 모든 엔티티에 Soft Delete 를 적용하게 되면 불필요한 필드 혹은 데이터가 늘어날 수 있으니 적절한 판단이 필요할 것 같다. 실제 회원의 경우에는 개인 정보가 포함되어있을 가능성이 크기 때문에 Soft Delete 를 적용하더라도 마스킹을 한다던가 그 기간에 대한 명시를 확실히 정해두고 시행하는 것이 좋을 것 같다.
참고
https://www.baeldung.com/spring-jpa-soft-delete
https://copyprogramming.com/howto/how-to-implement-a-soft-delete-with-spring-jpa
'Web' 카테고리의 다른 글
[JPA] deleteAll() 과 deleteAllInBatch() (0) | 2024.03.17 |
---|---|
[JPA] @ColumnDefault 이해하기 (0) | 2024.01.28 |
[Spring] QueryDsl 로 동적 쿼리 짜기 with JPA (0) | 2024.01.07 |
[JPA] 양방향 순환 참조 (0) | 2023.12.24 |
[Spring] 예외 코드 Enum 으로 관리하기 (0) | 2023.12.17 |