티스토리 뷰
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;
}
}
그리고 서비스 클래스에서는 엔티티매니저의 세션을 사용하여 필터가 해제된 커스텀된 쿼리를 사용할 수 있습니다. (하지만 추천하는 방법이 아닙니다.)
'Server' 카테고리의 다른 글
멀티노드 환경에서 ShedLock을 사용하여 Scheduled 사용하기 (0) | 2022.08.15 |
---|---|
[Java] JPA 에서 group 을 컬럼명으로 쓰는 방법 (0) | 2022.08.12 |
Jenkins에서 배치 생성하기 (1) | 2022.08.01 |
ECR 사용해서 Jenkins 배포하기 (0) | 2022.06.08 |
Amazon Linux Jenkins Docker Image 구성하기 (1) | 2022.05.28 |
Amazon Linux 서버의 HTTPS 설정 (0) | 2022.05.28 |