๐Ÿ”„ WEB-13: Race Condition (TOCTOU)

Time of Check, Time of Use ๊ฐ„์˜ ์‹œ๊ฐ„์ฐจ๋กœ ์ฟ ํฐ ์ค‘๋ณต ์‚ฌ์šฉ ๊ฐ€๋Šฅ

๐Ÿ’ฐ ํ˜„์žฌ ์ž”์•ก

0์›

๐ŸŽŸ๏ธ ์ฟ ํฐ ์‚ฌ์šฉ

๐Ÿ’ก ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์ฟ ํฐ:
โ€ข COUPON20 (20,000์›)
โ€ข COUPON50 (50,000์›)
๐Ÿšจ Race Condition: ์—ฌ๋Ÿฌ ํƒญ/์ฐฝ์—์„œ ๋™์‹œ์— ๊ฐ™์€ ์ฟ ํฐ์„ ์‚ฌ์šฉํ•˜๋ฉด ์—ฌ๋Ÿฌ ๋ฒˆ ์ ์šฉ๋  ์ˆ˜ ์žˆ์Œ

๐Ÿ“‹ ์ฟ ํฐ ๋ชฉ๋ก

โš ๏ธ Race Condition ๊ณต๊ฒฉ

์‹œ๋‚˜๋ฆฌ์˜ค: ๋™์‹œ์— ๊ฐ™์€ ์ฟ ํฐ์„ ์—ฌ๋Ÿฌ ๋ฒˆ ์‚ฌ์šฉ

# Terminal 1
while true; do curl -X POST -d "action=use_coupon&coupon_code=COUPON50" http://localhost/web13_race.php; done

# Terminal 2 (๋™์‹œ ์‹คํ–‰)
while true; do curl -X POST -d "action=use_coupon&coupon_code=COUPON50" http://localhost/web13_race.php; done

๐Ÿ’ก ์›๋ฆฌ:
1. Thread A: ์ฟ ํฐ ํ™•์ธ (์‚ฌ์šฉ์•ˆ๋จ) โœ“
2. Thread B: ์ฟ ํฐ ํ™•์ธ (์•„์ง ์“ฐ์ง€ ์•Š์Œ) โœ“ โ† ๋™์‹œ์„ฑ ๋ฌธ์ œ
3. Thread A: ์ฟ ํฐ ์ ์šฉ & ๋งˆํฌ โœ“
4. Thread B: ์ฟ ํฐ ์ ์šฉ & ๋งˆํฌ โœ“ โ† ๊ฐ™์€ ์ฟ ํฐ ๋‘ ๋ฒˆ ์ ์šฉ๋จ!

๊ฒฐ๊ณผ: 50,000์› ์ฟ ํฐ์ด ๋‘ ๋ฒˆ ์ ์šฉ๋˜์–ด 100,000์› ํ• ์ธ

๐ŸŽฏ ๋‹ค๋ฅธ Race Condition ์˜ˆ์‹œ:
โ€ข ๊ณ„์ขŒ ์ถœ๊ธˆ (A๊ฐ€ 100๋งŒ์› ์ถœ๊ธˆ ์ค‘์ผ ๋•Œ B๋„ ๋™์‹œ์— 100๋งŒ์› ์ถœ๊ธˆ)
โ€ข ์ฃผ์‹ ๋งค์ˆ˜ (์ˆ˜๋Ÿ‰ ์ฒดํฌ ํ›„ ๋งค์ˆ˜ ์‚ฌ์ด์˜ ์‹œ๊ฐ„์ฐจ ์ด์šฉ)
โ€ข ์žฌ๊ณ  ๊ด€๋ฆฌ (์žฌ๊ณ  -1 ํ›„ ์ฃผ๋ฌธ ์‚ฌ์ด์˜ ์‹œ๊ฐ„์ฐจ)

โœ… ๋ฐฉ์–ด ๋ฐฉ๋ฒ•

1. ํŠธ๋žœ์žญ์…˜ & ์ž ๊ธˆ (Transaction with Locking):

BEGIN TRANSACTION;
SELECT * FROM coupons WHERE code = ? FOR UPDATE; // ํ–‰ ์ž ๊ธˆ
IF used = 0 THEN
  UPDATE coupons SET used = 1 WHERE code = ?;
  UPDATE user_balance SET balance = balance + ? WHERE id = ?;
COMMIT;

2. ์›์ž์  ์—ฐ์‚ฐ (Atomic Operation):

// ํ•œ ๋ฒˆ์˜ ์ฟผ๋ฆฌ๋กœ CHECK + USE ๋™์‹œ ์ˆ˜ํ–‰
$stmt = $db->prepare("UPDATE coupons SET used = 1 WHERE code = ? AND used = 0");
$stmt->execute([$code]);

// ์˜ํ–ฅ๋ฐ›์€ ํ–‰์ด 1์ด๋ฉด ์„ฑ๊ณต, 0์ด๋ฉด ์ด๋ฏธ ์‚ฌ์šฉ๋จ
if ($db->exec("SELECT changes()") === 1) {
  // ์ž”์•ก ์—…๋ฐ์ดํŠธ
}

3. ๋‚™๊ด€์  ์ž ๊ธˆ (Optimistic Locking):

// ๋ฒ„์ „ ์ปฌ๋Ÿผ ์ถ”๊ฐ€
$stmt = $db->prepare("UPDATE coupons SET used = 1, version = version + 1
                     WHERE code = ? AND version = ?");
$affected = $stmt->execute([$code, $current_version]);
if ($affected !== 1) die('Optimistic lock failed');

4. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ˆ˜์ค€์˜ UNIQUE ์ œ์•ฝ:

CREATE UNIQUE INDEX idx_coupon_usage
ON coupon_usage(coupon_id, user_id);

// ๊ฐ™์€ ์ฟ ํฐ์„ ๋‘ ๋ฒˆ ์‚ฌ์šฉํ•˜๋ ค ํ•˜๋ฉด UNIQUE ์ œ์•ฝ ์œ„๋ฐ˜์œผ๋กœ ์‹คํŒจ

5. ๋ถ„์‚ฐ ์ž ๊ธˆ (Distributed Lock):

// Redis, Memcached ๋“ฑ์œผ๋กœ ๊ตฌํ˜„
if (!$redis->setnx("coupon:$code:lock", 1)) {
  die('Coupon is being processed');
}
$redis->expire("coupon:$code:lock", 10); // 10์ดˆ ํƒ€์ž„์•„์›ƒ

// ... ์ฟ ํฐ ์ฒ˜๋ฆฌ ...

$redis->del("coupon:$code:lock"); // ์ž ๊ธˆ ํ•ด์ œ