Spring/JPA

Hibernate 신기능 @SoftDelete 기능

개발섭 2024. 4. 10. 17:45

Spring Boot도 벌써 3.2.X버전대가 릴리즈 되었고, 그에 따라서 자동으로 하이버네이트도 마이너 버젼 업데이트가 되었습니다. 6.4 버젼부터 해당하는 기능을 사용할 수 있습니다. 해당하는 기능을 한번 사용해보고 싶어서 테스트 해봤습니다.
일단 기본적으로 스프링 부트는 3.2.1 부터 사용 가능할 수 있으므로 참고 부탁드립니다.

기존에 사용하던 Soft Delete 방식

기존에는 Soft Delete기능을 사용하기 위해서는 개념적으로 사용해야하는 @Where절과 같은 기능을 사용하여, 삭제 컬럼에 대한 flag에 대한 조건을 엔티티 자체에 붙여서 사용해야했다.
그리고 삭제조건을 자동으로 update절로 바꿔서 처리할 수 있는 @SqlDelete문 과 같은 어노테이션이 필요했다.
즉, 개념적인 Soft Delete를 구현하기 위한 좀 귀찮은 작업이 많아지는 문제가 있었다. 일단 어노테이션을 두개 반드시 사용해야했고, 아니면 Soft Delete란 함수를 만들어서 직접 강제로 조작하는 방식을 취하는 것도 있긴하다.

@Soft Delete가 생기고 달라진 방식

여러개의 어노테이션을 등록하고 해당하는 방식이 Soft Delete인지 명확하지 않았던 기존 방식에서, 현재 방식은 @SoftDelete라는 어노테이션 하나로 땡칠 수 있다.
Class의 위에 떨렁 @SoftDelete만 있으면 아래와 같은 작업이 자동으로 처리되어진다.

컬럼이나, 클래스의 변수없이도 자동으로 클래스의 deleted 컬럼이 생성된다. 즉, 해당하는 값은 boolean과 같은 값으로 처리된다. 기본적으로는 false처리가 된다.

부가 기능

1. 컬럼명 지정, Converter 사용

당연히 그렇겠지만 deleted라는 컬럼명을 사용한다면, 팀별로는 다르겠지만 "DB 컬럼명 표준화"를 제공하는 내부 룰이 있으면 해당하는 컬럼명을 사용할 수 없기 때문에, 컬럼명을 지정할 수 있다.

@SoftDelete(columnName = "DEL_YN")

//이렇게 쓰자
@Column(name = "DEL_YN", insertable = false, updatable = false)  
private boolean isDelete;

hibernate에서 제공하는 기능이므로, 해당하는 컬럼은 직접 삽입 불가능하도록 구현해야한다.
위 코드처럼 컬럼자체의 updatable, insertable을 이용하는 방법으로 컬럼 자체를 읽어올 수야는 있지만, 해당하는 컬럼이 유용하기 위해서는 true도 검색이 되야하는데 조회를 하면 deleted를 false인 컬럼만 조회하므로 당하는 컬럼의 유용성 자체는 없다고 본다.

→ 아래에서도 이야기 하겠지만, NativeQuery가 아닌 이상 거의 이컬럼은 비어있고, 실제로 객체적으로 채워지는 값이 아니다. 그래서 만약 내가 이 컬럼을 통해서 조회를 논리 로직을 만들때는 주의가 필요해보인다.

 

삭제를 하면? → 자동으로 update 구문이 나간다.

또한 꼭 boolean으로 처리할 필요도 없다. 예를 들면 Varchar타입의 Y,N의 문자로 처리하고 있다면, 해당하는 컨버터를 이용해 해당하는 값을 변환하여 넣을 수 있다.

@SoftDelete(converter = YesNoConverter.class, columnName = "DEL_YN")  
//...
public class MemberYN implements Serializable {  

    //....

    @Column(name = "DEL_YN", insertable = false, updatable = false)  
    private String delYn;

아래를 보면 yn이 varchar type으로 생성되었다


그리고 삭제나, 생성시 Y, N으로 들어가게 된다. 생성은 N이고, 삭제는 Y로 변환해준다.

2. 자식도 Soft Delete 처리 할 수 있음

이번에는 자식도 soft Delete처리를 할 수 있는지 확인해보겠다. 일단 양방향으로 걸어서 해당하는 값이 삭제되는지 확인해보면?

public class MemberYN implements Serializable { 

@OneToMany(mappedBy = "memberYN", cascade = CascadeType.ALL, orphanRemoval = true)  
private List<MembershipAddress> addresses = new ArrayList<>();
public class MembershipAddress {  

    @Id  
    @GeneratedValue(strategy = GenerationType.IDENTITY)  
    private Long addressNo;  

    private String addressName;  

    private String addressDetail;  

    @ManyToOne(fetch = FetchType.LAZY)  
    private MemberYN memberYN;

해당 테스트코드를 돌리면 sql에 자식 관계의 경우 hard delete처리 되버린다.

@Test
void yn컬럼이_삭제할때_Y로_들어갈것인지() {
    MemberYN memberYN = new MemberYN("test");
    em.persist(memberYN);
    em.remove(memberYN);
    em.flush();

    MemberYN find = em.find(MemberYN.class, memberYN.getTestNo());
    Object o = em.createNativeQuery("SELECT * FROM TEST_MEMYN WHERE test_no = 1", MemberYN.class).getSingleResult();
    assertThat(((MemberYN)o).getDelYn(), is("Y"));
    List<MemberYN> delfind = em.createQuery("Select my from MemberYN my where my.testNo = 1", MemberYN.class).getResultList();
    assertThat(delfind.size(), is(0)); //JPQL은 soft delete조건이 먹는다. singleResult하믄 오류 나서 List형으로 수정.
    assertThat(find, nullValue());
}


자식도 그러면 Soft Delete처리하고 싶으면?
동일하게 자식 entity에 softDelete 어노테이션을 입히면 동작한다.

@SoftDelete
public class MembershipAddress {  

    @Id  
    @GeneratedValue(strategy = GenerationType.IDENTITY)  
    private Long addressNo;  

위의 테스트 코드를 동작하게 하면?


둘 다 업데이트 처리가 된다!

필드 @softDelete처리도 가능은 한데... 되는 타입이 두개밖에 없다.

  1. FIELD / METHOD, where it applies to the rows of an jakarta.persistence.ElementCollection or jakarta.persistence.ManyToMany table.
    두 타입은 활용도가 없었어가지고, 해당하는 값을 처리하는건 아래의 링크 예제를 참고 해보는 것도 도움이 될거라 본다.

또한 긍정적인 지점들 몇개를 찾았다.
JPQL의 경우도 해당 JPQL절에 Where에 delete 조건 없이 걸어도 자동으로 delete값이 처리되어, @Query를 통해 값이 처리 된경우 해당하는 값을 매번처리 해주지 않아도 될것으로 보인다. (다 확인한건 아니니 확신은 하지말자!)


delfind을 자세히보면 난 where절에 testNo = 1만 걸었지만...? 실제 쿼리는?


and절에 yn절이 자동으로 먹어저 들어간다.
물론 확인은 해봐야하는데... @SoftDelete만의 특성인건지 @Where절때문에 자동으로 And 조건이 들어가는지는 확인해봐야하겠다.
연계글이 생기면, 여기에 남기겠다.

문제점 혹은 개선점

개인적으로 Soft Delete는 boolean으로 하는것에 큰 의의는 없다만, 운영적으로 봤을때는 LocalDateTime으로 처리하는게 좀더 좋지 않나 싶다. false 플래그를 세우면, 해당하는 Soft delete의 완전 삭제 조건을 반드시 "수정시간"기준으로 잡아야하는데, 그게 진짜로 flag에 의해서 update이후 수정 안됬다는 보장을 하기가 어렵다고 본다. 만약 정책적으로 해당 엔티티의 정책으로 어떤 변수가 전체 수정되어서 해당하는 값이 수정되었다면? 처럼 수정일시는 안의 값이 하나라도 바뀌면 무조건 수정시간이 바뀌므로 강제하기가 어렵다.
운영적으로 3년의 유예기간을 두고 실제로 삭제는 못하게 만들고, 배치로 소프트 삭제후 3년 이후의 값을 삭제한다던가와 같은 방식으로 처리할 수 있으므로, 운영적인 서비스 정책 그리고 외부 노출될 정책 문서에도 해당하는 값을 처리할 수 있으니 명확한 일시로 처리하는게 더 좋다고 본다. 문제는....

현재 @SoftDelete는 LocalDateTime을 지원할 수 없는 상태이다. 아니 엄밀히 말하면, 컬럼이 Null을 통해서 처리해야하는 경우가 불가능하다.
삭제일시가 필요한 경우 삭제 여부를 IS NULL을 통해 삭제 여부를 따져야하는데, 컬럼 컨버터가 Null을 허용하지 않는다!


그렇다면, 이렇게 null을 통해 일시처리는 불가능하다고 봐야할듯 싶다. 아쉬운 지점이다. hibernate에 한번 개선 문의를 남겨보고 싶긴하다.

마지막으로 해당기능을 나중에 개선이되어진다면, 한번쯤 써보고 싶다는 생각은 한다. 위에서 말했던 문제점을 개선하는 방향이면 훨씬 더 효과적으로 기능 구현을 해볼 수 있을 거 같아 보인다. 

 

출처

https://www.baeldung.com/java-hibernate-softdelete-annotation
https://docs.jboss.org/hibernate/orm/6.5/javadocs/org/hibernate/annotations/SoftDelete.html#strategy()
https://ohksj77.tistory.com/249
ㄴlocalDateTime에 대한 이야기를 좀 나눴고, 글쓰는데 도움을 받았습니다.

 

SoftDelete (Hibernate Javadocs)

(Optional) Conversion to apply to determine the appropriate value to store in the database. The "domain representation" can be: true Indicates that the row is considered deleted false Indicates that the row is considered NOT deleted By default, values are

docs.jboss.org

 

Hibernate 6.4의 @SoftDelete 사용법 탐구

soft-delete-hibernate hibernate가 제공하는 @SoftDelete 어노테이션을 알아보자 [hibernate 6.4.4 버전 기준으로 MySQL과 함께 테스트 했으며 6.4 버전부터 도입된 어노테이션이다.] @SoftDelete JavaDoc 다음 어노테이

ohksj77.tistory.com

 

Test코드

https://github.com/ventulus95/softdelete-test

 

GitHub - ventulus95/softdelete-test: hibernate 6.4 @softdelete 값 처리

hibernate 6.4 @softdelete 값 처리. Contribute to ventulus95/softdelete-test development by creating an account on GitHub.

github.com