티스토리 뷰

반응형

JPA 를 사용하는 일이 많아지면서 데이터를 물리삭제 (repository.delete)보다 논리삭제를 진행하는 경우가 더 많아졌습니다. 왜냐면 데이터를 관리하는 입장에서 데이터가 사라지는 것보다 남은 데이터이지만 사용자에게 보이게 만들기 위한 처리를 하는 관점이 훨씬 유리하기 때문입니다. 

 

JPA Soft Delete vs JPA Hard Delete

Soft Delete 는 말 그대로 데이터를 논리 삭제하는 것을 말합니다.

(특정 컬럼을 업데이트)

UPDATE entity SET deleted = true WHERE id = ?

 

Hard Delete

데이터를 물리 삭제합니다. 

DELETE FROM entity WHERE id = ?

 

 

JPA Annotation 구현

JPA 에서 데이터를 Soft하게 관리하기 위해서는 먼저 Annotation 2개를 공부할 필요가 있습니다. 

1. @SQLDelete

이 어노테이션의 경우 데이터를 논리삭제하게 만들어줄 컬럼과 테이블의 PK 값을 지정해주는 역할을 합니다. 

@SQLDelete(sql = "UPDATE entity_class SET deleted = true WHERE entityId = ?")

작성은 위와 같이 진행하고 Entity 클래스의 상단에 적어줍니다. 위와 같이 작성하게 되면 crudRepository.delete의 명령어가 실행되는 경우 아래와 같이 작동되는 것으로 확인할 수 있습니다. 

@Transactional
@Modified
void delete(T cls);

// UPDATE entity_class SET deleted = true WHERE entity_id = ?

위의 쿼리는 delete by column 처리를 하여 @Transactional 처리를 하게끔 만들었기 때문에 이어서 호출되는 SELECT 쿼리에서는 출력이 사라지게 됩니다. 아래의 Where 어노테이션이 그 역할을 합니다. 

 

2. @Where

@Where(clause = "deleted = false")

Where 어노테이션은 SELECT 절의 쿼리를 전송할 때 CrudRepository에서 findByColumn와 같은 쿼리에 자동 적용됩니다. 예를 들어 위와 같이 작성해주면 조회하는 쿼리는 그 아래와 같이 나옵니다. 

List<EntityClass> findByColumn(@Param("col") String col);
// SELECT * FROM entity_class 
// WHERE col = :col
// AND deleted = false

즉, Where 절에 자동으로 붙게 되어 쿼리를 길게 작성할 필요가 없습니다.

 

 

주의사항: 위의 쿼리가 모든 Repository 에 적용되지만
NativeQuery를 사용한 쿼리 함수에는 작동하지 않습니다.

 

 

LocalDate(LocalDateTime)로 Soft Delete 구현하기

boolean 값으로만 Soft Delete가 가능한 것은 아닙니다. 아래와 같이 클래스를 작성해보겠습니다. 

@SQLDelete(sql = "UPDATE entity SET deleted_at = current_timestamp WHERE entity_id = ?")
@Where(clause = "deleted_at is null")
@Entity
@Table(name = "entity_class")
@Getter
public class EntityClass {

   @Id
   @GeneratedValue(strategy = GenerationType.IDENTITY)
   private Long entityId;
   private String col1;
   @CreationTimestamp
   private LocalDateTime createdAt = LocalDateTime.now();
   private LocalDateTime deletedAt;
}

위와 같이 @SQLDelete 문을 작성하면 entityClassRepository.delete 명령이 수행된 경우 deletedAt에 삭제가 된 시점이 기록됩니다. 이렇게 되면 실제로 삭제가 되었는지는 Objects.isNull(deletedAt)으로 확인이 가능하겠죠.

 


 

부록: 논리삭제한 데이터 조회하기 

Entity Class

@SQLDelete(sql = "UPDATE entity SET deleted_at = current_timestamp WHERE entity_id = ?")
@Where(clause = "deleted_at is null")
@Entity
@Table(name = "entity_class")
@Getter
@FilterDef(name = "deletedEntityFilter", parameters = @ParamDef(name = "deleted", type = "boolean"))
@Filter(name = "deletedEntityFilter", condition = "deleted = :deleted")
public class EntityClass {

   @Id
   @GeneratedValue(strategy = GenerationType.IDENTITY)
   private Long entityId;
   private String col1;
   @CreationTimestamp
   private boolean deleted;
}

위의 클래스에서는 @FilterDef, @Filter 어노테이션을 사용하여 파라미터의 타입과 필터링 조건을 생성해주었습니다. 이 필터링 조건은 아래의 서비스 클래스에서와 같이 사용할 수 있습니다.

 

Service Class

@Service
@RequiredArgsConstructor
public class EntityClassService {
   private final EntityClassRepository entityClassRepository;
   
   private final EntityManager entityManager;
   
   public Iterable<EntityClass> findAll(boolean deleted) {
      Session session = entityManager.unwrap(Session.class);
      Filter filter = session.enableFilter("deletedEntityFilter"):
      filter.setParameter("deleted", deleted);
      Iterable<EntityClass> entities = entityClassRepository.findAll();
      session.disableFilter("deletedEntityFilter");
      return entities;
   }
}

그리고 서비스 클래스에서는 엔티티매니저의 세션을 사용하여 필터가 해제된 커스텀된 쿼리를 사용할 수 있습니다. (하지만 추천하는 방법이 아닙니다.)

반응형
댓글
공지사항