포스트

2024-07-24-TIL

2024-07-24-TIL

Today I Learned

Optimistic Lock vs Pessimistic Lock

여러 트랜잭션이 같은 데이터를 동시에 읽고 고칠 때 생기는 대표적인 문제가 갱신 손실(lost update)이다. Vlad Mihalcea의 글은 계좌 잔액 예로 설명한다. 두 사용자가 같은 잔액을 읽고, 한쪽이 먼저 수정해 커밋한 뒤, 다른 쪽이 예전에 읽은 값을 기준으로 다시 수정하면 앞선 변경이 사라지거나 잘못된 상태가 된다. 이를 막는 방법이 비관적 락과 낙관적 락이다.

비관적 락(Pessimistic Lock)

충돌이 일어날 것이라고 가정하고, 데이터를 읽는 시점에 미리 락을 건다. DB의 행 락을 직접 사용하며, 대표적으로 SELECT ... FOR UPDATE로 배타 락을 잡는다. 다른 트랜잭션은 락이 풀릴 때까지 기다리므로 충돌이 아예 일어나지 않는다.

1
2
3
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("select a from Account a where a.id = :id")
Optional<Account> findByIdForUpdate(@Param("id") Long id);

장점은 정합성이 확실하다는 것이고, 단점은 대기 시간이 생겨 처리량이 줄고, 락 순서가 엇갈리면 데드락이 생길 수 있다는 것이다. 락을 잡은 채 외부 API를 호출하는 식으로 트랜잭션이 길어지면 문제가 커진다.

낙관적 락(Optimistic Lock)

충돌이 드물다고 가정하고 락 없이 읽는다. 대신 수정할 때 내가 읽은 이후 다른 트랜잭션이 바꾸지 않았는지 버전 컬럼으로 확인한다.

1
2
3
UPDATE account
SET balance = 20, version = 2
WHERE id = 1 AND version = 1;

다른 트랜잭션이 먼저 버전을 올렸다면 조건에 맞는 행이 없어 갱신된 행 수가 0이 되고, JPA는 이를 OptimisticLockException으로 알린다. 엔티티에 @Version 필드를 두면 JPA가 이 조건을 자동으로 붙여 준다.

1
2
3
4
5
6
7
8
9
@Entity
public class Account {
    @Id
    private Long id;
    private long balance;

    @Version
    private Long version;
}

DB 락을 오래 잡지 않으므로 처리량이 좋고, 웹 애플리케이션처럼 조회와 수정이 서로 다른 HTTP 요청에 걸쳐 일어나는 경우에도 쓸 수 있다. 대신 충돌이 나면 애플리케이션이 재시도하거나 사용자에게 다시 시도하라고 알려야 한다.

선택 기준

기준비관적 락낙관적 락
충돌 빈도높을 때낮을 때
충돌 시 비용재시도 비용이 큰 작업재시도해도 괜찮은 작업
락 범위한 트랜잭션 안여러 요청에 걸친 작업도 가능
주의점대기, 데드락재시도 로직, 충돌 시 사용자 경험

Vlad Mihalcea도 재시도 비용이 매우 크거나 경합이 심해 많은 트랜잭션이 롤백될 상황이라면 비관적 락을, 요청을 넘나드는 긴 작업이라면 낙관적 락을 권한다. 재고 차감이나 좌석 예약처럼 같은 행에 요청이 몰리는 경우는 비관적 락이나 원자적 UPDATE(stock = stock - 1 WHERE stock > 0)가, 게시글 수정처럼 동시에 같은 데이터를 고칠 일이 드문 경우는 낙관적 락이 잘 맞는다.

참고한 자료외부 출처 7

외부 출처

이 글은 저작권자의 CC BY 4.0 라이선스를 따릅니다.

댓글

아직 댓글이 없습니다