개발노트

[Spring Boot] Scheduler로 Soft Delete 데이터 삭제

Happy._. 2024. 6. 21. 23:50
반응형

Todo 프로젝트에서 Soft Delete를 적용해 논리적으로 데이터가 삭제된 것처럼 구현하였다.

실제 데이터가 삭제된 것은 아니기 때문에 불필요한 데이터가 계속 쌓이게 된다.

불필요한 데이터를 계속 저장해 둘 필요는 없기 때문에 주기적으로 삭제 처리된 데이터를 지우려고 한다.

이런 경우 Spring Scheduler를 사용하면 정해진 시간마다 삭제 작업을 처리할 수 있다.

 

다음과 같이 @EnableScheduling 어노테이션을 추가한다.

import org.springframework.scheduling.annotation.EnableScheduling

@EnableScheduling // 스케줄링 활성화
@SpringBootApplication
class TodolistApplication

fun main(args: Array<String>) {
    runApplication<TodolistApplication>(*args)
}

 

Scheduler 작업을 모아둘 클래스를 생성해 다음과 같이 정상 동작하는지 테스트할 코드를 작성한다.

import org.springframework.scheduling.annotation.Scheduled

@Component
class ScheduledDeletionTasks {

    private val logger = LoggerFactory.getLogger("ScheduledDeletionTasks")

    @Scheduled(fixedRate = 5000) // 메서드 호출 간격
    fun reportCurrentTime() {
        logger.info("The time is now {}", LocalDateTime.now())
    }
}

 

@Scheduled 어노테이션 속성

  • fixedRate: 이전 작업 실행 여부와 관계없이 지정된 밀리초(ms)마다 실행
    • 작업의 실행이 독립적일 때 유용
    • 작업이 빨리 완료되지 않으면 메모리 부족 예외 발생
  • cron: "초 분 시 일 월 요일"순으로 지정 → 초, 분: 0 ~ 59 / 시간: 0 ~ 23 / 일: 1 ~ 31 / 월: 1 ~ 12 / 요일: 0 ~ 7
  • fixedDelay: 작업 실행 완료 시간과 다음 작업 실행 시간 사이에 밀리초의 지연을 두고 실행
    • 항상 하나의 작업 인스턴스만 실행되도록 해야 할 때 유용
  • initialDelay: 초기 지연 시간 설정(일회성 작업)

다음은 위 코드의 실행 결과로 5초마다 현재 날짜와 시간이 로그로 출력된다.

 

Scheduler가 정상적으로 실행되는 것을 확인했으니 실제 필요한 작업에 대한 코드를 작성한다.

@Component
class ScheduledDeletionTasks(
    private val todoRepository: TodoRepository
) {
    @StopWatch
    @Scheduled(cron = "0 0/5 * * * *") // 5분마다 실행
    fun reportCurrentTime() {
        todoRepository.deleteByIsDeletedTrue()
    }
}

 

서버를 실행시킨 후 scheduler가 수행되는지 확인하는데 where절에 is_deleted 컬럼에 대한 조건이 두 개나 들어있는 것을 볼 수 있었다.

 

이 이상한 쿼리가 발생한 이유는 Entity 클래스에 다음과 같이 어노테이션이 적용되어 있기 때문이다.

@Entity
@SQLRestriction("is_deleted = false") // org.hibernate.annotations.SQLRestriction
@SQLDelete(sql = "UPDATE todo SET is_deleted = true WHERE id = ?") // delete 쿼리 수행 시 update 처리
class Todo private constructor(
    // ...
) : BaseEntity() {
    // ...
}

 

@SQLRestriction 어노테이션에 설정한 is_deleted 컬럼의 값이 false인 조건과 삭제를 위해 데이터를 조회할 때 is_deleted 컬럼의 값이 true인 데이터를 찾으면서 같은 컬럼에 대해 true, false 조건이 모두 들어가게 된 것이다.

 

이 문제를 해결하기 위해서는 @SQLRestriction 어노테이션을 제거하고 각 조회 쿼리에 is_deleted 컬럼의 값이 false인 데이터만 필터링하는 조건을 설정해야 한다.

그 이유는 @SQLRestriction 어노테이션을 적용하면 해당 Entity를 조회하는 모든 쿼리에 where 절이 추가되기 때문이다.

JPQL, QueryDSL에도 전부 반영돼서 삭제 쿼리를 실행할 때도 where 절에 @SQLRestriction 어노테이션에 적용한 is_deleted = false가 추가되어 데이터를 삭제할 수 없게 된다.

 

이유를 알았으니 이제 코드를 수정한다.

Entity 클래스는 다음과 같이 @SQLRestriction 어노테이션을 제거한다.

@Entity
@SQLDelete(sql = "UPDATE todo SET is_deleted = true WHERE id = ?") // delete 쿼리 수행 시 update 처리
class Todo private constructor(
    // ...
) : BaseEntity() {
    // ...
}

 

전체 조회에 대한 QueryDSL은 다음처럼 where 조건에 isDeleted가 false인 값만 필터링 하도록 작성한다.

queryFactory.selectFrom(todo)
    .where(todo.isDeleted.isFalse.and(where))
    .offset(pageable.offset)
    .limit(pageable.pageSize.toLong())
    .fetch()

 

개별 조회는 쿼리 메서드 방식으로 작성했다.

interface TodoRepository : JpaRepository<Todo, Long>, CustomTodoRepository {

    fun findByIdAndIsDeleted(id: Long, isDeleted: Boolean): Todo?
}

 

코드를 수정한 후 서버를 재시작해서 Scheduler가 실행되는 것을 보면 삭제 쿼리가 정상적으로 수행되고 있고 실제 DB에서도 데이터가 삭제된 것을 볼 수 있다.

삭제 테스트를 위해 설정했던 시간(5분)만 실제 적용할 시간으로 변경하면 된다.

 

 

참고문서 : https://spring.io/guides/gs/scheduling-tasks

반응형