본문 바로가기

MY개발생각

[개발생각] JPA에서 soft delete 관리하기

JPA에서 soft delete관리를 어떻게 해야하나를 고민했었는데.. 방법이 있었네 ㅠ 

 

진행하는 프로젝트에서 jpa에서 row delete를 할때, soft delete방법을 사용하고 있다.

delete_dtm필드가 null인지 아닌지에 따라 삭제여부를 판단하여 사용하는것으로,

delete로 실제 row를 삭제하지 않는 방법을 사용한다.

(이부분은 여러 장점이 있기에 이렇게 하기로 결정했다.)

 

그런데 하나 귀찮은점이 있다.

바로 항상 조회시 deleteDtm is null을 붙여야 실제 삭제되지 않은 값을 가져와서 사용할수있기에,

아래 JpaRepository와 같이 항상 조건에 DeleteDtmIsNull스펙을 넣어야한다.

 

fun findAllByDeleteDtmIsNull() : List<User>

fun findOneByUserIdAndDeleteDtmIsNull(userId:UserId) : User

 

비즈니스로직에서는 삭제되지않은 라이브 데이터를 사용하기에 거의 모든 스펙에서 위와 같은 "DeleteDtmIsNull"가 포함된다.

이걸 프로젝트에서 매번 넣어주게해서 사용했는데..

 

쉽게 이 부분을 관리할수있는 기능이 따로 있어서 정리를 한다.

 

@Entity
@SQLRestriction("deleteDtm is null")
@SQLDelete(sql = """
    UPDATE users
    SET deleteDtm = now()
    WHERE id = ?
""")
class User {
   @Id
   val id:Long;
   val deleteDtm:LocalDateTime;
}



val users = userRepository.findAll();

userRepository.delete(user);

 

findAll하면 자동으로 deleteDtm is null 조건이 붙는다. (엄청 편하다!!!)

delete기본 함수를 호출하면, 실제 row삭제대신 deleteDtm이 업데이트 된다 (@SQLDelete)

 

위 방법을 사용하면, 훨씬 간결하게 jpa를 관리할수있다.

 

 

아래는 알아두면 유용한 몇가지 정리해 본다.

 

readOnly전용 Jpa인경우는 아래와 같이 @QueryHint를 추가하면, DirthCheck를 하지않아 성능상의 이점을 가져갈수있다.

@QueryHints(
    @QueryHint(
       name = "org.hibernate.readOnly",
       value = "true"
    )
)
fun findAllReadOnly():List<User>

 

 

그리고 가장 중요한것, 어드민만들때 보면, 조회시 여러 조회조건을 통해, 쿼리를 수행해야하는 경우가 있다.

이경우, 쿼리가 종류별로 나오거나, 다이나믹 쿼리를 사용하거나 하는데, 상당히 유지보수에서 좋지가 않다.

 

이런경우, Specification을 사용하면, 훨씬 동적 쿼리를 관리할수있게 기능을 만들수있다.

interface UserRepository :
    JpaRepository<User, Long>,
    JpaSpecificationExecutor<User>
    

object UserSpecs {

    fun ageGreaterThan(age: Int?): Specification<User> {
        return Specification { root, _, cb ->
            age?.let {
                cb.greaterThan(root.get("age"), it)
            }
        }
    }

    fun nameLike(name: String?): Specification<User> {
        return Specification { root, _, cb ->
            if (name.isNullOrBlank()) {
                null
            } else {
                cb.like(root.get("name"), "%$name%")
            }
        }
    }
}


val spec =
    Specification.where(UserSpecs.ageGreaterThan(20))
        .and(UserSpecs.nameLike("kim"))

val users = userRepository.findAll(spec)

이렇게 spec을 통해, 조회 조건을 동적으로 구성할수있고, 해당 조건을 쿼리문이 아닌,

코드베이스로 관리할수있어서,

재사용및 유지보수가 띄어나게 된다. (이 기능은 사실 미쳤다.. )

이 기능은 나도 잘사용해보지 않은것이라, 어드민등에 조건이 많은경우에 적용하면 참 유용하겠다 :)

 

물론, 가장 강력하다고 생각하는건 QueryDsl (또는 Kotlin JDSL)을 사용하는게 가장 강력한 동적쿼리 사용법이겠지만,

간단한 경우나 좀더 커스텀을 하고싶은경우에 따라서는 Spec구현 방식이 더 깔끔할수도 있다고 생각된다.

(결국, 용도에 따라 적절하게 사용하는것으로 마무리..ㅎ)

 

그나저나, 프로젝트에 @Query에 JPQL쿼리가 너무 많이 들어가있는데..

특히 DTO로 프로젝션하는 쿼리들, 조인쿼리들...관리가 어려울듯한뎅;;

 

Kotlin JDSL로 한번 바꾸긴해야겠다...

(JDSL사용하면, 정말 깔끔하지긴 할듯하다!! - 클래스패키지 정의 없어지는것만 해도 ㅠ)

 

Kotlin JDSL가 장점이 많아서, 안쓸이유가 없으니..

(마치 코틀린 한번 사용하면하면, 자바 못쓰는 느낌?!)

 

틈나는데로 적용을 해봐야겠다. (고칠부분이 많겠구나..ㅠㅠ)