프로젝트를 하다 보면,
다른 엔티티의 상태 변화에 따라 특정 값을 함께 갱신해야 하는 요구사항을 종종 마주하게 됩니다.
예를 들어 리뷰가 등록되면 Book의 reviewCount
를,
댓글이 달리면 Review의 commentCount
를 함께 갱신해야 하는 식이죠.
이번 글은 코드잇 스프링 부트캠프에서 실제로 멘토링 중 나왔던 이와 같은 고민에서 출발해,
JPQL UPDATE와 락 전략, 그리고 동시성 문제에 대해 정리해보았습니다.
reviewCount
는 리뷰가 생성될 때마다 어떻게 갱신할까?commentCount
는 댓글이 생성될 때마다 어떻게 관리해야 할까?네모님은 다음 세 가지 방식 사이에서 동시성과 정합성 측면에서 어떤 전략이 최선일지 깊이 고민하셨습니다:
book.reviewCount++
SELECT COUNT(*) FROM Review WHERE book_id = ?
로 매번 다시 세기 그중 네모님은 3번 방식, 즉 JPQL UPDATE 쿼리로 reviewCount와 rating을 함께 갱신하는 방식을 선택하셨어요.
구체적으로 락이나 JPQL의 동작 방식에 대해 언급하시진 않았지만,
그 선택만으로도 실무적인 고민이 엿보였기 때문에
👉 이 글에서는 그 흐름을 따라
락(Lock), JPQL UPDATE의 특성,
그리고 비관적 락과 낙관적 락 중 어떤 전략을 선택할 수 있는지까지 함께 정리해보고자 합니다.
@Modifying
@Query("""
UPDATE Book b
SET
b.reviewCount = (SELECT COUNT(r) FROM Review r WHERE r.book.id = :bookId),
b.rating = (SELECT COALESCE(AVG(r.rating), 0) FROM Review r WHERE r.book.id = :bookId)
WHERE b.id = :bookId
""")
void recalcStats(@Param("bookId") UUID bookId);
@Modifying
과 함께 JPQL UPDATE 사용그치만 ..!!!
JPQL UPDATE는 JPA의 영속성 컨텍스트를 무시하고 DB에 바로 반영되기 때문에,
같은 트랜잭션 내에서 조회 결과가 최신 값과 불일치할 수 있습니다.
이 문제를 방지하려면 @Modifying(clearAutomatically = true)
옵션을 반드시 사용하는 것이 좋아요.
@Modifying(clearAutomatically = true)
@Query("UPDATE Book b SET b.reviewCount = :count WHERE b.id = :bookId")
void updateCount(@Param("count") long count, @Param("bookId") UUID bookId);
JPQL로 직접 UPDATE를 수행하면, JPA는 해당 row에 대해 쓰기 락(write lock)을 요청해요.
이는 DB 차원에서 다른 트랜잭션의 접근을 차단하는 비관적 락(Pessimistic Lock)의 대표적인 예시입니다.
구분 | 비관적 락 (Pessimistic) | 낙관적 락 (Optimistic) |
---|---|---|
전략 | "다른 트랜잭션이 수정할 수도 있으니, 미리 잠그자"(부정핑) | "설마 동시에 수정하겠어? 나중에 충돌 검사하자"(긍정?핑) |
구현 방식 | DB 레벨 락 (예: SELECT FOR UPDATE, JPQL UPDATE) | 버전 필드(@Version ) 기반 충돌 체크 |
사용 예 | JPQL UPDATE, SELECT FOR UPDATE | 엔티티 merge 시 버전 체크 |
장점 | 정합성 강력 보장 | 성능 우수, 락 없음 |
단점 | 성능 저하, 블로킹 발생 가능 | 충돌 시 예외, 로직 복잡도 증가 |
graph TD
A[리뷰 생성 요청] --> B{락 전략 선택}
B -->|비관적 락| C[JPQL UPDATE → row 잠금]
B -->|낙관적 락| D[@Version 기반 충돌 검사]
C --> E[동시 수정 방지, 성능 부담]
D --> F[충돌 시 예외 발생, 성능 우위]
리뷰 생성 시 book.reviewCount
와 rating
을 어떻게 갱신할지 고민하던 네모님은,
JPQL UPDATE 쿼리를 통해 두 값을 한 번에 갱신하는 방법을 선택하셨어요.
이 방식이 야무졌던 이유는 다음과 같아요:
++
연산은 동시성 이슈에 쉽게 노출될 수 있기 때문이죠.복잡한 조건을 고려한 결정이라기보단, 현실적인 상황에서 자연스럽게 나온 선택이었지만,
결과적으로는 실무에서도 자주 사용하는 안전한 방식과 맞닿아 있었습니다.
이 선택에 대해서는 9팀 멘토링 시간에 직접 들을 수 있었고,
그때 떠올랐던 기술적 배경과 함께 알아두면 좋을 내용을
큐레이션 형식으로 정리해보면 좋겠다는 생각에 이 글을 작성하게 되었습니다.
한편, 꼭 JPQL UPDATE를 쓰지 않아도 JPA에서 비관적 락을 명시적으로 걸 수 있는 방법이 있습니다.
예를 들어 findById
와 함께 PESSIMISTIC_WRITE
를 사용하면,
JPQL 없이도 아래처럼 트랜잭션 레벨에서 락을 적용할 수 있어요:
Book book = em.find(Book.class, bookId, LockModeType.PESSIMISTIC_WRITE);
// 이후 book.reviewCount = ... 로직 수행
이 방식은 DB 락을 안전하게 걸면서도 엔티티를 수정하는 흐름을 그대로 유지할 수 있어,
JPQL UPDATE보다 더 유연하고 타입 안전한 구조로 이어질 수 있습니다.
→ 관련 예제 보기
reviewCount++
처럼 단순히 값을 증가시키는 방식은