JPA에서 soft delete 처리하는 방법에 대해 알아보겠습니다.
hard delete와 soft delete
데이터를 삭제할 때 크게 hard delete(물리 삭제)와 soft delete(논리 삭제) 두 가지 방법이 있습니다.
hard delete
- hard delete는 SQL의 delete 명령어를 사용하여 직접 데이터를 삭제하는 방법입니다.
- hard delete의 경우 delete 쿼리가 발생하기 때문에 삭제 후 DB에서 조회할 수 없습니다.
- 장점으로 select시 성능이 soft delete보다 상대적으로 좋습니다.
- 단점으로 삭제된 데이터를 복구하기 어렵고, 비즈니스에 데이터를 활용할 수 없습니다.
soft delete
- soft delete는 SQL의 update 명령어를 사용하여 특정 컬럼을 변경하여 삭제 여부를 알 수 있게 하는 방법입니다.
- soft delete의 경우 delete 쿼리가 발생하지 않기 때문에 삭제 후 DB에서 조회할 수 있습니다.
- 장점으로 삭제된 데이터를 복구하기 쉽고, 비즈니스에 데이터를 활용하기 용이합니다.
- 단점으로 select시 성능이 hard delete할 때 보다 상대적으로 떨어집니다.
위에 기술한 대로 hard delete와 soft delete는 각각의 장단점이 명확합니다.
사내에서는 일반적으로 soft delete를 많이 사용합니다. 그 이유로는 1) 삭제된 데이터를 복구하기 용이하며, 2) 삭제된 데이터의 수가 select 성능에 영향을 줄만큼 문제가 되지 않는다고 내부에서 판단하고 있습니다. 3) 또한 개인정보 보호법으로 인해 삭제 시 따로 보관해야하는 데이터도 있기 때문에 soft delete가 주로 사용되고 있습니다.
@Where
soft delete를 사용하기 위해서는 1) 삭제 요청시 delete 쿼리 대신 update 쿼리를 활용하여 삭제 여부를 판단할 수 있는 컬럼을 update해주고, 2) 조회시에는 where절에 삭제 여부를 판단할 수 있는 값을 넣어 조회하면 됩니다.
UserQueryRepository
@Repository
@RequiredArgsConstructor
public class UserQueryRepository {
private final JPAQueryFactory queryFactory;
public List<User> findAll() {
return queryFactory.selectFrom(user)
.where(user.deleted.eq(false))
.fetch();
}
}
위와 같이 삭제 여부를 판단할 수 있는 컬럼의 값을 where 절에 넣어 데이터를 조회할 수 있습니다. 저는 주로 예제와 같이 deleted 혹은 isDeleted 컬럼을 boolean 형태로 만들어 기본 생성은 false로, 삭제시 true로 변경하여 사용합니다.
위와 같은 방법은 각각의 쿼리문에 where을 추가해주는 것이기 때문에 커스텀이 용이합니다. 반면에 soft delete를 하는 쿼리문의 수가 많아질수록 관리가 힘들어지는 단점이 있습니다.
JPA에서는 위의 단점을 극복하기 위해 @Where를 지원합니다. @Where는 해당 엔티티에 일괄적으로 where문을 추가할 수 있도록 하는 어노테이션 입니다.
User
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Where(clause = "deleted = false") // 어떤 조건을 추가할 것인지 작성
public class User {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String email;
private String password;
private final boolean deleted = false;
@OneToMany(mappedBy = "user")
private final List<Post> posts = new ArrayList<>();
@OneToMany(mappedBy = "user")
private final List<Comment> comments = new ArrayList<>();
@Builder
public User(String email, String password) {
this.email = email;
this.password = password;
}
}
# result
select
user0_.id as id1_2_,
user0_.deleted as deleted2_2_,
user0_.email as email3_2_,
user0_.password as password4_2_
from
user user0_
where
(
user0_.deleted = false
)
위와 같이 @Where의 clause 내에 조건을 추가해주면 다음과 같이 조건이 추가되어 쿼리문이 나가는 것을 확인할 수 있습니다. 다만 이 기능의 아쉬운 점은 엔티티 전체에 일괄 적용 된다는 점입니다.
예를 들어 아래와 같이 where절 내의 user.deleted가 true인 값을 찾는다고 쿼리문을 작성하면 덮여 쓰여질 것 같지만 실제로는 그렇지 않습니다.
UserQueryRepository
@Repository
@RequiredArgsConstructor
public class UserQueryRepository {
private final JPAQueryFactory queryFactory;
public List<User> findAllV2() {
return queryFactory
.selectFrom(user)
.where(user.deleted.eq(true))
.fetch();
}
}
# result
select
user0_.id as id1_2_,
user0_.deleted as deleted2_2_,
user0_.email as email3_2_,
user0_.password as password4_2_
from
user user0_
where
(
user0_.deleted = false
)
and user0_.deleted=?
위와 같이 deleted의 값이 false 이면서 true인 값을 동시에 찾기 때문에 결과 값이 없습니다.
@Filter
@Where의 아쉬운 점을 개선(?)한 것이 @Filter라고 할 수 있습니다. @Filter는 다음과 같이 사용합니다.
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
// @FilterDef에 사용할 필터의 이름과 파라미터의 정보를 기술합니다.
@FilterDef(name = "deletedUserFilter", parameters = @ParamDef(name = "deleted", type = "boolean"))
// 실제 사용할 @Filter가 어떤 조건을 가지는지 기술합니다.
@Filter(name = "deletedUserFilter", condition = "deleted = :isDeleted")
public class User {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String email;
private String password;
private final boolean deleted = false;
@OneToMany(mappedBy = "user")
private final List<Post> posts = new ArrayList<>();
@OneToMany(mappedBy = "user")
private final List<Comment> comments = new ArrayList<>();
@Builder
public User(String email, String password) {
this.email = email;
this.password = password;
}
}
엔티티에 @Filter와 이를 정의하는 @FilterDef를 작성합니다.
- @FilterDef는 필터의 이름과 파라미터 등 필터의 정의를 하는 어노테이션입니다.
- @Filter는 실제 필터가 어떤 조건을 가지고 동작할지 정의하는 어노테이션입니다.
이후 아래와 같이 서비스 단에서 필터를 적용할 수 있습니다.
@Service
@RequiredArgsConstructor
@Transactional
public class UserService {
private final UserQueryRepository userQueryRepository;
private final EntityManager em;
@Transactional(readOnly = true)
public List<User> findAllFilter() {
Session session = em.unwrap(Session.class);
// 특정 필터 적용
Filter filter = session.enableFilter("deletedUserFilter");
// 특정 파라미터 설정
filter.setParameter("deleted", false);
// 실제 쿼리 호출
List<User> users = userQueryRepository.findAll();
// 특정 필터 해제
session.disableFilter("deletedUserFilter");
return users;
}
}
위에서 나오는 session은 영속성 컨텍스트를 의미합니다. 영속성 컨텍스트에 직접 필터를 적용하기 때문에 필터된 데이터들이 추출될 수 있는 것입니다.
다만 위의 방법은 영속성 컨텍스트에 직접 접근하면서 비즈니스 로직과 무관한 코드를 많이 발생시킨다는 단점이 있습니다. @Where에 비해 개선되기는 하였으나 개인적으로 자주 사용할 것 같지는 않습니다.
결론
- soft delete를 JPA에서 사용하기 위해서 @Where을 사용할 수 있습니다.
- 코드 관리 측면에서 도움이 되나, 해당 조건을 제외하고 조회하기 어렵기 떄문에 신중하게 사용할 필요가 있습니다.
- @Where의 대안으로 @Filter를 사용하여 선택적으로 soft delete할 수 있습니다.
- 다만 @Filter는 영속성 컨텍스트에 직접 접근하고, 불필요한 코드를 발생시킬 수 있어 신중하게 사용할 필요가 있습니다.
참조
'BE > JPA' 카테고리의 다른 글
@Column(nullable = false) 와 @NotNull 중 무엇을 사용해야 할까? (0) | 2022.07.18 |
---|---|
스프링 테스트 에러: JPA metamodel must not be empty! (0) | 2022.02.21 |
댓글