우당탕탕

[동시성 제어 2편] 비관적 락(Pessimistic Lock) - JPA 스프링으로 경험해보는 실전 가이드 본문

Tech/Spring

[동시성 제어 2편] 비관적 락(Pessimistic Lock) - JPA 스프링으로 경험해보는 실전 가이드

모찌모찝 2025. 8. 5. 08:00
[동시성 2편] 비관적 락(Pessimistic Lock)

 

안녕하세요!
이전 편에서는 "동시성 제어란?" 이란 내용으로 동시성이 무엇인지 알아보았습니다. 
이제 본격적으로 첫 번째 실전 대책, 비관적 락(Pessimistic Lock)에 대해 알아보도록 하겠습니다.

이전 편 보러 가기

[동시성] 동시성 제어란? - 1편 (데이터가 꼬이지 않는 백엔드의 첫걸음)

동시성문제 : 출처:https://blog.naver.com/chgy2131/223374232041

1. 비관적 락이란? 

  • 말 그대로 "충돌이 난다고 미리 가정"하고, 데이터를 사용하는 동안 다른 트랜잭션의 접근 자체를 막는 방식입니다.
  • 트랜잭션이 데이터를 읽거나 변경하는 동안 DB가 자동으로 행(row) 또는 테이블 전체에 락을 건다   
        → 데이터 일관성 100% 보장(속도보다 ‘정확’ 우선)
        → 다른 트랜잭션은 내 작업이 끝날 때까지 ‘대기’ 또는 ‘타임아웃/예외’

예를 들어, 은행 계좌 이체, 재고 차감처럼 “누가 먼저 건드리나”에 따라 결과가 달라지면 곤란한 데이터에 필수입니다

2. 비관적 락이 필요한 예시

  • 재고 감소:
        →
    여러 사람이 동시에 마지막 상품을 주문할 때, 실제로 1명만 성공!
  • 적립금·포인트 차감, 계좌 이체: 
       
    → 중복 차감, 이중 지급 같은 오류 완벽 차단 
        → 정말 중요한 데이터(돈, 수량)는 ‘비관적 락’이 기본 방패막

3. JPA로 비관적 락 해보기

1. JPA의 @Lock 어노테이션 + PESSIMISTIC_WRITE

// 예제 엔티티
@Entity
public class Stock {
    @Id @GeneratedValue
    private Long id;
    private Integer quantity;
    // ... getter, setter 등
}

// Repository
public interface StockRepository extends JpaRepository<Stock, Long> {
    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @Query("select s from Stock s where s.id = :id")
    Stock findByIdForUpdate(@Param("id") Long id);
}

2. 서비스에서 실제 락 걸기

@Service
public class StockService {
    @Autowired
    private StockRepository stockRepository;

    // 트랜잭션 안에서 락 + 처리
    @Transactional
    public void decrease(Long stockId, int amount) {
        Stock stock = stockRepository.findByIdForUpdate(stockId); // 락 걸림!
        if (stock.getQuantity() < amount) throw new RuntimeException("재고 부족");
        stock.setQuantity(stock.getQuantity() - amount);
        // 트랜잭션 커밋 시 DB에서 락 해제
    }
}

3. Native Query로 락 걸기

@Query(value = "select * from stock where id = :id for update", nativeQuery = true)
Stock nativeLock(@Param("id") Long id);

실제로는 SELECT ~~~ FOR UPDATE 쿼리가 나가며, 원하는 행에 락을 걸게 됩니다.

4. 락 사용 시 주의점

  • 트랜잭션은 무조건 짧게!
        →
     락 걸린 상태로 오래 유지하면 → 경합·데드락 위험 ⬆
  • LockTimeoutException 대비
        →
    경합이 심하면 DB가 자동으로 예외 던짐 → 타임아웃/롤백 처리 필요
  • 낙관적 락보다 쓰기 작업 빈도가 높을 때, 충돌 가능성이 높을 때 ‘비관적 락’이 더 안전
  • 읽기 작업(조회 위주)에는 가급적 피하기 ☝️
        → 
    동시성이 너무나 떨어지기 때문에 꼭 필요한 순간에만 걸 것!

5. 장/단점 요약

장점 단점
데이터 일관성 100% 보장 락 경합 많으면 성능 하락 가능성
정말 중요한 값은 항상 보호 트랜잭션 길어지면 데드락/지연 발생
구현이 너무 쉬움 (어노테이션 1줄) 읽기 작업까지 걸면 서버 막힘 주의


실무에서는 "이중 지불", "재고 처리" 등과 같은 중복처리가 치명적인 서비스에서 많이 사용하며, 쿠폰 한정 수량 이벤트, 콘서트 예약과 같은 서비스에서 많이 사용되곤 합니다.

다음 편에서는 "낙관적 락(Optimistic Lock)"@Version, 버전 충돌 검증 방식과 함께 다뤄볼 예정입니다. 궁금한 점이 있다면 댓글로 남겨주세요! 

Comments