사이드 프로젝트 진행을 진행하면서 ORM 기술로 QueryDSL을 사용하고 있었다. 그리고 장바구니 목록 삭제 구현 중 QueryDSL은 UPDATE, DELETE 쿼리에 대해서 이슈가 존재한다는 것을 알게되었고 이를 정리해보고자한다.
문제의 상황은 장바구니 목록을 삭제하기 위한 쿼리였다. 이때, 사용자들의 장바구니는 향후 마케팅이나 데이터 분석 쪽에서 사용할 수 있다는 점을 고려해서 실제로 데이터를 논리적으로 삭제하는 방식인 Soft Delete 방식을 이용했다.
그래서 장바구니를 삭제하는 것은 실제로 데이터베이스 내에서 삭제하는 것이날 삭제 Flag 필드를 통해서 삭제된 데이터인지 식별하였고, 코드는 아래와 같다.
public void deleteCartItem(Long userId, Long cartItem) {
queryFactory
.update(cartItem)
.set(cartItem.isDeleted, true)
.where(
cartItem.userId.eq(userId),
cartItem.id.eq(cartItem),
cartItem.isDeleted.isFalse()
)
.execute();
}
사실.. 원래대로라면.. 삭제 시 정합성을 유지하기 위해서 find...() 메서드로 조회 후 삭제를 해야하지만.. 코드를 작성할 때 약식으로 기능 구현했던 점은 .. 비밀이다...
실제로 데이터베이스에서 삭제하는 것이 아니라 삭제 여부 필드를 변경해주는 것이기 때문에 UPDATE 쿼리를 이용한다.
코드를 실행하면 난 당연히 정상적으로 업데이트가 될 줄 알았지만 아래와 같은 오류를 만났다...
org.springframework.dao.InvalidDataAccessApiUsageException: Executing an update/delete query
자료를 찾아보니 QueryDsl에서 Update와 Delete쿼리는 데이터 베이스에 바로 반영을한다고 한다. 즉, JPA 영속성 컨텍스트 내부 1차 캐시에 반영하지 않는다.
그래서 JPA와 flush()와 충돌이 날 수 있어서 에러가 나는 것이다.
이해를 위해서 flush()
부터 알아보자.
flush()
: flush()는 1차 캐시에 저장된 정보를 데이터베이스에 반영한다. (커밋은 안한다. 즉, 롤백 될 수 있다는 말이다.)그리고 트랜잭션 범위가 끝나면 커밋이 진행되는 것이다.
근데 가장 중요한 것은 commit()
시점에 JPA가 예상한 데이터와 실제 DB 데이터가 다르면 내부적으로 정합성 불일치로 판단해서 InvalidDataAccessApiUsageException
을 발생시킨다.
다시 정리해보자.
1. QueryDSL delete, update 쿼리는 JPA 영속성 컨텍스트에 반영하지 않는다. -> 바로 데이터베이스에 반영 (커밋안한상태)
2. 트랜잭션이 종료 시 호출되는 flush()는 1차 캐시가 없어서 아무 일도 안한다. 근데 JPA는 커밋을 해도 데이터 변경이 없을거라고 생각
3. commit() 호출 시 예상과 다르기 대문에 정합성 오류로 판단하고InvalidDataAccessApiUsageException
을 발생
QueryDSL의 update/delete 쿼리는 JPA의 1차 캐시에 반영하지 않는 것을 알게되었다. 따라서 조회의 경우 혹은 객체지향적으로 사용하기 위해서 QueryDSL을 사용하는 것이 좋다는 생각이 들었다...