๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์™€ ๋™์‹œ์„ฑ ์ œ์–ด

2026. 1. 1. 00:06ใ†Database

๋ฐ˜์‘ํ˜•

 

 

ํŠธ๋žœ์žญ์…˜์˜ ์—ญํ• 

ํŠธ๋žœ์žญ์…˜์ด๋ž€

ํŠธ๋žœ์žญ์…˜์€ ์—ฌ๋Ÿฌ ๊ฐœ์˜ ์กฐํšŒ๋‚˜ ์“ฐ๊ธฐ๋ฅผ ๋…ผ๋ฆฌ์ ์œผ๋กœ ํ•˜๋‚˜์˜ ์—ฐ์‚ฐ์œผ๋กœ ๋ฌถ๋Š” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์˜ ํ•ต์‹ฌ ๊ธฐ๋Šฅ์ž…๋‹ˆ๋‹ค.
๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค๋ฃจ๋Š” ๊ณผ์ •์—์„œ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋Š” ๋‹ค์–‘ํ•œ ๋™์‹œ์„ฑ ๋ฌธ์ œ๋ฅผ ์ฒ˜๋ฆฌํ•ด์ค๋‹ˆ๋‹ค.

 

ํŠธ๋žœ์žญ์…˜์˜ ํ•ต์‹ฌ ์›์น™

์›์ž์„ฑ (Atomicity)

  • ํ•˜๋‚˜์˜ ํŠธ๋žœ์žญ์…˜์— ํฌํ•จ๋œ ๋ชจ๋“  ์“ฐ๊ธฐ๋Š” ๋ชจ๋‘ ์ ์šฉ๋˜๊ฑฐ๋‚˜(์ปค๋ฐ‹) ๋ชจ๋‘ ์ทจ์†Œ(๋กค๋ฐฑ)๋ฉ๋‹ˆ๋‹ค
  • ํŠธ๋žœ์žญ์…˜ ์•ˆ์˜ ์ฟผ๋ฆฌ ์ค‘ ํ•˜๋‚˜๋ผ๋„ ์‹คํŒจํ•˜๋ฉด ์ „์ฒด ํŠธ๋žœ์žญ์…˜์„ ๋กค๋ฐฑํ•จ์œผ๋กœ์จ ๋ฐ์ดํ„ฐ๊ฐ€ ๊นจ์ง€๋Š” ๊ฒƒ์„ ๋ฐฉ์ง€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค
 
BEGIN;
    -- ์—ฌ๋Ÿฌ ์ž‘์—…์ด ํ•˜๋‚˜์˜ ๋…ผ๋ฆฌ์  ๋‹จ์œ„
    UPDATE account SET balance = balance - 1000 WHERE user_id = 1;
    UPDATE account SET balance = balance + 1000 WHERE user_id = 2;
COMMIT;  -- ๋ชจ๋‘ ์„ฑ๊ณตํ•˜๊ฑฐ๋‚˜, ํ•˜๋‚˜๋ผ๋„ ์‹คํŒจํ•˜๋ฉด ๋ชจ๋‘ ๋กค๋ฐฑ

 

๊ฒฉ๋ฆฌ์„ฑ (Isolation)

  • ๋™์‹œ์— ์‹คํ–‰๋˜๋Š” ์—ฌ๋Ÿฌ ํŠธ๋žœ์žญ์…˜์ด ์„œ๋กœ ๊ฐ„์„ญํ•˜์ง€ ์•Š๋„๋ก ๊ฒฉ๋ฆฌํ•ฉ๋‹ˆ๋‹ค
  • ํŠธ๋žœ์žญ์…˜์€ ๋งˆ์น˜ ํ˜ผ์ž ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ฒ˜๋Ÿผ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค

 

 

๋™์‹œ์„ฑ ๋ฌธ์ œ์˜ ์œ ํ˜•

1. Lost Update (๊ฐฑ์‹  ์†์‹ค)

๊ฐ€์žฅ ํ”ํ•˜๊ณ  ์น˜๋ช…์ ์ธ ๋ฌธ์ œ์ž…๋‹ˆ๋‹ค.
๋‘ ๊ฐœ์˜ ํŠธ๋žœ์žญ์…˜์ด ๊ฐ™์€ ๋ฐ์ดํ„ฐ๋ฅผ ์ฝ๊ณ  ์ˆ˜์ •ํ•  ๋•Œ, ๋‚˜์ค‘์— ์‹คํ–‰๋œ ํŠธ๋žœ์žญ์…˜์ด ๋จผ์ € ์‹คํ–‰๋œ ํŠธ๋žœ์žญ์…˜์˜ ๋ณ€๊ฒฝ์„ ๋ฎ์–ด์”๋‹ˆ๋‹ค.

# ์‚ฌ์šฉ์ž A์˜ ํฌ์ธํŠธ 1000P์—์„œ ๋™์‹œ์— ์ ๋ฆฝ(+500P)๊ณผ ์ฐจ๊ฐ(-300P) ๋ฐœ์ƒ
# ์ดˆ๊ธฐ: 1000P

์‹œ๊ฐ„ | ์ ๋ฆฝ ํŠธ๋žœ์žญ์…˜ (+500)    | ์ฐจ๊ฐ ํŠธ๋žœ์žญ์…˜ (-300)
-----|------------------------|-------------------------
T1   | SELECT point = 1000    |
T2   |                        | SELECT point = 1000
T3   | ๊ณ„์‚ฐ: 1000 + 500        |
T4   |                        | ๊ณ„์‚ฐ: 1000 - 300
T5   | UPDATE point = 1500    |
T6   |                        | UPDATE point = 700 (๋ฎ์–ด์”€!)

# ๊ฒฐ๊ณผ: 700P (๊ธฐ๋Œ€: 1200P) → 500P ์†์‹ค

2. Dirty Read (์˜ค์† ์ฝ๊ธฐ)

์ปค๋ฐ‹๋˜์ง€ ์•Š์€ ๋‹ค๋ฅธ ํŠธ๋žœ์žญ์…˜์˜ ๋ณ€๊ฒฝ์‚ฌํ•ญ์„ ์ฝ๋Š” ๋ฌธ์ œ์ž…๋‹ˆ๋‹ค.

3. Non-Repeatable Read (๋ฐ˜๋ณต ๋ถˆ๊ฐ€๋Šฅ ์ฝ๊ธฐ)

๊ฐ™์€ ํŠธ๋žœ์žญ์…˜ ๋‚ด์—์„œ ๊ฐ™์€ ๋ฐ์ดํ„ฐ๋ฅผ ๋‘ ๋ฒˆ ์ฝ์—ˆ์„ ๋•Œ ๋‹ค๋ฅธ ๊ฒฐ๊ณผ๊ฐ€ ๋‚˜์˜ค๋Š” ๋ฌธ์ œ์ž…๋‹ˆ๋‹ค.

4. Phantom Read (์œ ๋ น ์ฝ๊ธฐ)

๊ฐ™์€ ์กฐ๊ฑด์œผ๋กœ ์กฐํšŒํ–ˆ์„ ๋•Œ ์ด์ „์— ์—†๋˜ ํ–‰์ด ๋‚˜ํƒ€๋‚˜๊ฑฐ๋‚˜ ์žˆ๋˜ ํ–‰์ด ์‚ฌ๋ผ์ง€๋Š” ๋ฌธ์ œ์ž…๋‹ˆ๋‹ค.

 

 

 

ํŠธ๋žœ์žญ์…˜ ๊ฒฉ๋ฆฌ ์ˆ˜์ค€

PostgreSQL์€ 4๊ฐ€์ง€ ๊ฒฉ๋ฆฌ ์ˆ˜์ค€์„ ์ œ๊ณตํ•˜์—ฌ ๋™์‹œ์„ฑ๊ณผ ์ •ํ•ฉ์„ฑ์˜ ๊ท ํ˜•์„ ์กฐ์ ˆํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ฒฉ๋ฆฌ ์ˆ˜์ค€ Dirty Read Non-Repeatable Read Phantom Read ํŠน์ง•
READ UNCOMMITTED ๊ฐ€๋Šฅ ๊ฐ€๋Šฅ ๊ฐ€๋Šฅ PostgreSQL์—์„œ๋Š” READ COMMITTED์ฒ˜๋Ÿผ ๋™์ž‘
READ COMMITTED ๋ฐฉ์ง€ ๊ฐ€๋Šฅ ๊ฐ€๋Šฅ PostgreSQL ๊ธฐ๋ณธ๊ฐ’, ์ปค๋ฐ‹๋œ ๋ฐ์ดํ„ฐ๋งŒ ์ฝ์Œ
REPEATABLE READ ๋ฐฉ์ง€ ๋ฐฉ์ง€ ๋ฐฉ์ง€ ํŠธ๋žœ์žญ์…˜ ์‹œ์ž‘ ์‹œ์ ์˜ ์Šค๋ƒ…์ƒท ์‚ฌ์šฉ
SERIALIZABLE ๋ฐฉ์ง€ ๋ฐฉ์ง€ ๋ฐฉ์ง€ ์™„์ „ํ•œ ๊ฒฉ๋ฆฌ, ์„ฑ๋Šฅ ์ €ํ•˜
-- ๊ฒฉ๋ฆฌ ์ˆ˜์ค€ ์„ค์ •
BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- ์ž‘์—… ์ˆ˜ํ–‰
COMMIT;
 
 

๊ฒฉ๋ฆฌ ์ˆ˜์ค€๋งŒ์œผ๋กœ ํ•ด๊ฒฐ๋˜์ง€ ์•Š๋Š” ๋ฌธ์ œ

๊ฒฉ๋ฆฌ ์ˆ˜์ค€์„ ๋†’์—ฌ๋„ Lost Update ๋ฌธ์ œ๋Š” ์™„์ „ํžˆ ํ•ด๊ฒฐ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ด๋ฅผ ์œ„ํ•ด ๋ช…์‹œ์ ์ธ ์ž ๊ธˆ ๊ธฐ๋ฒ•์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

 

 

 

๋™์‹œ์„ฑ ์ œ์–ด ์ „๋žต

๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋Š” ๋™์‹œ์„ฑ ์ œ์–ด๋ฅผ ์œ„ํ•ด ํฌ๊ฒŒ ๋‘ ๊ฐ€์ง€ ์ž ๊ธˆ ๋ฐฉ์‹์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

1. ๋น„๊ด€์  ์ž ๊ธˆ (Pessimistic Lock)

๊ฐœ๋…

์„ ์  ์ž ๊ธˆ์ด๋ผ๊ณ ๋„ ํ•˜๋ฉฐ, ๋ฐ์ดํ„ฐ๋ฅผ ์ฝ๋Š” ์ˆœ๊ฐ„ ์ž ๊ธˆ์„ ํš๋“ํ•˜์—ฌ ๋‹ค๋ฅธ ํŠธ๋žœ์žญ์…˜์˜ ์ ‘๊ทผ์„ ์ฐจ๋‹จํ•˜๋Š” ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค. "์ถฉ๋Œ์ด ๋ฐœ์ƒํ•  ๊ฒƒ์ด๋‹ค"๋ผ๊ณ  ๋น„๊ด€์ ์œผ๋กœ ๊ฐ€์ •ํ•˜๊ณ  ๋ฏธ๋ฆฌ ์ž ๊ธˆ์„ ๊ฒ๋‹ˆ๋‹ค.

๋™์ž‘ ์›๋ฆฌ

  1. ํŠธ๋žœ์žญ์…˜ 1์ด ๋จผ์ € ๋ฐ์ดํ„ฐ์— ๋Œ€ํ•œ ์ž ๊ธˆ์„ ๊ตฌํ•˜๊ณ ,
  2. ํŠธ๋žœ์žญ์…˜ 2๋Š” ํŠธ๋žœ์žญ์…˜ 1์ด ๋๋‚œ ๋’ค์— ์™„๋ฃŒํ•ด์„œ ์ž ๊ธˆ์„ ๋ฐ˜ํ™˜ํ•  ๋•Œ๊นŒ์ง€ ๋Œ€๊ธฐํ•˜์—ฌ ์ผ๊ด€์„ฑ์ด ๊นจ์ง€๋Š” ๋ฌธ์ œ๋ฅผ ๋ฐฉ์ง€.

 

 

  1. ์ฃผ๋ฌธ ์ทจ์†Œ ํŠธ๋žœ์žญ์…˜์ด ๋จผ์ € ์ฃผ๋ฌธ ์ƒํƒœ๋ฅผ ์ทจ์†Œ ์ƒํƒœ๋กœ ๋ณ€๊ฒฝ.
  2. ๋ฐฐ์†ก ์‹œ์ž‘ ํŠธ๋žœ์žญ์…˜์ด ์ฃผ๋ฌธ ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•˜๋ฉด ์ทจ์†Œ๋œ ์ƒํƒœ์ด๋ฏ€๋กœ, ๋ฐฐ์†ก ์‹œ์ž‘ ์‹คํŒจ

 

 

 


FOR UPDATE ์˜ต์…˜

-- ํฌ์ธํŠธ ์ ๋ฆฝ
BEGIN;

-- 1. ํ–‰ ์ž ๊ธˆ ํš๋“ (๋‹ค๋ฅธ ํŠธ๋žœ์žญ์…˜์€ ๋Œ€๊ธฐ)
SELECT balance FROM point 
WHERE user_id = 1 
FOR UPDATE;  -- ๋‹ค๋ฅธ ํŠธ๋žœ์žญ์…˜ ์ฐจ๋‹จ

-- 2. ์•ˆ์ „ํ•˜๊ฒŒ ์—…๋ฐ์ดํŠธ (๋…์  ์ƒํƒœ)
UPDATE point 
SET balance = balance + 500 
WHERE user_id = 1
RETURNING balance;

INSERT INTO point_history (user_id, amount, reason, balance_after)
VALUES (1, 500, '๊ตฌ๋งค ์ ๋ฆฝ', 1500);

COMMIT; -- ์ž ๊ธˆ ํ•ด์ œ

 

๋ช…๋ น์–ด ์„ค๋ช… ์ฐจ๋‹จ ๋Œ€์ƒ
FOR UPDATE ์“ฐ๊ธฐ ์ž ๊ธˆ ๋‹ค๋ฅธ ๋ชจ๋“  ์ž ๊ธˆ ์ฐจ๋‹จ
FOR NO KEY UPDATE ์™ธ๋ž˜ ํ‚ค ์ œ์™ธ ์“ฐ๊ธฐ ์ž ๊ธˆ UPDATE ์ฐจ๋‹จ, ์™ธ๋ž˜ ํ‚ค ์ฐธ์กฐ๋Š” ํ—ˆ์šฉ
FOR SHARE ์ฝ๊ธฐ ์ž ๊ธˆ UPDATE/DELETE ์ฐจ๋‹จ, SELECT๋Š” ํ—ˆ์šฉ
FOR KEY SHARE ์™ธ๋ž˜ ํ‚ค์šฉ ์ฝ๊ธฐ ์ž ๊ธˆ DELETE ์ฐจ๋‹จ, UPDATE๋Š” ํ—ˆ์šฉ

 

-- FOR UPDATE: ์™„์ „ํ•œ ์“ฐ๊ธฐ ์ž ๊ธˆ
SELECT * FROM orders WHERE order_id = 1 FOR UPDATE;

-- FOR SHARE: ์ฝ๊ธฐ๋Š” ํ—ˆ์šฉ, ์“ฐ๊ธฐ๋งŒ ์ฐจ๋‹จ
SELECT * FROM products WHERE product_id = 1 FOR SHARE;



์ž ๊ธˆ ํƒ€์ž„์•„์›ƒ ์„ค์ •

-- ์ž ๊ธˆ ๋Œ€๊ธฐ ์‹œ๊ฐ„ ์ œํ•œ (5์ดˆ)
SET lock_timeout = '5s';

BEGIN;
SELECT * FROM account WHERE user_id = 1 FOR UPDATE;
-- 5์ดˆ ์ด์ƒ ๋Œ€๊ธฐํ•˜๋ฉด ์˜ˆ์™ธ ๋ฐœ์ƒ
 

์žฅ์ 

  • ์ •ํ•ฉ์„ฑ 100% ๋ณด์žฅ: ๋™์‹œ ์ˆ˜์ • ๋ถˆ๊ฐ€๋Šฅ
  • ๊ตฌํ˜„์ด ๋‹จ์ˆœ: SELECT์— FOR UPDATE๋งŒ ์ถ”๊ฐ€
  • ์˜ˆ์ธก ๊ฐ€๋Šฅ: ์ˆœ์ฐจ์ ์œผ๋กœ ์ฒ˜๋ฆฌ๋˜์–ด ๊ฒฐ๊ณผ ์˜ˆ์ธก ๊ฐ€๋Šฅ

๋‹จ์ 

  • ๋Œ€๊ธฐ ์‹œ๊ฐ„ ์ฆ๊ฐ€: ๋™์‹œ ์š”์ฒญ์ด ๋งŽ์œผ๋ฉด ์ˆœ์ฐจ ๋Œ€๊ธฐ
  • ์ฒ˜๋ฆฌ๋Ÿ‰ ์ œํ•œ: ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ ๋ถˆ๊ฐ€๋Šฅ
  • ๋ฐ๋“œ๋ฝ ์œ„ํ—˜: ์—ฌ๋Ÿฌ ์ž์› ์ž ๊ธˆ ์‹œ ์ˆœ์„œ ๊ด€๋ฆฌ ํ•„์š”

์ ํ•ฉํ•œ ์ƒํ™ฉ

  • ์žฌ๊ณ  ์ฐจ๊ฐ, ํฌ์ธํŠธ ์ฐจ๊ฐ ๋“ฑ ์ •ํ•ฉ์„ฑ์ด ์ ˆ๋Œ€์ ์œผ๋กœ ์ค‘์š”ํ•œ ๊ฒฝ์šฐ
  • ๊ฒฐ์ œ ์ฒ˜๋ฆฌ, ์ขŒ์„ ์˜ˆ์•ฝ ๋“ฑ ์ถฉ๋Œ์ด ๋นˆ๋ฒˆํ•˜๊ฒŒ ๋ฐœ์ƒํ•˜๋Š” ๊ฒฝ์šฐ
  • ๊ธˆ์ „ ๊ฑฐ๋ž˜, ํšŒ๊ณ„ ๋ฐ์ดํ„ฐ ๋“ฑ ๋ฐ์ดํ„ฐ ์†์‹ค์ด ํ—ˆ์šฉ๋˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ

 

 

2. ๋‚™๊ด€์  ์ž ๊ธˆ (Optimistic Lock)

๊ฐœ๋…

๋น„์„ ์  ์ž ๊ธˆ์ด๋ผ๊ณ ๋„ ํ•˜๋ฉฐ, ์ž ๊ธˆ ์—†์ด ๋ฐ์ดํ„ฐ๋ฅผ ์ฝ๊ณ  ์ˆ˜์ •ํ•  ๋•Œ ๊ฐ’์„ ๋น„๊ตํ•ด์„œ ์ˆ˜์ •ํ•˜๋Š” ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค.
"์ถฉ๋Œ์ด ๊ฑฐ์˜ ์—†์„ ๊ฒƒ์ด๋‹ค"๋ผ๊ณ  ๋‚™๊ด€์ ์œผ๋กœ ๊ฐ€์ •ํ•˜๊ณ  ์ถฉ๋Œ์ด ๋ฐœ์ƒํ•˜๋ฉด ๊ทธ๋•Œ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

 

๋™์ž‘ ์›๋ฆฌ

  1. ๋น„์„ ์  ์ž ๊ธˆ์€ ์šฐ์„  ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ์„ ์‹œ๋„
  2. ์กฐํšŒํ•œ ๋ฒ„์ „ ๊ฐ’์„ ๋น„๊ตํ•ด์„œ ๋ฐ์ดํ„ฐ๊ฐ€ ๋ณ€๊ฒฝ๋˜์ง€ ์•Š์•˜๋Š”์ง€๋ฅผ ํ™•์ธํ•˜๊ณ  ์ปค๋ฐ‹ํ•˜๊ฑฐ๋‚˜ ๋กค๋ฐฑ ์‹คํ–‰

 

 

 

 

PostgreSQL ๊ตฌํ˜„

๋‚™๊ด€์  ์ž ๊ธˆ์˜ ํ•ต์‹ฌ์€ ์ฟผ๋ฆฌ ์‹คํ–‰์„ ๋ง‰์ง€ ์•Š์œผ๋ฉด์„œ ๋ฐ์ดํ„ฐ๊ฐ€ ์ž˜๋ชป ๋ณ€๊ฒฝ๋˜๋Š” ๊ฒƒ์„ ๋ฐฉ์ง€ํ•œ๋‹ค๋Š” ์ !

-- ํฌ์ธํŠธ ์ ๋ฆฝ
BEGIN;

-- ํ˜„์žฌ ๋ฒ„์ „ ์กฐํšŒ (์ž ๊ธˆ ์—†์Œ)
SELECT balance, version FROM point WHERE user_id = 1;
-- ๊ฒฐ๊ณผ: balance=1000, version=5

-- ๋ฒ„์ „ ํ™•์ธํ•˜๋ฉฐ ์—…๋ฐ์ดํŠธ
UPDATE point 
SET balance = balance + 500,
    version = version + 1
WHERE user_id = 1 
  AND version = 5;  -- ๋ฒ„์ „ ๋ถˆ์ผ์น˜ ์‹œ ์‹คํŒจ → ์žฌ์‹œ๋„

INSERT INTO point_history (user_id, amount, reason, balance_after)
VALUES (1, 500, '๊ตฌ๋งค ์ ๋ฆฝ', 1500);

COMMIT;โ€‹
-- ๋น„๊ด€์  ์ž ๊ธˆ: SELECT ์‹œ์ ์— ๋‹ค๋ฅธ ํŠธ๋žœ์žญ์…˜ ์ฐจ๋‹จ
SELECT * FROM account WHERE user_id = 1 FOR UPDATE;
-- ↑ ๋‹ค๋ฅธ ํŠธ๋žœ์žญ์…˜์€ ์—ฌ๊ธฐ์„œ ๋Œ€๊ธฐ

-- ๋‚™๊ด€์  ์ž ๊ธˆ: SELECT๋Š” ๋ชจ๋‘ ํ—ˆ์šฉ
SELECT * FROM account WHERE user_id = 1;
-- ↑ ์—ฌ๋Ÿฌ ํŠธ๋žœ์žญ์…˜์ด ๋™์‹œ ์กฐํšŒ ๊ฐ€๋Šฅ
-- UPDATE ์‹œ์ ์—๋งŒ version์œผ๋กœ ๊ฒ€์ฆ

 

 

 

์žฌ์‹œ๋„ ๋กœ์ง (ํ•„์ˆ˜)

์ถฉ๋Œ ๋ฐœ์ƒ ์‹œ ์žฌ์‹œ๋„ ๋กœ์ง์ด ๋ฐ˜๋“œ์‹œ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

-- ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋ ˆ๋ฒจ ์žฌ์‹œ๋„ ๋กœ์ง ์˜ˆ์‹œ
DO $$
DECLARE
    retry_count INT := 0;
    max_retries INT := 3;
    rows_affected INT;
    current_version BIGINT;
    current_balance INT;
BEGIN
    LOOP
        -- ํ˜„์žฌ ๊ฐ’ ์กฐํšŒ
        SELECT balance, version INTO current_balance, current_version
        FROM account WHERE user_id = 1;
        
        -- ์—…๋ฐ์ดํŠธ ์‹œ๋„
        UPDATE account 
        SET balance = current_balance - 300,
            version = version + 1
        WHERE user_id = 1 
          AND version = current_version;
        
        GET DIAGNOSTICS rows_affected = ROW_COUNT;
        
        IF rows_affected > 0 THEN
            EXIT;  -- ์„ฑ๊ณต ์‹œ ์ข…๋ฃŒ
        END IF;
        
        -- ์žฌ์‹œ๋„ ์นด์šดํŠธ ์ฆ๊ฐ€
        retry_count := retry_count + 1;
        IF retry_count >= max_retries THEN
            RAISE EXCEPTION '์ตœ๋Œ€ ์žฌ์‹œ๋„ ํšŸ์ˆ˜ ์ดˆ๊ณผ';
        END IF;
        
        -- ์งง์€ ๋Œ€๊ธฐ ํ›„ ์žฌ์‹œ๋„
        PERFORM pg_sleep(0.01 * retry_count);  -- ์ง€์ˆ˜ ๋ฐฑ์˜คํ”„
    END LOOP;
END $$;
 

์žฅ์ 

  • ๋†’์€ ๋™์‹œ์„ฑ: ์ž ๊ธˆ ๋Œ€๊ธฐ ์—†์ด ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ ๊ฐ€๋Šฅ
  • ์ฝ๊ธฐ ์„ฑ๋Šฅ ์šฐ์ˆ˜: SELECT ์‹œ ์ž ๊ธˆ ํš๋“ ๋ถˆํ•„์š”
  • ๋ฐ๋“œ๋ฝ ์—†์Œ: ์ž ๊ธˆ์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š์•„ ๋ฐ๋“œ๋ฝ ๋ฐœ์ƒ ์•ˆ ํ•จ

๋‹จ์ 

  • ์žฌ์‹œ๋„ ๋กœ์ง ํ•„์ˆ˜: ์ถฉ๋Œ ์‹œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋ ˆ๋ฒจ ์ฒ˜๋ฆฌ ํ•„์š”
  • version ๊ด€๋ฆฌ: ๋ณ„๋„ ์ปฌ๋Ÿผ ์ถ”๊ฐ€ ๋ฐ ๊ด€๋ฆฌ ํ•„์š”
  • ์ถฉ๋Œ ๋นˆ๋ฒˆ ์‹œ ๋น„ํšจ์œจ: ์žฌ์‹œ๋„๊ฐ€ ๋งŽ์•„์ง€๋ฉด ์˜คํžˆ๋ ค ์„ฑ๋Šฅ ์ €ํ•˜

์ ํ•ฉํ•œ ์ƒํ™ฉ

  • ๊ฒŒ์‹œ๋ฌผ ์กฐํšŒ์ˆ˜, ์ข‹์•„์š” ๋“ฑ ์ถฉ๋Œ์ด ์ ๊ณ  ๊ฐ€๋” ์‹คํŒจํ•ด๋„ ๊ดœ์ฐฎ์€ ๊ฒฝ์šฐ
  • ํ†ต๊ณ„ ๋ฐ์ดํ„ฐ, ์บ์‹œ ๊ฐฑ์‹  ๋“ฑ ์ •ํ•ฉ์„ฑ๋ณด๋‹ค ์„ฑ๋Šฅ์ด ์ค‘์š”ํ•œ ๊ฒฝ์šฐ
  • ์ฝ๊ธฐ๊ฐ€ ๋งŽ๊ณ  ์“ฐ๊ธฐ๊ฐ€ ์ ์€ ๊ฒฝ์šฐ

 



 

3. ์›์ž์  ์—ฐ์‚ฐ (์ถ”๊ฐ€ ์ „๋žต)

๊ฐœ๋…

SELECT ์—†์ด UPDATE ๋ฌธ์—์„œ ์ง์ ‘ ๊ณ„์‚ฐํ•˜์—ฌ ์ฝ๊ธฐ-์“ฐ๊ธฐ ๊ฐญ์„ ์›์ฒœ์ ์œผ๋กœ ์ œ๊ฑฐํ•˜๋Š” ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—”์ง„์ด ๋‚ด๋ถ€์ ์œผ๋กœ ๋™์‹œ์„ฑ์„ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค.

 

PostgreSQL ๊ตฌํ˜„

-- ์ฝ๊ธฐ ์—†์ด ๋ฐ”๋กœ ์—…๋ฐ์ดํŠธ
BEGIN;

UPDATE point 
SET balance = balance + 500 
WHERE user_id = 1
RETURNING balance; -- ์—…๋ฐ์ดํŠธ ํ›„ ๊ฐ’ ๋ฐ˜ํ™˜ ํ•„์ˆ˜!

INSERT INTO point_history (user_id, amount, reason, balance_after)
VALUES (1, 500, '๊ตฌ๋งค ์ ๋ฆฝ', 1500);

COMMIT;โ€‹

 

์žฅ์ 

  • ๊ฐ€์žฅ ์•ˆ์ „ํ•˜๊ณ  ๋น ๋ฆ„: ๋‚ด๋ถ€์ ์œผ๋กœ ๋™์‹œ์„ฑ ๋ณด์žฅ
  • ์ฝ”๋“œ ๊ฐ„๊ฒฐ: ์ถ”๊ฐ€ ๋กœ์ง ๋ถˆํ•„์š”
  • ์ž ๊ธˆ ๋ถˆํ•„์š”: ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๊ฐ€ ์•Œ์•„์„œ ์ฒ˜๋ฆฌ

๋‹จ์ 

  • ๋‹จ์ˆœ ์—ฐ์‚ฐ๋งŒ ๊ฐ€๋Šฅ: ๋ณต์žกํ•œ ๊ณ„์‚ฐ์ด๋‚˜ ์กฐ๊ฑด ์ฒ˜๋ฆฌ ๋ถˆ๊ฐ€
  • ๊ฒฐ๊ณผ๊ฐ’ ํ•„์š” ์‹œ: RETURNING ์ ˆ ์‚ฌ์šฉ ํ•„์ˆ˜

 

 

 

 

๋ฐ๋“œ๋ฝ (Deadlock) ๋ฐฉ์ง€

๋ฐ๋“œ๋ฝ์ด๋ž€

๋‘ ๊ฐœ ์ด์ƒ์˜ ํŠธ๋žœ์žญ์…˜์ด ์„œ๋กœ๊ฐ€ ๋ณด์œ ํ•œ ์ž ๊ธˆ์„ ๊ธฐ๋‹ค๋ฆฌ๋ฉฐ ๋ฌดํ•œ ๋Œ€๊ธฐ ์ƒํƒœ์— ๋น ์ง€๋Š” ํ˜„์ƒ์ž…๋‹ˆ๋‹ค.

ํŠธ๋žœ์žญ์…˜ A              ํŠธ๋žœ์žญ์…˜ B
-----------------      -----------------
์ž ๊ธˆ(์ž์›1)            ์ž ๊ธˆ(์ž์›2)
๋Œ€๊ธฐ(์ž์›2) ←-------→  ๋Œ€๊ธฐ(์ž์›1)

 

 

๋ฐ๋“œ๋ฝ ๋ฐœ์ƒ ์˜ˆ์‹œ

-- ํŠธ๋žœ์žญ์…˜ A
BEGIN;
UPDATE account SET balance = balance - 100 WHERE user_id = 1;
UPDATE account SET balance = balance + 100 WHERE user_id = 2;  -- ๋Œ€๊ธฐ
COMMIT;

-- ํŠธ๋žœ์žญ์…˜ B (๋™์‹œ ์‹คํ–‰)
BEGIN;
UPDATE account SET balance = balance - 100 WHERE user_id = 2;
UPDATE account SET balance = balance + 100 WHERE user_id = 1;  -- ๋ฐ๋“œ๋ฝ!
COMMIT;
 

 
 

๋ฐ๋“œ๋ฝ ๋ฐฉ์ง€ ์ „๋žต

1. ์ž ๊ธˆ ์ˆœ์„œ ํ†ต์ผ

์—ฌ๋Ÿฌ ์ž์›์„ ์ž ๊ธ€ ๋•Œ ํ•ญ์ƒ ๊ฐ™์€ ์ˆœ์„œ๋กœ ์ž ๊ธˆ์„ ํš๋“ํ•ฉ๋‹ˆ๋‹ค.

-- โœ… ์˜ฌ๋ฐ”๋ฅธ ์˜ˆ: user_id ์˜ค๋ฆ„์ฐจ์ˆœ์œผ๋กœ ์ž ๊ธˆ
BEGIN;
SELECT * FROM account 
WHERE user_id IN (1, 2) 
ORDER BY user_id ASC  -- ํ•ญ์ƒ ์ž‘์€ ๋ฒˆํ˜ธ๋ถ€ํ„ฐ
FOR UPDATE;

UPDATE account SET balance = balance - 100 WHERE user_id = 1;
UPDATE account SET balance = balance + 100 WHERE user_id = 2;
COMMIT;
 
 

2. ํƒ€์ž„์•„์›ƒ ์„ค์ •

-- ๋ฐ๋“œ๋ฝ ๊ฐ์ง€ ์‹œ๊ฐ„ ์„ค์ • (๊ธฐ๋ณธ 1์ดˆ)
SET deadlock_timeout = '1s';

-- ์ž ๊ธˆ ๋Œ€๊ธฐ ์ตœ๋Œ€ ์‹œ๊ฐ„ ์„ค์ •
SET lock_timeout = '5s';

 

 

3. ํŠธ๋žœ์žญ์…˜ ์‹œ๊ฐ„ ์ตœ์†Œํ™”

-- โŒ ๋‚˜์œ ์˜ˆ: ํŠธ๋žœ์žญ์…˜ ์•ˆ์—์„œ ์™ธ๋ถ€ API ํ˜ธ์ถœ
BEGIN;
SELECT * FROM orders WHERE order_id = 1 FOR UPDATE;
-- ์™ธ๋ถ€ ๊ฒฐ์ œ API ํ˜ธ์ถœS (3์ดˆ ์†Œ์š”)
UPDATE orders SET status = 'paid';
COMMIT;

-- โœ… ์ข‹์€ ์˜ˆ: ํŠธ๋žœ์žญ์…˜ ์ „์— ์™ธ๋ถ€ ์ž‘์—… ์™„๋ฃŒ
-- ์™ธ๋ถ€ ๊ฒฐ์ œ API ํ˜ธ์ถœ
BEGIN;
SELECT * FROM orders WHERE order_id = 1 FOR UPDATE;
UPDATE orders SET status = 'paid';
COMMIT;

 

 

 

 

 

์‹ค๋ฌด ์ ์šฉ ์‹œ๋‚˜๋ฆฌ์˜ค

์‹œ๋‚˜๋ฆฌ์˜ค 1: ํฌ์ธํŠธ ์ฐจ๊ฐ (๋น„๊ด€์  ์ž ๊ธˆ ํ•„์ˆ˜)

BEGIN;

-- 1. ํฌ์ธํŠธ ์กฐํšŒ ๋ฐ ์ž ๊ธˆ ํš๋“ (๋‹ค๋ฅธ ํŠธ๋žœ์žญ์…˜ ์ฐจ๋‹จ)
SELECT user_id, balance 
FROM point 
WHERE user_id = 1 
FOR UPDATE;
-- ๊ฒฐ๊ณผ: balance = 10000

-- 2. ์ž”์•ก ํ™•์ธ ๋ฐ ์ฐจ๊ฐ
UPDATE point 
SET balance = balance - 5000,
    updated_at = CURRENT_TIMESTAMP 
WHERE user_id = 1 
  AND balance >= 5000;  -- ์ž”์•ก ๋ถ€์กฑ ๋ฐฉ์ง€

-- 3. ์—…๋ฐ์ดํŠธ ์‹คํŒจ ์‹œ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ
IF NOT FOUND THEN
    ROLLBACK;
    RAISE EXCEPTION 'ํฌ์ธํŠธ ์ž”์•ก์ด ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค';
END IF;

-- 4. ์ฐจ๊ฐ ์ด๋ ฅ ์ €์žฅ
INSERT INTO point_history (user_id, amount, reason, balance_after)
SELECT 1, -5000, '์ƒํ’ˆ ๊ตฌ๋งค (์ฃผ๋ฌธ๋ฒˆํ˜ธ: ORD-12345)', balance 
FROM point 
WHERE user_id = 1;

COMMIT;  -- ์ž ๊ธˆ ํ•ด์ œ
-- ๋‹ค๋ฅธ ํŠธ๋žœ์žญ์…˜์€ ์ž ๊ธˆ ํ•ด์ œ๊นŒ์ง€ ๋Œ€๊ธฐํ•˜๋ฏ€๋กœ ๋™์‹œ ์ฐจ๊ฐ ์‹œ์—๋„ ์ •ํ•ฉ์„ฑ ๋ณด์žฅ
 

 

์‹œ๋‚˜๋ฆฌ์˜ค 2: ์กฐํšŒ์ˆ˜ ์ฆ๊ฐ€ (๋‚™๊ด€์  ์ž ๊ธˆ ๋˜๋Š” ์›์ž์  ์—ฐ์‚ฐ)

-- ๋ฐฉ๋ฒ• 1: ์›์ž์  ์—ฐ์‚ฐ (๊ถŒ์žฅ)
UPDATE posts 
SET view_count = view_count + 1 
WHERE post_id = 1;

-- ๋ฐฉ๋ฒ• 2: ๋‚™๊ด€์  ์ž ๊ธˆ
UPDATE posts 
SET view_count = view_count + 1,
    version = version + 1 
WHERE post_id = 1 
  AND version = 5;
 

 

์‹œ๋‚˜๋ฆฌ์˜ค 3: ์œ ์ € ๊ฐ„ ํฌ์ธํŠธ ์ด๋™(์ˆœ์„œ ๋ณด์žฅ)

ํ•ต์‹ฌ ํฌ์ธํŠธ:

  • ์ˆœ์„œ ๋ณด์žฅ: ORDER BY user_id ASC : ๋ชจ๋“  ํŠธ๋žœ์žญ์…˜์ด ๋™์ผํ•œ ์ˆœ์„œ(์ž‘์€ ๋ฒˆํ˜ธ → ํฐ ๋ฒˆํ˜ธ)๋กœ ์ž ๊ธˆ ํš๋“
  • ์—ฌ๋Ÿฌ ํ–‰์„ ํ•œ ๋ฒˆ์— ์ž ๊ธˆ: WHERE user_id IN (1, 2)๋กœ ํ•„์š”ํ•œ ๋ชจ๋“  ํ–‰์„ ๋จผ์ €
BEGIN;

-- 1. user_id ์˜ค๋ฆ„์ฐจ์ˆœ์œผ๋กœ ์ž ๊ธˆ ํš๋“ (๋ฐ๋“œ๋ฝ ๋ฐฉ์ง€)
SELECT user_id, balance 
FROM point 
WHERE user_id IN (1, 2) 
ORDER BY user_id ASC  -- ํ•ญ์ƒ ์ž‘์€ ๋ฒˆํ˜ธ๋ถ€ํ„ฐ ์ž ๊ธˆ
FOR UPDATE;

-- 2. ์†ก์‹ ์ž(A) ์ž”์•ก ํ™•์ธ
DECLARE
    sender_balance INT;
BEGIN
    SELECT balance INTO sender_balance 
    FROM point 
    WHERE user_id = 1;
    
    IF sender_balance < 1000 THEN
        RAISE EXCEPTION '์†ก์‹ ์ž์˜ ํฌ์ธํŠธ ์ž”์•ก์ด ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค';
    END IF;
END;

-- 3. ์†ก์‹ ์ž(A) ์ฐจ๊ฐ
UPDATE point 
SET balance = balance - 1000,
    updated_at = CURRENT_TIMESTAMP 
WHERE user_id = 1;

INSERT INTO point_history (user_id, amount, reason, balance_after)
SELECT 1, -1000, 'ํฌ์ธํŠธ ์„ ๋ฌผ (๋ฐ›๋Š” ์‚ฌ๋žŒ: user_2)', balance 
FROM point 
WHERE user_id = 1;

-- 4. ์ˆ˜์‹ ์ž(B) ์ ๋ฆฝ
UPDATE point 
SET balance = balance + 1000,
    updated_at = CURRENT_TIMESTAMP 
WHERE user_id = 2;

INSERT INTO point_history (user_id, amount, reason, balance_after)
SELECT 2, 1000, 'ํฌ์ธํŠธ ์„ ๋ฌผ ๋ฐ›์Œ (๋ณด๋‚ธ ์‚ฌ๋žŒ: user_1)', balance 
FROM point 
WHERE user_id = 2;

COMMIT;
[๋ฐ๋“œ๋ฝ ๋ฐœ์ƒ]
ํŠธ๋žœ์žญ์…˜ A        ํŠธ๋žœ์žญ์…˜ B
์ž ๊ธˆ(user_1)      ์ž ๊ธˆ(user_2)
๋Œ€๊ธฐ(user_2) ←-→  ๋Œ€๊ธฐ(user_1)  ← ๋ฐ๋“œ๋ฝ!

[์ˆœ์„œ ๋ณด์žฅ์œผ๋กœ ๋ฐฉ์ง€]
ํŠธ๋žœ์žญ์…˜ A               ํŠธ๋žœ์žญ์…˜ B
์ž ๊ธˆ(user_1, user_2)    ๋Œ€๊ธฐ...
์ž‘์—… ์™„๋ฃŒ                ๋Œ€๊ธฐ...
์ž ๊ธˆ ํ•ด์ œ                ์ž ๊ธˆ(user_1, user_2)
                       ์ž‘์—… ์™„๋ฃŒ
                       ์ž ๊ธˆ ํ•ด์ œ

 

 

๊ฒฐ๋ก 

  1. ํŠธ๋žœ์žญ์…˜์€ ์—ฌ๋Ÿฌ ์ฟผ๋ฆฌ๋ฅผ ํ•˜๋‚˜์˜ ๋…ผ๋ฆฌ์  ๋‹จ์œ„๋กœ ๋ฌถ์–ด ๋™์‹œ์„ฑ ๋ฌธ์ œ๋ฅผ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค
  2. ํ•˜๋‚˜์˜ ํŠธ๋žœ์žญ์…˜์€ ๋ชจ๋‘ ์„ฑ๊ณตํ•˜๊ฑฐ๋‚˜ ๋ชจ๋‘ ์‹คํŒจํ•ฉ๋‹ˆ๋‹ค (์›์ž์„ฑ)
  3. ๊ฒฉ๋ฆฌ ์ˆ˜์ค€๋งŒ์œผ๋กœ๋Š” Lost Update ๋ฌธ์ œ๋ฅผ ์™„์ „ํžˆ ํ•ด๊ฒฐํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค
  4. ๋น„๊ด€์  ์ž ๊ธˆ(์„ ์  ์ž ๊ธˆ)์€ ๋ฐ์ดํ„ฐ๋ฅผ ์ฝ๋Š” ์ˆœ๊ฐ„ ์ž ๊ธˆ์„ ๊ฑธ์–ด ์ถฉ๋Œ์„ ๋ฐฉ์ง€ํ•ฉ๋‹ˆ๋‹ค
  5. ๋‚™๊ด€์  ์ž ๊ธˆ(๋น„์„ ์  ์ž ๊ธˆ)์€ ์ฟผ๋ฆฌ ์‹คํ–‰ ์ž์ฒด๋Š” ๋ง‰์ง€ ์•Š์œผ๋ฉด์„œ version ๋น„๊ต๋กœ ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ์„ ๋ฐฉ์ง€ํ•ฉ๋‹ˆ๋‹ค
  6. ์ •ํ•ฉ์„ฑ์ด ์ค‘์š”ํ•˜๋ฉด ๋น„๊ด€์  ์ž ๊ธˆ, ์„ฑ๋Šฅ์ด ์ค‘์š”ํ•˜๊ณ  ์ถฉ๋Œ์ด ์ ์œผ๋ฉด ๋‚™๊ด€์  ์ž ๊ธˆ์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค

 

 

 

DB ๋ ˆ๋ฒจ์˜ ๋™์‹œ์„ฑ ์ œ์–ด ์™ธ์—๋„, ์‹œ์Šคํ…œ ์•ˆ์ •์„ฑ๊ณผ ์‹ค๋ฌด ์œ ์—ฐ์„ฑ์„ ์œ„ํ•ด ๋‹ค๋ฅธ ๊ด€์ ๋“ค๋„ ํ•จ๊ป˜ ๊ณ ๋ คํ•ด๋ณด์•„์•ผ ํ•œ๋‹ค!
์˜ˆ๋ฅผ๋“ค๋ฉด ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋ ˆ๋ฒจ ๋ฝ (Java/Spring ๊ธฐ๋ฐ˜), ๋ถ„์‚ฐ ๋ฝ ์‹œ์Šคํ…œ ๋“ฑ
ํŠธ๋ž˜ํ”ฝ์ด ๋งŽ์ด ๋ฐœ์ƒํ•˜๋Š” ์‹ค์„œ๋น„์Šค์—์„œ๋Š” DB ๋ฝ๋งŒ์œผ๋กœ๋Š” ๋ถ€์กฑํ•œ ๊ฒฝ์šฐ๊ฐ€ ๋งŽ์•„,
์œ„์™€ ๊ฐ™์ด ๋‹ค๊ณ„์ธต ๋™์‹œ์„ฑ ์ „๋žต์„ ํ•จ๊ป˜ ์„ค๊ณ„ํ•˜๋Š” ๋ฐฉ๋ฒ•๋„ ์ค‘์š”ํ•จ.


 

๋ฐ˜์‘ํ˜•