무심코 저지르는 습관, 트랜잭션 순서가 성능을 망친다

궁금하면 500원·2025년 6월 13일

데이터 저장하기

목록 보기
18/23

트랜잭션과 락 보유 시간의 관계

관계형 데이터베이스는 트랜잭션의 격리성(Isolation)을 보장하기 위해 데이터에 락을 걸어 다른 트랜잭션이 접근하지 못하도록 합니다.

  1. update 재고 -1이 먼저 실행되는 경우

    • 트랜잭션 시작 직후, 재고 테이블의 특정 로우(예: product_id = 123인 로우)에 배타적 락(Exclusive Lock)이 걸립니다.
    • 이후 2번부터 9번까지의 모든 작업이 끝날 때까지 락은 풀리지 않습니다.
    • 만약 다른 여러 트랜잭션이 동시에 같은 상품의 재고를 수정하려고 한다면, 이들은 락이 풀릴 때까지 대기(waiting)하게 됩니다.
    • 따라서 락이 유지되는 시간이 길어질수록, 대기하는 트랜잭션이 늘어나고 동시성 처리 성능이 급격히 저하됩니다.
  2. update 재고 -1이 마지막에 실행되는 경우

    • 트랜잭션 시작 후 2번부터 9번까지의 작업이 먼저 실행됩니다.
      이 작업들은 락을 걸지 않거나, 다른 테이블에 락을 걸 수 있으므로 재고 로우에 대한
      락은 아직 걸리지 않습니다.
    • 9번에서 재고를 업데이트할 때 비로소 락이 걸립니다.
    • 10번 커밋이 즉시 이뤄지므로, 락이 걸려 있는 시간이 매우 짧습니다.
    • 결과적으로 다른 트랜잭션들이 재고를 수정하기 위해 대기하는 시간이 짧아지고, 동시성 처리 성능이 크게 향상됩니다.

특히 여러 트랜잭션이 같은 리소스(여기서는 특정 재고 로우)에 집중될 때 이 순서의 차이는 수십 배의 성능 차이로 나타날 수 있습니다.

JPA와 영속성 컨텍스트

JPA는 말씀하신 대로 영속성 컨텍스트의 더티 체킹(Dirty Checking)을 활용합니다. 엔티티의 변경 사항은 즉시 DB에 반영되지 않고, 트랜잭션이 커밋될 때(또는 flush() 호출 시) 한 번에 모아서 SQL을 실행합니다.

따라서 JPA 환경에서는 코드로 product.setStock(product.getStock() - 1)를 호출하더라도, 실제로 UPDATE 쿼리는 트랜잭션 커밋 직전에 실행됩니다. 이는 의도치 않게 재고 업데이트가 트랜잭션의 마지막 순서로 배치되는 효과를 가져오므로, JPA를 사용하면 자연스럽게 락 보유 시간을 최소화하는 방식이 됩니다.
하지만 이 동작 방식을 이해하지 못하면 왜 이런 성능 차이가 발생하는지 파악하기 어려울 수 있습니다.

profile
에러가 나도 괜찮아 — 그건 내가 배우고 있다는 증거야.

0개의 댓글