[JPA] 낙관적 락(Optimistic Lock) VS 비관적 락(Pessimistic Lock)

Sadie·2025년 4월 2일
1

Spring And JPA

목록 보기
12/12

문제 상황

주식을 주제로 한 프로젝트를 진행하던 중, 사용자가 여러 종목에 대해 자동매매를 동시에 실행할 때 동시성 문제와 정합성 문제가 발생하는 상황을 확인했습니다.


현재 Member 테이블에는 사용자의 자산 정보가, Account 테이블에는 현재 보유 중인 주식 정보가 저장되어 있습니다.

그런데 여러 스레드(또는 요청)가 동시에 주문을 넣거나 체결을 처리하게 되면, 다음과 같은 문제가 생겼습니다.


예를 들어,

if (member.getMoney() >= 주문금액 && account.getStockCnt() >= 주문수량) {
    // 주문 처리
}

member.getMoney()와 account.getStockCnt()는 서로 다른 테이블에서 각각의 데이터를 조회하게 되는데,
이 과정이 동일한 트랜잭션 내에서 일관성 있게 보장되지 않으면 다음과 같은 상황이 벌어질 수 있습니다.

예를 들어, A 스레드가 두 값을 각각 조회해 조건을 만족한다고 판단한 직후, B 스레드가 먼저 주문을 체결하면서 자산 또는 주식 수량을 변경해버리는 경우입니다.
하지만 A 스레드는 여전히 "조건을 만족한다"고 착각하고 주문을 처리하게 되어서, 실제 자산보다 많은 주문이 체결되거나, 보유하지 않은 주식을 매도하는 상황이 발생할 수 있습니다.


해결방안

이런 동시성 문제를 막기 위해 처음에는 @Version을 활용한 낙관적 락(Optimistic Lock) 을 사용해 문제를 해결하려고 했지만,

자동매매처럼 트래픽이 빈번한 환경에서는 충돌이 자주 발생하고 롤백이 많아지는 문제가 있었고, 결국 비관적 락(Pessimistic Lock) 으로의 전환을 검토하게 되었습니다.



낙관적 락

낙관적 락은 말 그대로 "충돌이 거의 없을 것"이라고 낙관적으로 가정하는 락 방식입니다.
트랜잭션 간의 충돌 가능성이 낮다고 보고, 일단 데이터를 자유롭게 읽고 수정한 뒤 커밋 시점에 충돌 여부를 검사합니다.

JPA에서는 @Version 어노테이션을 사용해서 구현합니다.
이 어노테이션이 붙은 필드는 트랜잭션이 커밋될 때 자동으로 버전 체크가 수행됩니다.

데이터가 업데이트될 때마다 @Version 필드 값이 1씩 증가하는 구조입니다.


동작 방식

  1. 트랜잭션 시작 시, DB에서 데이터를 읽어올 때 version 값을 함께 읽음
  2. 데이터를 수정하고 커밋할 때, UPDATE 쿼리에서 WHERE version = ? 조건으로 실행
  3. 만약 트랜잭션 동안 다른 누군가가 해당 데이터를 수정했다면 version 값이 바뀌므로, 쿼리가 적용되지 않음 → OptimisticLockException 발생 → 롤백 필요

JPA 사용 예시

@Entity
public class Member {
    @Id
    private Long id;

    private Long money;

    @Version
    private Long version;
}

UPDATE member SET money = ?, version = version + 1 WHERE id = ? AND version = ? 와 같은 쿼리를 실행


장단점

장점

  • 락을 걸지 않기 때문에 데이터베이스 락 경합(여러 트랜잭션이 동시에 같은 락을 요청할 때 발생하는 문제)이 없다
  • 읽기 성능이 높고, 충돌이 드문 환경에 적합

단점

  • 충돌 시 롤백해야 하므로 처리 비용이 높다
  • 동시 요청이 많고 충돌이 빈번한 환경에서는 불리



비관적 락

비관적 락은 충돌이 일어날 가능성이 높다고 비관적으로 가정하고,
데이터를 읽을 때부터 다른 트랜잭션이 접근하지 못하도록 락을 거는 방식입니다.
JPA에서는 @Lock 어노테이션과 LockModeType.PESSIMISTIC_WRITE 등을 사용합니다.


동작 방식

  1. 데이터를 조회할 때 DB에 SELECT ... FOR UPDATE 쿼리가 날아가며 락이 걸림
  2. 트랜잭션이 끝날 때까지 다른 트랜잭션은 해당 데이터를 읽거나 수정하지 못함
  3. 충돌 없이 순차적으로 처리되므로, 데이터 정합성을 확실히 보장할 수 있음

JPA 사용 예시

public interface MemberRepository extends JpaRepository<Member, Long> {

    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @Query("SELECT m FROM Member m WHERE m.id = :id")
    Optional<Member> findByIdForUpdate(@Param("id") Long id);
}

SELECT ... FOR UPDATE 쿼리가 날아가고, 해당 행은 락이 걸린 상태


장단점

장점

  • 충돌 없이 데이터 정합성을 강력하게 보장할 수 있음
  • 롤백보다 락을 통한 순차처리로 안정적

단점

  • 락이 걸려 있는 동안 다른 트랜잭션은 대기하게 되므로 성능 저하 가능성 있음
  • 데드락(영원히 대기..) 가능성 있음 → 트랜잭션 설계 주의 필요


결론

(프로젝트 사진 추가 예정)

현재는 JPA와 비관적 락을 기반으로 구현을 시작했습니다.
이후 거래량이 증가하고 병목 현상이 발생할 경우, Redis 기반의 분산 락으로 전환할 예정입니다.
만약 더 큰 규모의 트래픽 처리나 자동 매매 로직의 고속화가 필요해진다면, Kafka 기반의 비동기 처리 구조로 확장할 예정입니다.


정합성이 중요한 로직에서는 충돌을 피할 수 있는 전략이 반드시 필요하다는 것을 느꼈습니다. 낙관적 락이 성능 면에서 유리하긴 하지만, 충돌 가능성이 높은 환경에서는 비관적 락이나 분산 락으로의 전환을 고려해야만 안정적인 시스템을 유지할 수 있다는 점을 체감했습니다.

트랜잭션 설계와 데이터베이스 특성까지 함께 고려한 전략적인 판단이 결국 사용자에게 안정적인 서비스를 제공하는 핵심임을 다시금 느낄 수 있었습니다.



참고

https://f-lab.kr/insight/understanding-optimistic-and-pessimistic-locking

https://sabarada.tistory.com/175

https://gyeongsuuuu.tistory.com/68

https://seongwon.dev/Spring-MVC/20230430-%EB%B9%84%EA%B4%80%EC%A0%81%EB%9D%BD%EA%B3%BC_%EB%82%99%EA%B4%80%EC%A0%81%EB%9D%BD/

0개의 댓글