optimistic lock이란?
optimistic lock은 낙관적 락으로 실제로 DB에서 row에 lock을 거는것이 아닌, 어플리케이션 레벨에서 version 정보를 활용해서 수정 여부를 확인하는 방식이다.
트랜잭션 대부분은 충돌이 발생하지 않을것이다라고 낙관적으로 가정해서 일단 수정하고 DB에 저장을 하려고 할때, 누가 이미 수정했는지 체크한다. 그래서 트랜잭션의 커밋이 끝나기 전에는 수정됐는지 확인할 수 없다.
version이 뭐지?
version은 이 row의 버전을 의미한다. version은 타임스탬프일수도 있고 Auto Increment 값일 수도 있다. version 정보를 활용해서 경쟁상태인지 체크하는 방법은 아래와 같다.
- 하나의 row를 SELECT 해서 메모리에 가져온다. 여기엔 버전 정보도 포함되어있다. ex) SELECT * FROM product
- row의 값을 수정한다. ex) product.name = '테스트-수정'
- DB에 업데이트 쿼리를 요청한다. 다만 조건에 version을 추가한다. 만약 그 version에 해당하는 row가 없으면 에러가 발생한다. ex) UPDATE product SET name = '테스트-수정' WHERE id = ? and version = ?
경쟁 상태인 경우에는 어떻게 될까?
예를 들어 하나의 상품을 두 명의 MD가 거의 동시에 수정했다고 가정하자
아무런 lock을 적용하지 않은 경우 조금 더 뒤에 수정한 B의 수정본이 최종 반영되고 A의 수정본은 없어질것이다.
하지만 낙관적 락을 적용한다면 어떻게 될까? B가 수정하려고 하면 에러가 발생하면서 수정되지 않을 것이다.
django-concurrency 사용하기
Spring으로 개발할때는 JPA에서 제공해주는 @Version 어노테이션으로 version을 쉽게 사용할 수 있었다. django에도 그런 라이브러리가 없나 찾아봤더니 다행히도 낙관적 락을 쉽게 구현할 수 있는 라이브러리가 있었다.
Django Concurrency — Django Concurrency 2.4 documentation
© Copyright 2012-2019, Stefano Apostolico Revision fba78f11.
django-concurrency.readthedocs.io
django-concurrency를 사용하면 version 필드를 쉽게 사용할 수 있다.
- IntegerVersionField: version 정보를 타임스탬프 정보를 활용해서 유니크한 버전을 보장해주는 방식이다. update할 때 마다 유니크한 버전을 보장해준다.
- AutoIncVersionField: version 정보를 Auto Increment 값으로 한다. 초기값은 1이고 수정할때마다 +1이 된다. 개인적으로 이 방식을 더 선호한다.(1이면 수정되지 않은 값이란걸 알 수 있어서 더 유용한 것 같다.)
from django.db import models
from concurrency.fields import AutoIncVersionField
class Product(models.Model):
name = models.CharField(max_length=128, blank=False, null=False, help_text='상품명')
description = models.CharField(max_length=256, blank=False, null=False, help_text='상품 설명')
sale_price = models.IntegerField(blank=False, null=False, help_text='판매가')
regular_price = models.IntegerField(blank=False, null=False, help_text='정가')
version = AutoIncVersionField(blank=False, null=False)
class Meta:
db_table = 'product'
예시로 Product라는 model에 version이라는 필드를 선언해주고 django_extensions의 shell_plus로 어떻게 update 되는지 테스트 해보았다.
만약 경쟁 상태가 발생한다면 어떻게 될까? 이미 수정된 row를 수정하려고 하면 RecordModifiedError가 발생하게 된다.
def test_product_concurrency_use_case(self):
productA = Product.objects.get(id=1)
productB = Product.objects.get(id=1)
productA.sale_price = F('sale_price') - 2000
productB.sale_price = F('sale_price') - 1000
productA.save()
with self.assertRaises(RecordModifiedError):
productB.save()
주의해야할 점
1. save에 update_fields를 사용하는 경우 version을 추가해주지 않으면 version이 업데이트되지 않는다. ex) product.save(update_fields=['name'])
2. 바로 update하는 경우 version이 업데이트되지 않는다. ex) Product.objects.filter(id=1).update(sale_price=F('sale_price') + 2000)
실무에서의 활용
실무에서도 낙관적 락을 다양한 사례로 활용할 수 있다.
예를 들 재고 관리나 상품 상세 정보, 회원 정보 수정 등 경쟁 상태가 될 수 있는 다양한 상황에서 사용할 수 있다.
하지만 낙관적 락은 수정에 실패할 경우 처리(재시도, 그냥 에러)를 따로 해줘야하기 때문에 유즈 케이스를 잘 고려해서 적용해주는 것이 좋다.
'Django,Python' 카테고리의 다른 글
[Django] shell_plus로 개발 생산성 높이기 (0) | 2024.07.07 |
---|---|
[Django] Django에서는 HTTP 요청을 어떻게 처리할까? (0) | 2024.06.21 |
[Django] 동시성 고려하기(1) - F 객체 사용하기 (0) | 2024.05.12 |
[Django] factory_boy (0) | 2024.04.16 |
[Python] Pickle (0) | 2024.04.06 |