2 분 소요

락이 왜 필요한가

아래와 같은 상황을 상상해 봅시다.

은행에 $100을 가지고 있는 사람이 거의 동시에 두 인출기(A, B)에서 $10씩 입금을 시도

  • A인출기가 잔고를 조회할 때 $100을 가지고 있으므로 $10 를 입금하고 $110 를 다시 저장.
  • 위의 A인출기가 $110를 저장하기 전에 B인출기가 잔고를 $100로 조회했고 역시 $10를 추가한 다음 $110를 저장.

$100에서 총 $20가 입금이 되었지만 실제 잔고는 $110 (기대: $120)

자원에 대한 동시 요청이 발생 했을 때 일관성(Consistency)에 문제가 발생할 수 있습니다. 이를 방지하기 위해 자원에 대한 수정을 못하도록 을 사용합니다.

낙관적 락과 비관적 락

비관적 락 (Pessimistic Lock)

위의 문제를 해결하는 방법은 A인출기와 B인출기가 잔고를 불러올 때 자원을 선점(Lock)해서 다른 인출기가 접근하지 못하도록 막는 것입니다. 접근할 때 무조건 잠그고 시작하기 때문에 선점 락, 비관적 락이라는 용어를 사용합니다.

MySQL 에서 쉽게 사용할 수 있는 방법이 SELECT FOR UPDATE 쿼리를 이용해서 업데이트할 데이터에 락을 걸 수 있습니다. 만약 A인출기가 먼저 위 쿼리를 실행했다면 B인출기는 A인출기가 위의 선점을 풀기(Release)까지 기다리게 됩니다. 그럼 위의 상황에서 B인출기는 잔고를 조회할 때 $110를 읽어오기 때문에 결과적으로 잔고를 $120로 업데이트할 것이고 모든 문제가 해결되었습니다.

하지만 모든 상황에서 비관적 락 사용이 가능한 것은 아닙니다. 아래와 같은 경우에 비관적 락을 사용할 수 없거나 문제가 발생할 수 있습니다.

  • 자원의 순환참조로 교착상태에 빠질 수 있는 경우
    • 로직1(A → B) 과 로직2(B → A) 의 경우 각각 A, B까지 수행한 경우 교착상태에 빠짐
  • MSA와 같이 물리적으로 데이터베이스가 분리되어 있는 경우
    • 락을 걸 방법이 없음

낙관적 락 (Optimistic Lock)

낙관적이라는 용어를 사용할까요? 일단 락을 이용한 선점을 미리 하지 말고 문제가 발생하면 그 때 대응하자 라는 의미로 생각하시면 좋습니다. 그럼 어떻게 락을 걸지 않고 동시성 문제를 해결할까요? 바로 Version 을 이용해서 입니다.

-- 읽어온 버전이 10이라고 가정하면,
UPDATE deposit_accounts
SET
  balance = 110,
  version = 11
WHERE
  id = 1234 AND version = 10

위의 예제에서 만약 동시성 이슈로 다른 비즈니스로직에서 업데이트 쿼리가 수행되었다면 어떤 결과를 리턴 할까요? 다른 비즈니스로직에서 이미 VERSION11 로 업데이트 했을 것이므로 WHERE 절에서 VERSION = 10 을 찾을 수 없어서 위 쿼리가 반영되지 않게 됩니다.

그럼 여기서 종료하게 되면 위 쿼리는 영영 반영할 수 없게 됩니다. 비즈니스 요구사항에 따라서 여기서 바로 로직을 실패처리 할수도 있지만 경우에 따라서는 성공으로 처리 해야 하는 경우도 있습니다. 그럼 어떻게 해야 위 쿼리를 반영할 수 있을까요? 바로 자동 재시도를 통해 해결이 가능합니다.

자동 재시도

이번엔 Python으로 로직을 작성해 보도록 하겠습니다. 위의 인출기에서 10원

for i in range(10): # 10 이상 재시도가 필요할 경우 수정필요
    account = DepositAccounts.get(id=1234)
    account.balance += 10
    affected_rows = DepositAccounts
            .filter(
                id=1234,
                version=account.version, # 읽어온 버전으로 필터링
            ).update(
                balance = account.balance,
                version=account.version+1,
            )
    #  업데이트 쿼리가 반영된 Row  있으므로 재시도를 중단함
    if affected_rows:
        break-- 읽어온 버전이 10이라고 가정하면,
UPDATE deposit_accounts
SET
  balance = 110,
  version = 11
WHERE
  id = 1234 AND version = 10

위의 로직에서 affected_rows 가 0이면 for loop 을 다시 수행하고 10번 안에 성공을 하게 되면 잔고를 $10 더한 다음 저장할 수 있습니다.

결론

낙관적 락은 언제나 사용할 수 있는 것이 아닙니다. 위의 자동 재시도 로직 처럼 유한으로 재시도를 할 경우 이로 인해 무결성(Consistency) 문제가 발생할 수 있으며 이런 경우 여전히 비관적 락을 사용할 수 있습니다. 낙관적 락은 모든 경우의 해결책이 아니지만 동시성 문제가 발생할 때 다른, 그리고 어느경우에는 좀더 좋은 해결책이 될 수 있을 것입니다.

댓글남기기