[JPA] Hibernate @SoftDelete

2025. 9. 5. 20:54스프링

프로젝트 진행중 데이터를 Soft Delete로 데이터를 삭제하기 위해서 deleted_at 필드를 추가하고

@Where 어노테이션을 활용하려 했는데, Hibernate 6.3버전부터 @Where이 Deprecated 되어서 찾아보던중 

Hibernate 6.4 버전부터 추가된 @SoftDelete 어노테이션을 알게되어서 해당 내용에 대해서 설명해보겠습니다.


@SoftDelete적용

적용하는 방법은 아주 간단합니다.

Entity위에 @SoftDelete 어노테이션을 달아주면 됩니다.

@Entity
@SoftDelete
public class MeetingSoftEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String meetingName;
}

@Entity
public class MeetingHardEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String meetingName;
}

 

이렇게하고 생성된 테이블을 보면 클래스에 deleted라는 필드를 추가하지 않았음에도 추가된걸 확인할 수 있습니다.

 

 

삭제 동작 확인

아래와 같이 서비스를 만들고 SQL을 보면 차이를 알 수 있습니다.

public void softDeleteTest() {
    saveMeetingData();
    
    meetingSoftRepository.deleteAll();
    meetingHardRepository.deleteAll();
}
Hibernate: 
    update
        meeting_soft_entity 
    set
        deleted=1 
    where
        id=? 
        and deleted=0
Hibernate: 
    delete 
    from
        meeting_hard_entity 
    where
        id=?

 

@SoftDelete를 붙인 엔티티를 삭제할 때는 DELETE 구문이 아닌 UPDATE 구문으로 deleted를 업데이트하는 걸 확인할 수 있습니다.

 

조회 확인

@Test
@DisplayName("soft delete 조회 테스트")
public void softDeleteSelectTest() {
	saveMeetingData();
	meetingSoftRepository.deleteAll();
	List<MeetingSoftEntity> meetingSoftEntities = meetingSoftRepository.findAll();
	
    	Assertions.assertTrue(meetingSoftEntities.isEmpty(), "SoftDelete된 엔티티는 조회 X");
}

 

SQL을 보면 findAll()을 했음에도 deleted = 0인걸 자동으로 조회하는걸 확인할 수 있습니다.


연관관계 SoftDelete

연관관계도 SoftDelete 되는지 확인하기 위해서 아래와 같이 엔티티를 만들어보겠습니다.

@Entity
@SoftDelete
public class MeetingSoftEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String meetingName;

    @OneToMany(mappedBy = "meeting", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<ParticipantSoftEntity> participants = new ArrayList<>();
}

@Entity
public class ParticipantSoftEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @ManyToOne
    @JoinColumn(name = "meeting_id")
    private MeetingSoftEntity meeting;
}

 

이렇게 만든 후 모임을 삭제하면 아래와 같은 SQL이 보입니다.

 

모임 테이블는 update 구문이 실행되지만 연관관계인 참가자 테이블에는 delete 구문이 실행되는걸 확인할 수 있습니다.

연관관계 엔티티에도 soft delete를 하고 싶다면 해당 엔티티에도 @SoftDelete 어노테이션을 붙여주면 됩니다.

 

@SoftDelete 어노테이션을 붙이고 다시 실행해보면 위처럼 두 테이블 모두 update 구문이 나오는걸 확인할 수 있습니다.

단, @SoftDelete를 적용했을 때는 To-One 관계에 대한 fetch 전략을 LAZY로 설정할 수 없습니다.

그래서 항상 EAGER를 사용해야해서 성능이슈가 있을 수 있습니다.


번외. LocalDateTime 사용하기

deleted를 이용해 true/false로 삭제여부를 저장하는것도 좋지만

LocalDateTime을 사용해서 Null로 삭제 여부를 판단하는 것이 실무적으로는 더 도움이 될 수 있습니다.

그래서 LocalDateTime을 사용해서 SoftDelete를 구현해보겠습니다.

 

@SoftDelete

Hibernate JavaDoc에서 @SoftDelete를 보면

 

converter라는 속성이 있는것을 볼 수 있습니다.

이 속성을 사용해서 DB에 저장되는 값을 변환하기 위해 사용할 AttributeConverter를 지정할 수 있습니다.

 

저희는 LocalDateTime을 사용할 것 이기때문에 커스텀 컨버터를 만들어주겠습니다.

@Converter
public class DeletedAtConverter implements AttributeConverter<Boolean, LocalDateTime> {

    // Boolean => DB 변환
    @Override
    public LocalDateTime convertToDatabaseColumn(Boolean deleted) {
        if(deleted != null && deleted) {
            return LocalDateTime.now();
        }
        return null;
    }

    // DB => Boolean 변환
    @Override
    public Boolean convertToEntityAttribute(LocalDateTime deletedAt) {
        return deletedAt != null;
    }
}

 

이렇게 컨버터를 만들고 엔티티의 @SoftDelete 속성도 설정해줍니다.

@Entity
@SoftDelete(
        converter = DeletedAtConverter.class,
        columnName = "deleted_at")
public class MeetingSoftEntity { ... }

 

아마 이렇게하면 에러가 발생할 것 입니다.

이는 Hibernate JavaDoc를 보면 확인할 수 있습니다.

위에 converter 속성에 대한 API Note를 보면 null을 절대 리턴하면 안된다고 나와있어서 당연하게도 에러가 발생합니다.

하지만, 보통 LocalDateTime을 사용해서 soft delete를 할 때는 해당값을 null로 두고 판단합니다.

그래서 @SoftDelete를 사용해서 LocalDateTime을 이용하려면 "꼼수로 임의의 시간대를 설정하고 해당 값이면 삭제되지 않은것"

이렇게 구현을 해야합니다.

이 방식은 깔끔하지 않고 실수가 나올 수 있어 추천하지 않습니다.

따라서, 사실상 @SoftDelete으로 LocalDateTime을 이용하는건 무리가 있다고 봐도 무방할 것 같습니다.

 

@Where, @SQLDelete 적용

@SQLDelete는 엔티티 삭제시 실행되는 SQL을 직접 지정할 수 있는 어노테이션 입니다.

@Where 엔티티 조회시 항상 사용할 전역 Where 조건을 지정하는 어노테이션 입니다.

위 2개의 어노테이션을 이용해서 LocalDateTime으로 Soft Delete를 구현해보겠습니다.

 

방법은 간단합니다. 아래와 같이 어노테이션을 붙여주고 deleteAt 필드를 추가해주면 됩니다.

@Entity
@SQLDelete(sql = "UPDATE meeting_deleted_at_entity SET deleted_at = NOW() WHERE id = ?")
@Where(clause = "deleted_at IS NULL")
public class MeetingDeletedAtEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String meetingName;

    private LocalDateTime deletedAt;
}

 

그래서 아래와 같이 테스트를 만들어서 실행해보면

    @Test
    void Where과_SQLDelete_어노테이션_적용_확인() {
        LocalDateTime_테스트용_데이터_저장();

        meetingDeletedAtRepository.deleteAll();

        List<MeetingDeletedAtEntity> softList = meetingDeletedAtRepository.findAll();
        assertThat(softList).isEmpty();

        List<MeetingDeletedAtEntity> softList2 = meetingDeletedAtRepository.findAll();
        assertThat(softList2).isEmpty();

        List<MeetingDeletedAtEntity> softList3 = meetingDeletedAtRepository.findAllWithDeleted();
        assertThat(softList3).isNotEmpty();
    }

 

삭제시에 지정한대로 Update 구문이 실행되고 조회시에는 deletea_at IS NULL로 조건을 걸어서 조회하는 것을 확인할 수 있습니다.


https://docs.jboss.org/hibernate/orm/7.1/javadocs/org/hibernate/annotations/SoftDelete.html

https://docs.jboss.org/hibernate/orm/6.6/javadocs/org/hibernate/annotations/SQLDelete.html

https://docs.jboss.org/hibernate/orm/6.6/javadocs/org/hibernate/annotations/Where.html