어 나 갱수.

[DB] 낙관적 락(Optimistic Lock), 비관적 락(Pessimistic Lock) 본문

DB

[DB] 낙관적 락(Optimistic Lock), 비관적 락(Pessimistic Lock)

김경수 2024. 8. 20. 17:15
728x90

데이터베이스에 접근해서 데이터를 수정할 때 동시에 수정이 일어나게 되면 충돌이 발생하게 됩니다. 충돌이 발생 안 하려면 어떻게 해야 할까요? 저희는 데이터베이스의 락(Lock)을 사용해서 충돌을 방지할 수 있습니다.
이러한 동시성 문제는 데이터의 일관성과 무결성을 위협할 수 있습니다. 따라서 동시에 데이터에 접근하려는 다양한 요청들을 잘 제어하는 것은 필수적인 작업입니다. 동시성 제어를 위한 두 가지 핵심적인 방법에 대해 알아보겠습니다.

발생할 수 있는 동시성 문제

상품 재고 수량이 맞지 않는 경우

 

  1. 사용자 A가 재고가 5개인 상품을 조회하고 재고를 하나 차감하는 트랜잭션을 실행
  2. 사용자 B가 전체 상품을 조회, 사용자 A가 실행시킨 트랜잭션이 커밋되지 않았기 때문에 5개가 조회
  3. 사용자 A가 실행한 트랜잭션이 커밋됨(현재 재고 4개)
  4. 실제 재고는 4개지만 사용자 B가 재고를 조회했을 때는 커밋 이전이었기 때문에 5개의 재고가 조회됨(데이터 불일치 발생)

송금 내역이 분실되는 경우

 

  1. 계좌에 10,000원이 있는 상황이다. 트랜잭션 A는 계좌의 50,000원을 입금하고, 트랜잭션 B는 계좌의 20,000원을 입금한다.
  2. 입금을 하기 위해서는 계좌의 잔액을 조회해야 하고 입금 금액을 더해서 잔액을 갱신해야 합니다.
  3. 트랜잭션 A에서 먼저 잔액 10,000원을 조회했습니다. 그리고 뒤이어 트랜잭션 B도 잔액 10,000원을 조회했습니다.
  4. 트랜잭션 A는 잔액 10,000원에서 50,000원을 더한 60,000원으로 잔액을 갱신하고 커밋하였습니다. (계좌의 잔액 60,000원)
  5. 트랜잭션 B는 잔액 10,000원에서 20,000원을 더한 30,000원으로 잔액을 갱신하고 커밋합니다. (계좌의 잔액 30,000원)
  6. 이렇게 되면 나중에 커밋한 트랜잭션 B의 결과만 적용됩니다.
  7. 트랜잭션 A의 송금내역은 사라지게 되는 겁니다. (데이터 불일치)

동시성 제어

  • 테이블에 접근했을 때 Lock이 걸려있으면 수정이 불가능하고, Lock이 걸려 있지 않을 경우에만 수정을 가능하게 할 수 있습니다.
  • 데이터를 수정할 때 내가 먼저 이 값을 수정했다고 명시하여 다른 사람이 수정 전의 값으로 수정할 수 없게 하는 것입니다.

낙관적 락(Optimistic Lock)

낙관적 락은 대부분의 트랜잭션이 충돌이 발생하지 않을 거라고 낙관적으로 가정하는 방법입니다. 그렇기 때문에 여러 트랜잭션이 동시에 데이터에 접근할 수 있도록 허용해 줍니다. 따라서 데이터베이스에서 제공되는 락 기능을 사용하지 않고 엔티티의 버전을 통해 동시성을 제어합니다. 자원에 대해 미리 락을 걸지 않고 충돌이 발생하면 그때 처리하도록 동시성을 제어합니다.

 

낙관적 락은 DB가 아니라 애플리케이션 단에서 처리 합니다. 데이터를 읽은 후에 데이터를 update 하는 과정에서 예외가 발생하면 다른 트랜잭션에서 이미 데이터를 update 하고 버전을 변경했다는 말이기 때문입니다. 그렇기 때문에 예외가 발생한 작업은 롤백을 통해 충돌을 해결해야 합니다.

 

@Version

JPA는 @Version 어노테이션을 제공합니다. 이를 사용해서 엔티티의 버전을 관리할 수 있습니다. @Version은 아래와 같이 버전 관리용 필드를 만들어서 적용합니다.

@Entity
public class Board {

  @Id
  private String id;
  private String name;

  @Version
  private Integer version;
}

 

Board 엔티티가 변경될 때마다 version 이 자동으로 하나씩 증가합니다. 그리고 엔티티를 수정할 때 엔티티의 조회 시점의 엔티티의 버전과 일치하지 않으면 예외가 발생하면서 수정이 안됩니다.

 

예를 들어서 게시판이 있으면 그 게시판의 최초 name은 "데이터베이스 락"이라는 name이었습니다. 초기 버전은 v1입니다.

그 상태에서 첫 번째 관리자가 그 name을 "낙관적 락"이라고 변경하고자 하고, 두 번째 관리자가 그 name을 "비관적 락"이라고 변경하고자 합니다. 첫 번째 관리자가 조금 빠르게 먼저 변경 요청을 해서 "데이터베이스 락"을 "낙관적 락"으로 이름을 변경하였고 version도 기존의 v1에서 v2로 변경하였습니다. 두 번째 관리자도 name이 "데이터베이스 락" 상태일 때 조회를 해서 "비관적 락"으로 이름을 변경 후 요청을 했지만 조회를 할 때 해당 게시글의 버전은 v1이었지만 변경 요청을 할 때는 v2이므로 예외가 발생합니다.

핵심은 최초 커밋만 인정하는 정책이라는 것을 알 수 있습니다.

 

LockModeType을 사용해서 명시적으로 선언해서 적용할 수도 있습니다.

 

None

별도의 옵션을 사용하지 않아도 Entity에 @Version이 적용된 필드만 있으면 낙관적 잠금이 적용됩니다.

 

OPTIMISTIC (Read)

Entity를 수정 시에만 락이 발생하는 것이 아니라 읽는 작업에서도 락이 발생하도록 설정합니다. 읽는 작업에서도 락 버전을 확인하고 트랜잭션이 종료될 때까지 다른 트랜잭션에서 변경되지 않음을 보장해 줍니다. 

 

OPTIMISTIC_FORCE_INCREMENT (Write)

낙관적 잠금을 사용하면서 버전 정보를 강제로 증가시키는 옵션입니다.

 

버전 변경 비교

JPA가 엔티티를 변경하고 트랜잭션을 커밋하면 update 쿼리가 발생합니다.

UPDATE BOARD
SET
  title = ?,
  version = ? # 버전 + 1 증가
WHERE
  id = ?,
  and version = ? # 버전 비교

위와 같이 버전을 비교해서 최초 조회했을 때의 버전과 맞지 않으면 변경이 안됩니다.

OptimisticLockException

Persistence Provider가 Entity에서 낙관적 락 충돌을 감지하면 OptimisticLockException을 발생시킵니다. 그리고 트랜잭션도 함께 롤백시켜 작업을 되돌립니다.

비관적 락(Pessimistic Lock)

비관적 락은 간단하게 말하면 데이터를 수정하기 전에 해당 데이터에 대한 접근을 미리 제한하는 방식입니다. 데이터를 요청할 때 동시성 문제가 발생할 것이라고 예상하고 락을 거는 방법입니다. 한 번에 같은 데이터에 접근을 못하게 하는 방식입니다. 이렇게 동시에 같은 데이터 접근을 막는다면 여러 데이터의 충돌을 막을 수 있습니다.

 

비관적 락은 Shared Lock(읽기 잠금)이 있고 Exclusive Lock(쓰기 잠금)이 있습니다.

  • Shared Lock인 비관적 락은 다른 트랜잭션에서 읽는 작업만 가능합니다. 
  • Exclusive Lock의 경우 다른 트랜잭션에서 읽기, 쓰기 모두 불가능합니다.

비관적락은 데이터의 대한 접근이 엄격하므로 보통 데이터의 일관성이 중요하고, 데이터 충돌이 많을 거 같은 상황에서 많이 사용됩니다.

 

그러나, 비관적 락은 시스템 성능에는 부정적인 영향으로 미칠 수 있습니다. 왜냐하면 같은 데이터를 수정하기 위해 다른 트랜잭션에서 가지고 있는 락을 획득하기 위해 대기 시간이 길어질 수 있기 때문입니다. 대기 시간이 길어지면 자연스럽게 애플리케이션의 성능 저하 문제로 이어지게 됩니다. 또한 여러 트랜잭션이 서로 여러 데이터를 요청하면 데드락이 발생할 수 있습니다.

 

비관적 락의 LockModeType 종류

 

PESSIMISTIC_READ

공유 잠금 (Shared Lock)을 획득하고 데이터가 다른 트랜잭션에서 update, delete 되는 것을 방지합니다. 읽기 작업만 허용합니다.

 

PESSIMISTIC_WRITE

배타적 잠금 (Exclusive Lock)을 획득하고 데이터가 다른 트랜잭션에서 read, update, delete 되는 것을 방지합니다. 읽기 작업을 포함한 어떠한 쓰기 작업이 불가합니다.

 

Exception

 

PessimisticLockException

락은 Shared Lock 또는 Exclusive Lock 둘 중 하나만 획득 가능하며, 그 락을 획득하는데 실패하면 발생하는 예외입니다.

 

LockTimeoutException

락을 대기시켜 놓았다가 설정해 놓은 wait time을 초과했을 경우에 발생하는 예외입니다.

 

낙관적 락 VS 비관적 락

사실 어떤 락 방법이 더 좋다 이런 거는 없습니다. 현재 상황에 맞는 락 방법을 사용하는 것이 가장 중요하다고 생각합니다. 그럼 어떤 상황에서 어떤 락 방법을 사용하는 것이 좋을까요? 방법을 선택하는데 가장 중요한 것은 트랜잭션 간의 충돌 빈도입니다.

 

  • 비관적 락
    • 충돌이 많이 발생하고 데이터의 일관성, 정합성들이 중요시되는 애플리케이션
  • 낙관적 락
    • 충돌이 적게 발생하고 데이터의 일관성, 정합성보다는 성능이 중요시되는 애플리케이션

요약하면, 비관적 락은 동시성 충돌이 발생할 것으로 가정하고 미리 락을 걸어 동시 접근을 차단하는 방식이며, 낙관적 락은 동시성 충돌이 거의 발생하지 않을 것으로 가정하고 동시 접근을 허용하고 충돌이 발생하면 그 시점에서 처리하는 방식입니다.

728x90

'DB' 카테고리의 다른 글

[DB] Spring Boot에서 Redis Cache를 구현하기 😤  (0) 2024.07.09
[DB] Redis Cache 🔥  (0) 2024.07.08
[DB] INNER 조인 & OUTER조인 🤚  (0) 2024.02.29
[DB] 정규화란 ? 🐼  (1) 2024.02.04
[DB] 인덱스(Index)란? 😛  (1) 2024.01.31