Lock ๐ (Django ๊ธฐ์ค)


๋น๊ด์ ๋ฝ(Pessimistic Lock) ๐
"์ถฉ๋์ด ๋ฐ์ํ ๊ฒ์ด๋ผ๊ณ ๋น๊ด์ ์ผ๋ก ๊ฐ์ "ํฉ๋๋ค.
- ๋ฐ์ดํฐ๋ฅผ ์ฝ๋ ์์ ์ ์ฆ์ ๋ฝ์ ๊ฑธ์ด ๋ค๋ฅธ ์ฌ์ฉ์๊ฐ ์์ ํ์ง ๋ชปํ๋๋ก ์์ฒ ์ฐจ๋จํ๋ ๋ฐฉ์์
๋๋ค.
select_for_update()
- ๋ฐ๋์
transaction.atomic() ๋ธ๋ก ๋ด์์ ์คํ๋์ด์ผ ํฉ๋๋ค.
์ฝ๋
select_for_update() ๋ ํด๋น ๋ก์ฐ๊ฐ ํธ๋์ญ์
์ข
๋ฃ ์์ ๊น์ง ์ ๊ธฐ๋๋ก
SELECT ... FOR UPDATE SQL ๋ฌธ์ ์์ฑ
from django.db import transaction
from .models import Product
def decrease_stock_pessimistic(product_id, quantity):
with transaction.atomic():
product = Product.objects.select_for_update().get(id=product_id)
if product.stock >= quantity:
product.stock -= quantity
product.save()
else:
raise ValueError("์ฌ๊ณ ๊ฐ ๋ถ์กฑํฉ๋๋ค.")
select_for_update(nowait=True)
- ์ฌ์ฉํ๋ฉด ๋ค๋ฅธ ๊ณณ์์ ์ด๋ฏธ ๋ฝ์ ๊ฑธ์์ ๋
- ๊ธฐ๋ค๋ฆฌ์ง ์๊ณ ๋ฐ๋ก DatabaseError๋ฅผ ๋ฐ์์์ผ ๋น ๋ฅธ ์๋ต ์ฒ๋ฆฌ๊ฐ ๊ฐ๋ฅํฉ๋๋ค.
๋น๊ด์ ๋ฝ์ ์น๋ช
์ ์ธ ๋จ์ โผ๏ธ
๋น๊ด์ ๋ฝ์ ๊ฐ๋ ฅํ์ง๋ง, ๊ทธ๋งํผ ๋๊ฐ๊ฐ ํฝ๋๋ค.
์ฑ๋ฅ ์ ํ (Throughput ๊ฐ์)
- ํน์ ๋ฐ์ดํฐ์ ๋ฝ์ด ๊ฑธ๋ฆฌ๋ฉด ๋ค๋ฅธ ๋ชจ๋ ์์ฒญ์ ํด๋น ํธ๋์ญ์
์ด ๋๋ ๋๊น์ง
- "์ค์ ์์ ๋๊ธฐ"ํด์ผ ํฉ๋๋ค.
- ์ด๋ ์น ์๋น์ค์ ์๋ต ์๋๋ฅผ ๋ฆ์ถ๋ ์ฃผ๋ฒ์ด ๋ฉ๋๋ค.
๋ฐ๋๋ฝ(Deadlock) ์ํ
- ์ฌ๋ฌ ํธ๋์ญ์
์ด ์๋ก๊ฐ ๊ฐ์ง ๋ฝ์ ๊ธฐ๋ค๋ฆฌ๋ ๊ต์ฐฉ ์ํ์ ๋น ์ง ์ ์์ต๋๋ค.
- ์์คํ
์ ์ฒด๊ฐ ๋ฉ์ถ ์ํ์ด ์์ต๋๋ค.
DB ์ปค๋ฅ์
์ ์
- ๋ฝ์ ์ ์งํ๋ ๋์ DB ์ปค๋ฅ์
์ ๊ณ์ ๋ถ์ก๊ณ ์์ด์ผ ํฉ๋๋ค.
- ๋์ ์ ์์๊ฐ ๋ง์์ง๋ฉด ์ปค๋ฅ์
ํ์ด ๊ณ ๊ฐ๋์ด ์๋น์ค ์ฅ์ ๋ก ์ด์ด์ง๋๋ค.
๋๊ด์ ๋ฝ(Optimistic Lock) ๐
"์ถฉ๋์ด ๊ฑฐ์ ๋ฐ์ํ์ง ์์ ๊ฒ์ด๋ผ๊ณ ๋๊ด์ ์ผ๋ก ๊ฐ์ " ํฉ๋๋ค.
- ๋ฐ์ดํฐ๋ฅผ ์ฝ์ ๋๋ ๋ฝ์ ๊ฑธ์ง ์๊ณ , ์์ ์๋ฃ ์์ ์ ๋ด๊ฐ ์ฝ์๋ ๋ฐ์ดํฐ๊ฐ
- ๊ทธ์ฌ์ด ๋ณํ๋์ง ํ์ธํ์ฌ ์ถฉ๋์ ๊ฐ์งํ๋ ๋ฐฉ์์
๋๋ค.
Race Condition (๊ฒฝ์ ์ํ)
- ๋ ๊ฐ ์ด์์ ํ๋ก์ธ์ค๊ฐ ๊ณตํต ์์์ ๋์์ ์ ๊ทผํ ๋ ๋ฐ์ํ๋ ๋ฌธ์ ์ํฉ์
๋๋ค.
Version / F
- Django๋ ๋ณ๋์ @Version ๊ฐ์ ๋ด์ฅ ํ๋๊ฐ ์์
- ๋ฐ๋ผ์ version ํ๋๋ฅผ ์ง์ ์ ์ํ๊ฑฐ๋
- F ๊ฐ์ฒด๋ฅผ ์ด์ฉํ ์์์ (Atomic) ์
๋ฐ์ดํธ ๋ฐฉ์์ ์ฃผ๋ก ์ฌ์ฉ
F ๊ฐ์ฒด
-
์ผ๋ฐ์ ์ธ ๋ฐฉ์ (F ๊ฐ์ฒด ๋ฏธ์ฌ์ฉ):
- DB์์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ ํ์ด์ฌ ๋ฉ๋ชจ๋ฆฌ์ ์ฌ๋ฆฝ๋๋ค. (stock = 10)
- ํ์ด์ฌ์์ ๊ณ์ฐํฉ๋๋ค. (10 - 1 = 9)
- ๊ณ์ฐ๋ ๊ฒฐ๊ณผ 9๋ฅผ DB์ ๋ค์ ์ ์ฅํฉ๋๋ค. (UPDATE ... SET stock = 9)
- ์ํ์ฑ
- ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๊ณ ์ ์ฅํ๋ ์ฐฐ๋์ ์๊ฐ์ ๋ค๋ฅธ ์ฌ์ฉ์๊ฐ ์ฌ๊ณ ๋ฅผ ๋ฐ๊ฟ๋ฒ๋ฆฌ๋ฉด ๋ฐ์ดํฐ๊ฐ ๊ผฌ์
๋๋ค.
-
F ๊ฐ์ฒด ๋ฐฉ์:
- ํ์ด์ฌ์ ๊ณ์ฐ์ ํ์ง ์์ต๋๋ค.
- ๋์ "DB์ผ, ๋ค๊ฐ ๊ฐ์ง stock ๊ฐ์์ quantity๋งํผ ๋นผ์ค"๋ผ๋ SQL ๋ช
๋ น์ด๋ง ๋ณด๋
๋๋ค.
- ์ค์ ๊ณ์ฐ์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์์ง ๋ด๋ถ์์ ์ผ์ด๋ฉ๋๋ค.
-
F ๊ฐ์ฒด ์ฌ์ฉ์ ์ฅ์
- ๊ฒฝ์ ์ํ(Race Condition) ๋ฐฉ์ง
- ์ฌ๋ฌ ์์ฒญ์ด ๋์์ ๋ค์ด์๋ DB๊ฐ ์์ฐจ์ ์ผ๋ก ์ฐ์ฐ์ ์ฒ๋ฆฌํ๋ฏ๋ก ๋ฐ์ดํฐ ๋ฌด๊ฒฐ์ฑ์ด ์ง์ผ์ง๋๋ค.
- ์ด๊ฒ์ด ๋ฐ๋ก ๋๊ด์ ๋ฝ์ ํต์ฌ ์๋ฆฌ ์ค ํ๋์
๋๋ค.
- ์ฑ๋ฅ ์ต์ ํ
- ๋ฐ์ดํฐ๋ฅผ ํ์ด์ฌ ๋ฉ๋ชจ๋ฆฌ๋ก ๋ถ๋ฌ์ฌ(SELECT) ํ์ ์์ด ๋ฐ๋ก UPDATE ๋ช
๋ น์ ๋ด๋ฆฌ๋ฏ๋ก
- ๋ฐ์ดํฐ๋ฒ ์ด์ค ํต์ ํ์์ ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋์ด ์ค์ด๋ญ๋๋ค.
- ์์์ฑ(Atomicity)
- ์กฐํ์ ์์ ์ ํ๋์ ์ฟผ๋ฆฌ๋ก ์ฒ๋ฆฌํ์ฌ ์์์ ์ธ ์์
์ ๋ณด์ฅํฉ๋๋ค
์ฝ๋
F ๊ฐ์ฒด๋ฅผ ์ด์ฉํ ์์์ ์
๋ฐ์ดํธ
- ๋ณ๋์ ๋ฒ์ ํ๋ ์์ด๋ Race Condition์ ๋ฐฉ์งํ๋ ๊ฐ์ฅ "ํ์ด์ฌ์ค๋ฌ์ด" ๋ฐฉ์์
๋๋ค.
- DB ์์ค์์ ํ์ฌ ๊ฐ์ ๊ธฐ์ค์ผ๋ก ์ฐ์ฐํฉ๋๋ค
from django.db.models import F
from .models import Product
def decrease_stock_f_expression(product_id, quantity):
updated_count = Product.objects.filter(
id=product_id,
stock__gte=quantity
).update(stock=F('stock') - quantity) โฆ๏ธ
if updated_count == 0:
raise ValueError("์ฌ๊ณ ๊ฐ ๋ถ์กฑํ๊ฑฐ๋ ๋ฐ์ดํฐ๊ฐ ๋ณ๊ฒฝ๋์์ต๋๋ค.")
์๋ ๋ฒ์ ๊ด๋ฆฌ (JPA ๋ฐฉ์๊ณผ ์ ์ฌ)
- ์ํฐํฐ์ version ํ๋๋ฅผ ์ถ๊ฐํ์ฌ ์ง์ ๊ฒ์ฌํฉ๋๋ค.
from .models import Product
def update_product_manual_version(product_id, new_name):
product = Product.objects.get(id=product_id)
current_version = product.version โฆ๏ธ
product.name = new_name
product.version += 1
updated = Product.objects.filter(
id=product_id,
version=current_version
).update(name=new_name, version=F('version') + 1)
if not updated:
raise Exception("์ด๋ฏธ ๋ค๋ฅธ ์ฌ์ฉ์์ ์ํด ์์ ๋ ๋ฐ์ดํฐ์
๋๋ค.")
๋๊ด์ ๋ฝ์ด ๋น์ ๋ฐํ๋ ์ํฉ โจ
- ๋๊ด์ ๋ฝ์ "์ถฉ๋์ด ๊ฐ๋ ์ผ์ด๋๋ค"๋ ์ ์ ํ์ ์์คํ
์ ์ ์ฒด์ ์ธ ์ฒ๋ฆฌ๋์ ๊ทน๋ํ
์ฝ๊ธฐ ์์
์ด ์๋์ ์ผ๋ก ๋ง์ ๋
- ๋๋ถ๋ถ์ ์น ์๋น์ค๋ ๋ฐ์ดํฐ๋ฅผ ์์ ํ๋ ์์ฒญ๋ณด๋ค ์กฐํํ๋ ์์ฒญ์ด ํจ์ฌ ๋ง์ต๋๋ค.
- ์กฐํํ ๋๋ง๋ค ๋ฝ์ ๊ฑธ๋ฉด(Shared Lock ํฌํจ) ์ฑ๋ฅ์ด ์ฌ๊ฐํ๊ฒ ์ ํ๋๋๋ฐ,
- ๋๊ด์ ๋ฝ์ ์กฐํ ์ ์๋ฌด๋ฐ ์ ์ฝ์ ์ฃผ์ง ์์ผ๋ฏ๋ก ๋งค์ฐ ๋น ๋ฆ
๋๋ค.
ํธ๋์ญ์
์ด ๊ธธ์ด์ง ๋
- ์ฌ์ฉ์๊ฐ ๊ธ์ ์์ ํ๊ธฐ ์ํด ํธ์ง ์ฐฝ์ ์ด์ด๋๊ณ 10๋ถ ๋ค์ '์ ์ฅ'์ ๋๋ฅธ๋ค๊ณ ๊ฐ์ ํด ๋ด
์๋ค.
- ๋น๊ด์ ๋ฝ
- 10๋ถ ๋์ ๋ค๋ฅธ ๋๊ตฌ๋ ๊ทธ ๊ธ์ ์ฝ๊ฑฐ๋ ์์ ํ์ง ๋ชปํ๊ฒ ๋ง์ต๋๋ค. (์ฌ์ค์ ๋ถ๊ฐ๋ฅ)
- ๋๊ด์ ๋ฝ
- ์ ์ฅ ๋ฒํผ์ ๋๋ฅด๋ ์๊ฐ์๋ง ๋ฒ์ ์ด ๋ฐ๋์๋์ง ์ฒดํฌํฉ๋๋ค. ํจ์จ์ฑ์ด ์๋์ ์
๋๋ค.
์ค๋ฌด ์ ์ฉ ๊ฐ์ด๋
๋น๊ด์ ๋ฝ ๊ถ์ฅ
- ์ฌ๊ณ ์ฐจ๊ฐ, ํฌ์ธํธ ๊ฒฐ์
- ๋ฐ์ดํฐ์ ์ ํ์ฑ์ด ์๋ช
์ด๋ฏ๋ก
select_for_update()๋ฅผ ๊ถ์ฅํฉ๋๋ค.
๋๊ด์ ๋ฝ ๊ถ์ฅ
- ๋จ์ ๊ฒ์๊ธ ์์ , ์ฌ์ฉ์ ํ๋กํ ๋ณ๊ฒฝ
- ์ถฉ๋ ๊ฐ๋ฅ์ฑ์ด ๋ฎ์ผ๋ฏ๋ก ๋ฝ ์์ด ์ฒ๋ฆฌํ๊ฑฐ๋
F ๊ฐ์ฒด๋ฅผ ์ด์ฉํ ๋๊ด์ ์
๋ฐ์ดํธ๋ฅผ ๊ถ์ฅํฉ๋๋ค.
์ฑ๋ฅ ์ต์ ํ
- select_for_update()๋ DB์ ์ปค๋ฅ์
์ ์ ์๊ฐ์ ๋๋ฆฌ๋ฏ๋ก
- ํธ๋ํฝ์ด ๋งค์ฐ ๋๋ค๋ฉด Redis๋ฅผ ํ์ฉํ ๋ถ์ฐ ๋ฝ์ ๊ณ ๋ คํด์ผ ํฉ๋๋ค.
์ ํ๊ธฐ์ค
๋น๊ด์ ๋ฝ์ ์ฐ๋ ๊ฒฝ์ฐ (ํน์ ์ํฉ)
- ์ฌ๊ณ ๊ฐ 1๊ฐ ๋จ์ ์ดํน๊ฐ ์ธ์ผ ์ํ (์ถฉ๋์ด 100% ํ์คํ ๋)
- ๊ธ์ต ์์คํ
์ ์์ก ์ด์ฒด (๋จ 0.1%์ ์ค์ฐจ๋ ํ์ฉํ์ง ์์ผ๋ฉฐ, ์คํจ ์ ์ฌ์๋๊ฐ ๋งค์ฐ ๋ณต์กํ ๋)
๋๊ด์ ๋ฝ์ ์ฐ๋ ๊ฒฝ์ฐ (์ผ๋ฐ ์ํฉ)
- ๋๋ถ๋ถ์ ๊ฒ์ํ, ํ๋กํ ์์ , ์ค์ ๋ณ๊ฒฝ
- ๋์ ์์ ํ๋ฅ ์ด ๋ฎ์ง๋ง ๋ฐ์ดํฐ ์ ํฉ์ฑ์ ์ง์ผ์ผ ํ ๋
- ์ฑ๋ฅ(์๋ต ์๋)์ด ๋งค์ฐ ์ค์ํ ๋๊ท๋ชจ ํธ๋ํฝ ์๋น์ค
์ถ๊ฐ ํ์ต
๋ถ์ฐ ๋ฝ (Distributed Lock)
- Redis์ Redlock ๋ฑ์ ์ด์ฉํด DB ๋ถํ๋ฅผ ์ค์ด๋ฉด์ ๋์์ฑ์ ์ ์ดํ๋ ๋ฐฉ๋ฒ.
๋ฉ์์ง ํ (Message Queue)
- ์์ฒญ์ ํ์ ์์ ์์ฐจ์ ์ผ๋ก ์ฒ๋ฆฌํจ์ผ๋ก์จ DB ๋ฝ ์์ด ๋์์ฑ์ ํด๊ฒฐํ๋ ๊ตฌ์กฐ.
๋ฉฑ๋ฑ์ฑ (Idempotency)
- ์ฌ๋ฌ ๋ฒ ์์ฒญํด๋ ๊ฒฐ๊ณผ๊ฐ ๊ฐ์ API ์ค๊ณ ๋ฐฉ์ (๋๊ด์ ๋ฝ ์คํจ ์ ์ฌ์๋์ ์ฐ๊ด).
๋ ๋์ค ๋ฝ ๐
- ์ฌ๋ฌ ๋์ ์๋ฒ๊ฐ ๊ณต์ ์์์ ์ ๊ทผํ๋ ํ๊ฒฝ์์ ์ฌ์ฉํ๋
- '๋ถ์ฐ ๋ฝ(Distributed Lock)'์ ๋ํ์ ์ธ ๊ตฌํ ๋ฐฉ์
- ์ผ๋ฐ์ ์ธ DB ๋ฝ์ ํน์ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ๋ ์ฝ๋๋ ํ
์ด๋ธ์ ๋ฝ์ ๊ฒ๋๋ค.
- ํ์ง๋ง ์๋น์ค ๊ท๋ชจ๊ฐ ์ปค์ ธ์ ์๋ฒ๊ฐ ์ฌ๋ฌ ๋(Distributed System)๊ฐ ๋๋ฉด
- ๊ฐ ์๋ฒ๊ฐ ์๋ก ๋ค๋ฅธ DB ์ปค๋ฅ์
์ ๊ฐ์ง๊ธฐ ๋๋ฌธ์ DB ๋ ๋ฒจ์ ๋ฝ๋ง์ผ๋ก๋ ํ๊ณ๊ฐ ๋ฐ์ํ ์ ์์ต๋๋ค.
- ์ด๋ Redis๋ผ๋ ์ธ๋ถ ์ ์ฅ์๋ฅผ ํ์ฉํด "ํ์ฌ ์ด ์์์ ๋ด๊ฐ ์ฌ์ฉ ์ค์ด๋ค"๋ผ๋ ํ์(Ticket)์
- ๋จ๊น์ผ๋ก์จ ์ฌ๋ฌ ์๋ฒ ๊ฐ์ ๋๊ธฐํ๋ฅผ ๋ง์ถ๋ ๊ฒ์ด ๋ฐ๋ก ๋ ๋์ค ๋ฝ์
๋๋ค.
ํต์ฌ ๋ช
๋ น์ด
SET resource_name my_random_value NX PX 30000
NX (Not eXists): key๊ฐ ์์ ๋๋ง ์ ์ฅํฉ๋๋ค. (๋ฝ ํ๋ ์๋)
PX 30000: 30,000 ๋ฐ๋ฆฌ์ด(30์ด) ํ์ ์๋์ผ๋ก ์ญ์ ํฉ๋๋ค. (๋ฐ๋๋ฝ ๋ฐฉ์ง)
my_random_value: ๋ฝ์ ํด์ ํ ๋ ๋ณธ์ธ์ด ์ก์ ๋ฝ์ธ์ง ํ์ธํ๊ธฐ ์ํ ๊ณ ์ ๊ฐ์
๋๋ค.
์ค๋ฌด์ ์ธ ๊ตฌํ ๋ฐฉ๋ฒ
-
Redisson ๋ผ์ด๋ธ๋ฌ๋ฆฌ
- ๋จ์ํ SETNX๋ฅผ ์ง์ ๊ตฌํํ๋ฉด ๋ฝ ๋ง๋ฃ ์๊ฐ ๊ด๋ฆฌ๋ ์ฌ์๋(Retry) ๋ก์ง์ ์ง์ ์ง์ผ ํ๋ฏ๋ก ๋งค์ฐ ๋ณต์ก
- ์๋ฐ(Spring) ์ง์์์๋ ์ด๋ฅผ ์ถ์ํํ Redisson ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ํ์ค์ฒ๋ผ ์ฌ์ฉํฉ๋๋ค.
-
Redisson์ ์ด์ฉํ ๋ถ์ฐ ๋ฝ ์์ ์ฝ๋
- Watchdog ๊ธฐ๋ฅ
- Redisson์ ํน์ง์ผ๋ก, ๋ก์ง์ด ๊ธธ์ด์ ธ ๋ฝ ๋ง๋ฃ ์๊ฐ์ด ๋ค๊ฐ์ค๋ฉด ์๋์ผ๋ก ์๊ฐ์ ์ฐ์ฅํด์ค๋๋ค.
- tryLock()
- ๋ฝ์ ํ๋ํ ๋๊น์ง ๋ฌดํ์ ๋๊ธฐํ์ง ์๊ณ
- ์ง์ ๋ ์๊ฐ(waitTime)๋งํผ๋ง ์๋ํฉ๋๋ค. ์ด๋ ์์คํ
์ ์ฒด์ ์ค๋ ๋ ์ ์ ๋ฅผ ๋ง์์ค๋๋ค.
- isHeldByCurrentThread()
- ๋ด๊ฐ ์ก์ง ์์ ๋ฝ์ ์ต์ง๋ก ํด์ ํ๋ ค๊ณ ํ๋ฉด ์๋ฌ๊ฐ ๋ฐ์ํ ์ ์์ผ๋ฏ๋ก, ๋ฐ๋์ ํ์ธ ํ ํด์ ํฉ๋๋ค.
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Service
public class StockService {
private final RedissonClient redissonClient;
public StockService(RedissonClient redissonClient) {
this.redissonClient = redissonClient;
}
public void decreaseStock(Long productId) {
// 1. ํด๋น ์์์ ๋ํ ๋ฝ ๊ฐ์ฒด๋ฅผ ๊ฐ์ ธ์ต๋๋ค.
RLock lock = redissonClient.getLock("lock:product:" + productId);
try {
// 2. ๋ฝ ํ๋ ์๋ (์ต๋ 10์ด ๋๊ธฐ, ํ๋ ํ 1์ด๊ฐ ์ ์ง)
// waitTime: ๋ฝ์ ์ป๊ธฐ ์ํด ๊ธฐ๋ค๋ฆฌ๋ ์๊ฐ
// leaseTime: ๋ฝ์ ํ๋ํ ํ ์ ์ ํ๋ ์๊ฐ
boolean available = lock.tryLock(10, 1, TimeUnit.SECONDS);
if (!available) {
System.out.println("๋ฝ ํ๋์ ์คํจํ์ต๋๋ค.");
return;
}
// 3. ๋น์ฆ๋์ค ๋ก์ง ์ํ (์ฌ๊ณ ๊ฐ์ ๋ฑ)
System.out.println("์ฌ๊ณ ๋ฅผ ๊ฐ์์ํต๋๋ค.");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
// 4. ๋ฐ๋์ ๋ฝ์ ํด์ ํฉ๋๋ค. (ํ์ฌ ์ฐ๋ ๋๊ฐ ๋ฝ์ ๊ฐ์ง๊ณ ์์ ๋๋ง)
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}
