Notice
Recent Posts
Recent Comments
Link
«   2025/03   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31
Archives
Today
Total
관리 메뉴

차곡차곡 쌓아가는 개발일기

벌크 연산을 사용해본 후기 본문

Spring/QueryDSL

벌크 연산을 사용해본 후기

KimTaeO 2023. 6. 20. 15:18

어느날 Kotlin 스프링 부트 서버를 유지보수하던 중이었다
그러다 새로운 API 개발을 담당하게 되었는데 내용을 들어 보니 DB에 있는 데이터들의 일괄 삭제 API가 필요한 상황이 생긴 것이었다
내용은 다음과 같았는데 OAuth 서비스에서 로그인된 상태로 로그인 한 사람이 등록해놓은 서비스들을 삭제하는 API였다

처음 코드는 DeleteByIdsAndCreatedBy(User)가 내가 사용하는 스프링 버전에서 지원하지 않아 다음과 같이 작성했었다

@Service
@Transactional(rollbackfor = [Exception::class])
class deleteClientsService(
    private val clientRepository: ClientRepository
) {
    fun execute(ids: List<Long>) {
        ids.map{ clientRepository.deleteById(it) }
    }
}

하지만 이 방식은 select 쿼리를 날리고 delete 쿼리를 날리기를 ids변수의 인덱스 개수만큼 반복하기 때문에 별로 효율적이지 않은 쿼리이다

QueryDSL을 사용하여 커스텀 쿼리 만들기

따라서 QueryDSL을 통하여 커스텀 쿼리를 만들게 되었는데

@Repository
class CustomClientRepositoryImpl(
    private val jpaQueryFactory: JPAQueryFactory
): CustomClientRepository {
    override fun deleteAllByIdsAndCreatedBy(ids: List<Long>, createdBy: User) {
        jpaQueryFactory.delete(client)
            .where(client.id.`in`(ids), client.createdBy.eq(createdBy))
            .execute()
    }
}

이렇게 하면 select 쿼리문 한번과 delete 벌크 연산을 한번 하게 된다

이때, 여기서 벌크 연산이란 여러 개의 데이터를 한번에 처리하는 연산이라 한다

하지만 벌크 연산에도 치명적인 단점이 있는데
JPA의 영속성 컨텍스트의 1차캐시를 무시하고 쿼리를 날리기 때문에 DB와 영속성 컨텍스트의 캐시의 정보가 다를 수 있다
이는 곧 데이터베이스의 무결성이 깨지는 것이다

이를 해결하기 위해서는 영속성 컨텍스트를 관리하는 EntityManager를 통해 벌크연산 직후에 refresh()를 호출하여 영속성 컨텍스트를 새로고침 하거나
영속성 컨텍스트에 있는 엔티티를 삭제하여 DB의 조회쿼리가 날아가도록 하면 된다