[JPA] soft delete 자동으로 처리하기

Junseo Kim·2022년 4월 11일
15

[JPA] 응용

목록 보기
1/1
post-thumbnail

데이터를 삭제하는 방법에는 hard delete, soft delete 2가지 종류가 있습니다.

hard deletedelete 쿼리를 날려서 데이터베이스에서 실제로 삭제하는 방법을 말합니다.

soft delete는 실제로 데이터베이스에서 데이터를 삭제하는 것이 아니라, 테이블에 deleted와 같은 필드를 추가해주고, update 쿼리를 날려서 deleted 값을 변경해주는 방법입니다.

soft delete를 한 경우 조회 쿼리 결과로 삭제 처리된 값이 반환되면 안되기 때문에 where deleted = false과 같은 조건을 추가해주거나 어플리케이션 단에서 삭제되지 않은 데이터만 필터링한다던지 하는 작업이 필요합니다. 하지만 모든 조회 로직 하나하나 직접 조건을 달아준다면 빼먹을 수도 있습니다.

JPA의 구현체인 하이버네이트에는 아래 2가지 기능이 있습니다.
1) 삭제시 delete 쿼리 대신 다른 구문 실행
2) 어떤 엔티티를 조회하는 모든 쿼리에 where 조건을 추가해주는 기능

이를 이용하면 soft delete처리와 삭제되지 않은 데이터 조회를 편리하게 할 수 있습니다.

예시

아래와 같은 엔티티를 예시로 들어보겠습니다. 처음 생성시 삭제 여부 기본값을 false로 줍니다.

@Entity
public class Shop {

	@Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String name;
    
    private String address;
    
    private boolean deleted = Boolean.FALSE; // 삭제 여부 기본값 false
}

먼저 원래 삭제를 시도하면 아래와 같이 처리됩니다. delete 호출 이후에 findById로 삭제된 Shop을 조회하려고하면 실제 데이터베이스에 저장된 값이 삭제되었기 때문에 optional이 비어있게됩니다.

    @Test
    @DisplayName("Shop soft delete")
    public void delete() {
        Shop shop = new Shop("가게이름", "대구광역시");
        Shop savedShop = shopRepository.save(shop);

        assertThat(savedShop.getId()).isNotNull();
        assertThat(savedShop.isDeleted()).isFalse();

        shopRepository.delete(savedShop);
        entityManager.flush();

        Optional<Shop> afterDelete = shopRepository.findById(savedShop.getId());
        assertThat(afterDelete).isEmpty();
    }

delete 쿼리가 발생하는 것도 볼 수 있습니다.

    delete 
    from
        shop 
    where
        id=?

@SQLDelete

삭제 로직이 수행되면 delete 쿼리 대신, update 쿼리를 통해 deleted 필드 값만 true로 변경시켜줘야합니다. 이때 @SQLDelete를 사용할 수 있습니다.

@SQLDelete는 엔티티 삭제가 발생했을 때 delete 쿼리 대신 실행시켜줄 커스텀 sql 구문을 뜻하는 어노테이션입니다. 이렇게 @SQLDelete를 적어주면 엔티티 삭제 요청시 delete 쿼리 대신 적어준 update 쿼리가 실행됩니다.

@Entity
@SQLDelete(sql = "UPDATE shop SET deleted = true WHERE id = ?")
public class Shop {

	@Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String name;
    
    private String address;
    
    private boolean deleted = Boolean.FALSE; // 삭제 여부 기본값 false
}

아까 했던 테스트를 실행시켜보겠습니다. 실제 delete를 하지 않았기 때문에 optional이 비어있지 않게되고, deleted값이 true로 변경된 것을 확인할 수 있습니다.

    @Test
    @DisplayName("Shop soft delete")
    public void delete() {
        Shop shop = new Shop("가게이름", "대구광역시");
        Shop savedShop = shopRepository.save(shop);

        assertThat(savedShop.getId()).isNotNull();
        assertThat(savedShop.isDeleted()).isFalse();

        shopRepository.delete(savedShop);
        entityManager.flush();

        Optional<Shop> afterDelete = shopRepository.findById(savedShop.getId());
        assertThat(afterDelete).isNotEmpty();
        assertThat(afterDelete.get().isDeleted()).isTrue();
    }

update 쿼리가 발생한 것도 볼 수 있습니다.

    UPDATE
        shop 
    SET
        deleted = true 
    WHERE
        id = ?

*SQLDelete는 영속성컨텍스트에서 관리되다가 트랜잭션이 끝나고 실제 DB에 쿼리를 보낼때 처리를 해준다고 합니다.

@Where

soft delete 처리를 하는 경우 조회 요청시 삭제처리되지 않은 데이터만 가져와야합니다. 이때 @Where을 이용할 수 있습니다.

@Where어노테이션은 기본적으로 적용할 where 구문을 뜻하는 어노테이션입니다. 일반적으로 soft delete를 할 때 사용합니다.

@SQLDelete와 마찬가지로 엔티티 위에 적어주면 됩니다. 이렇게 하면 이 엔티티를 조회하는 모든 요청에 default 옵션으로 적용됩니다.

@Entity
@SQLDelete(sql = "UPDATE shop SET deleted = true WHERE id = ?")
@Where(clause = "deleted = false")
public class Shop {

	@Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String name;
    
    private String address;
    
    private boolean deleted = Boolean.FALSE; // 삭제 여부 기본값 false
}

이렇게 한 후 조회를 하게 된다면(ex. shopRepository.findAll()) 아래와 같이 자동으로 where 조건이 추가된 것을 볼 수 있습니다.

    select
        shop0_.id as id1_11_,
        shop0_.address as address2_11_,
        shop0_.deleted as deleted3_11_,
        shop0_.name as name4_11_ 
    from
        shop shop0_ 
    where
        (
            shop0_.deleted = false
        )

상속 관계인 경우

soft delete를 처리하려는 엔티티가 상속 관계인 경우가 있다.

@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn
public abstract class Shop {

}
@Entity
@DiscriminatorValue("C")
public class Restaurant extends Shop {

}
@Entity
@DiscriminatorValue("R")
public class Cafe extends Shop {

}

이런 경우 부모클래스에만 @SQLDelete(sql = "UPDATE shop SET deleted = true WHERE id = ?") 처리를 해주면 자식 테이블에서는 실제 삭제가 일어나게 된다.

아래와 같이 각 자식 클래스에 @OnDelete(action = OnDeleteAction.CASCADE)처리를 해줘서 막아줄 수 있다.

@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn
@SQLDelete(sql = "UPDATE shop SET deleted = true WHERE id = ?")
public abstract class Shop {

}
@Entity
@DiscriminatorValue("C")
@OnDelete(action = OnDeleteAction.CASCADE)
public class Restaurant extends Shop {

}
@Entity
@DiscriminatorValue("R")
@OnDelete(action = OnDeleteAction.CASCADE)
public class Cafe extends Shop {

}

reference

1개의 댓글

soft delete를 수동으로 구현하다가 자꾸 조건을 빼먹어서 시스템 적인 방법이 없을까 찾아보다가 이 글을 발견했네요.
도움이 되었습니다. 감사합니다.

답글 달기