Development
Firestore Transaction - ๋น๊ด์ ๋ฝ์ ํด์
4/28/2025

์ฌ์ง์ค์ธ ํ์ฌ์์ ์ด์ํ๋ ์ ํ๋ฆฌ์ผ์ด์ ์์ ๋์์ฑ ๋ฌธ์ ๊ฐ ๋ฐ์ํ์์ต๋๋ค. Firestore์ ํธ๋์ญ์ ๊ธฐ๋ฅ์ ๊ธํ๊ฒ ๋ฌ์๋์์ง๋ง ์๋ฒ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ ๋น๊ด์ ๋ฝ์ผ๋ก ๋์ํ๋ฏ๋ก ๋ค์๊ณผ ๊ฐ์ ์์๋ค์ ์ถ๊ฐ๋ก ์กฐ์ฌํด ๋ณผ ํ์๊ฐ ์์์ต๋๋ค.
1. ์ ๊ธ์ ๋จ์๋ ๋ฌด์์ธ๊ฐ? ๋ฌธ์ ๋จ์์ธ๊ฐ? ์๋๋ฉด ํน์ ์์ฑ๋ง ์ ๊ธ์ด ๊ฐ๋ฅํ๊ฐ?
2. ์ ๊ธ์ด ๊ฑธ๋ฆฐ ๋ฌธ์๋ ๋ค๋ฅธ ํ๋ก์ธ์ค์์ ์ฝ์ ์ ์๋๊ฐ? ํธ๋์ญ์ ์ ์ฌ์ฉํ์ง ์์ ํ๋ก์ธ์ค์์๋ ์ด๋ป๊ฒ ๋์ํ๋๊ฐ?
3. ๋ฐ๋๋ฝ์ด ๊ฑธ๋ฆฐ๋ค๋ฉด ํ์์์์ ๋ช ์ด์ธ๊ฐ?
๊ฒฝํฉ ์ํ ์๋ฎฌ๋ ์ด์ ์ฝ๋ ์์ฑ
์ ์์๋ฅผ ํ์ ํ๊ธฐ ์ํด ๊ณ ์๋ก ๋์์ฑ ๋ฌธ์ ๋ฅผ ๋ง๋๋ ์ฝ๋๋ฅผ ์์ฑํ์์ต๋๋ค.
const increase = async (threadId) => {
await store.runTransaction(async (tx) => {
console.log(`tx[${threadId}] start`);
const docRef = store.collection('Product').doc('test');
const doc = await tx.get(docRef);
console.log(`tx[${threadId}] read done`);
const newValue = doc.data().value + 1;
console.log(
`tx[${threadId}] update from ${doc.data().value} to ${newValue}`
);
await new Promise((resolve) => setTimeout(resolve, 3000)); //๋๊ธฐ
tx.update(docRef, {
[`value`]: newValue,
});
console.log(`tx[${threadId}] end`);
});
}
increase(1);
await new Promise((resolve) => setTimeout(resolve, 1000));
increase(2);increase()๋ test ๋ฌธ์์ value์ 1 ์ฆ๊ฐ์ํต๋๋ค. ์ด ๊ณผ์ ์์ ๋์์ฑ ๋ฌธ์ ๋ฅผ ๋ฐ์์ํค๊ธฐ ์ํด ํธ๋์ญ์ ์ ๋ฐ๋ก ์ปค๋ฐํ์ง ์๊ณ 3์ด ๋๊ธฐํฉ๋๋ค. threadId๋ฅผ ๋ฐ์ ์ฝ์์ ์ถ๋ ฅํ์ฌ ํธ๋์ญ์ ๊ฐ ๋ก๊ทธ๋ฅผ ๊ตฌ๋ถํด์ ๋ณผ ์ ์๋๋ก ํฉ๋๋ค.
์ด ๊ฒฝ์ฐ ๋น๊ด์ ๋ฝ์ผ๋ก ๋์ํ๋ฏ๋ก ๋๋ฒ์งธ ํธ๋์ญ์ ์ ์ฝ๊ธฐ๋ฅผ ๋๊ธฐ์ํฌ ๊ฒ์ด๋ผ ์๊ฐํ์ต๋๋ค. ์์๋๋ ๋ก๊ทธ๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
tx[1] start
tx[1] read done
tx[1] update from 0 to 1, wait 3 sec
tx[2] start <- 1์ด ํ ์์, tx[1]์ด ์ข ๋ฃ๋ ๋๊น์ง ๋๊ธฐ
ย
tx[1] end <- tx[1]์ 3์ด ๋๊ธฐ ์ข ๋ฃ, ๋ฌธ์ ์ ๊ธ ํด์
tx[2] read done <- tx[2] ๋๊ธฐ ํด์
tx[2] update to 1 to 2, wait 3 sec
tx[2] end
ํ์ง๋ง ๊ฒฐ๊ณผ๋ ์์ ํ ๋ฌ๋์ต๋๋ค.
tx[1] start
tx[1] read done
tx[1] update from 0 to 1, wait 3 sec
tx[2] start <- 1์ด ํ ์์
tx[2] read done <- ๋๊ธฐํ์ง ์๊ณ ๋ฐ๋ก ๋ฌธ์๋ฅผ ์ฝ์
tx[2] update from 0 to 1, wait 3 sec
ย
tx[1] end
tx[2] end
tx[2] start <- tx[2] ์ฌ์๋
tx[2] read done
tx[2] update from 1 to 2, wait 3 sec
tx[2] end
Firestore์ ์๋ฒ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ ํด๋ผ์ด์ธํธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ๋ง์ฐฌ๊ฐ์ง๋ก ๋๊ด์ ๋ฝ๊ณผ ์ฌ์๋ ๋ฐฉ์์ผ๋ก ๋์ํ๋ ๊ฒ ์ฒ๋ผ ๋ณด์์ต๋๋ค. ์ ๊ฐ ํ ์คํธ ํด๋ณด๋ ค๊ณ ํ๋ ๋ชจ๋ ๋ด์ฉ์ด ๋ฌด์๋ฏธํด์ก์ต๋๋ค.


๋์ ์ป๊ณ ๋ค์ ์ฝ์ด๋ด๋ ์๋ฒ ํด๋ผ์ด์ธํธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ ์ด๋ ๊ฒ ๋์ํ๋ฉด ์๋ ํ ๋ฐ๋ผ๋ ์๊ฐ๋ง ๋ค์์ต๋๋ค. ๋ญ๊ฐ๊ฐ ์๋ชป๋์๋ค๋ ์๊ฐ์ ๊ตฌ๊ธ๋ง์ ํ๋ค ๊ด๋ จ ์ด์๋ฅผ ์ฐพ๊ฒ ๋์์ต๋๋ค.
๋น๊ด์ ๋ฝ์ ํด์
Documentation describing transactions as pessimistics is wrong, should be optimistic ์ด๋ผ๋ ์ ๋ชฉ์ ๊นํ๋ธ ์ด์์ ๋๋ค.
์ด์ ๋ฐํ์๋ Firestore์ ์๋ฒ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ๋ฝ์ ๋น๊ด์ ๋ฝ์ด ์๋ ๋๊ด์ ๋ฝ์ด๋ผ๋ฉฐ, ๋ฌธ์๊ฐ ์๋ชป๋์์์ ์ฃผ์ฅํ๊ณ ์์ต๋๋ค.
The way runTransaction works is it tries to commit changes at the end of the transaction and if it is unable to because of conflicts it retries - that is optimistic. You can have 2 transactions running at the same time operating on the same document
๋์ผํ ๋ฌธ์๋ฅผ ์๋ก ๋ค๋ฅธ ๋ ํธ๋์ญ์
์ด ๋์์ ์ฝ์ ์ ์๊ณ , ์ํฉ์ ๋ฐ๋ผ ์ฌ์๋๊ฐ ๋ฐ์ํ๋ ์ ์ ๊ทผ๊ฑฐ๋ก ๋ค๊ณ ์์ต๋๋ค. ์ด๋ ์ผ๋ฐ์ ์ธ ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ SELECT ... FOR UPDATE๋ฅผ ์ฌ์ฉํ ๋น๊ด์ ๋ฝ์ ๋์ ๋ฐฉ์๊ณผ ๋น๊ตํด ๋ณผ ๋ ์ถฉ๋ถํ ๋ฉ๋ํ ์ ์๋ ์ฃผ์ฅ์
๋๋ค. ๋ณดํต SELECT ... FOR UPDATE ์ฟผ๋ฆฌ๋ฅผ ํตํด ๋ฝ์ ์ค์ ํ๋ ์๊ฐ, ๋ค๋ฅธ ์ธ์
์์ ๋์ผํ ๋ ์ฝ๋๋ฅผ SELECT ... FOR UPDATE๋ก ์ฝ์ผ๋ ค๋ ์๋๋ ๋ฝ์ด ํด์ ๋ ๋๊น์ง ๋๊ธฐํ๊ฒ ๋๊ธฐ ๋๋ฌธ์
๋๋ค.
ํํธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ๊ธฐ์ฌ์๋ ๋ค์๊ณผ ๊ฐ์ ์ด์ ๋ก Firestore ์๋ฒ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ ๋น๊ด์ ๋ฝ์ด๋ฉฐ, ๋ฌธ์๋ฅผ ์์ ํ ๊ณํ์ด ์๋ค๊ณ ํ์์ต๋๋ค.
If you have two transactions running that both try to read the same document, the first one will succeed and the second one will hang until the first one completes.
๋ ํธ๋์ญ์ ์ด ๊ฐ์ ๋ฌธ์๋ฅผ ์ฝ์ ๋, ์ฒซ๋ฒ์งธ ํธ๋์ญ์ ์ ์ฑ๊ณตํ ๊ฒ์ด๊ณ ๋๋ฒ์งธ ํธ๋์ญ์ ์ ์ฒซ๋ฒ์งธ ํธ๋์ญ์ ์ด ์๋ฃ๋ ๋ ๊น์ง ๊ธฐ๋ค๋ฆฐ๋ค๋ ๊ฒ์ ๊ทธ ์ด์ ๋ก ๋ค์์ต๋๋ค. ์ด๋ฅผ ์ดํดํ๊ธฐ ์ํด ์๋ก์ด ์ผ์ด์ค๋ฅผ ์๋ฎฌ๋ ์ด์ ํด๋ณด์์ต๋๋ค.
const increase = async (threadId, ms) => {
await store.runTransaction(async (tx) => {
console.log(`tx[${threadId}] start`);
const docRef = store.collection('Product').doc('test');
const doc = await tx.get(docRef);
console.log(`tx[${threadId}] read done`);
const newValue = doc.data().value + 1;
console.log(
`tx[${threadId}] update from ${doc.data().value} to ${newValue}`
);
await new Promise((resolve) => setTimeout(resolve, ms)); //๋๊ธฐ
tx.update(docRef, {
[`value`]: newValue,
});
console.log(`tx[${threadId}] end`);
});
}
increase(1, 5000);
await new Promise((resolve) => setTimeout(resolve, 1000));
increase(2, 1000);ํธ๋์ญ์ ๋์ค์ ๋๊ธฐํ๋ ์๊ฐ์ ํ๋ผ๋ฏธํฐ๋ก ๋ฐ๋๋ค๋ ์ ๋ง ๋ฐ๋์์ต๋๋ค. ์ฒซ๋ฒ์งธ ํธ๋์ญ์ ์ 5์ด์ ์๊ฐ์ ๋์๊ณ , ๋๋ฒ์งธ ํธ๋์ญ์ ์ 1์ด์ ์๊ฐ๋ง ๋์์ต๋๋ค. ์ด๋ ๋ ๋ฒ์งธ ํธ๋์ญ์ ์ ์์ ์ด ๋ ๋นจ๋ฆฌ ์ข ๋ฃ๋๊ฒ ๋ง๋ค๊ธฐ ์ํจ์ ๋๋ค. ๊ฒฐ๊ณผ๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
tx[1] start
tx[1] read done
tx[1] update from 0 to 1, wait 5 sec
tx[2] start
tx[2] read done
tx[2] update from 0 to 1, wait 1 sec
tx[2] end <-- tx[2]์ ์์ ์ด ๋จผ์ ์ข ๋ฃ๋์๋ค.
tx[1] end <-- ์ดํ tx[1]์ด ์ข ๋ฃ๋๊ณ , ์ปค๋ฐ๋จ
ย
tx[2] start <-- tx[2]๋ ์ฌ์๋
tx[2] read done
tx[2] update from 1 to 2, wait 1 sec
tx[2] end
tx[2]๊ฐ ๋จผ์ ์์ ์ ๋ง์ณค์์๋, ํด๋น ๋ฌธ์์ ๋ํ ์ฐ๊ธฐ ๋ฝ์ tx[1]์ด ๊ฐ์ง๊ณ ์์๊ธฐ์ ๋๊ธฐํ๋ ๋ชจ์ต์ ๋ณด์์ต๋๋ค. tx[1]์ด ์ปค๋ฐ๋ ์ดํ tx[2]๋ ๋ฌธ์์ ๋ณ๊ฒฝ์ ๊ฐ์งํ๊ณ ์ฌ์๋ ํฉ๋๋ค.
๊ฒฐ๊ตญ ํต์ฌ์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
์๋ฒ ํธ๋์ญ์ (๋น๊ด์ ๋ฝ)์ ์ฐ๊ธฐ ์ ๊ธ์ ํ๋ํ ๋ ๊น์ง ๊ธฐ๋ค๋ฆฐ ํ ์ฐ๊ธฐ๋ฅผ ์๋ํ๊ณ , ํ์์ ๋ฐ๋ผ ์ฌ์๋ํ๋ค. ๋ฐ๋ฉด, ํด๋ผ์ด์ธํธ(๋๊ด์ ๋ฝ)๋ ์ฆ์ ์ฐ๊ธฐ๋ฅผ ์๋ํ๋ค.
์ผ๋จ ๋ฌธ์์ ๋ํ ๋ฝ์ ํ๋ํ๋ฉด ๋ค๋ฅธ ์ค๋ ๋์์๋ ํด๋น ๋ฌธ์์ ๋ํ ์ฐ๊ธฐ ์์ ์ ์ํํ ์ ์์ต๋๋ค. ์ฐ๊ธฐ ๊ถํ์ ์ป์ ๋๊น์ง ๋๊ธฐํด์ผํฉ๋๋ค. ์ด๋ฌํ ๊ทผ๊ฑฐ์ ๋ฐ๋ฅด๋ฉด Firestore์ ์๋ฒ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ๋ฝ์ด ๋น๊ด์ ๋ฝ์ด๋ผ๋ ๊ฒ์ ํฉ๋นํด ๋ณด์ ๋๋ค. ์ ๋ ์ฒ์์๋ ์์ด๋ฌ๋ ํ์ผ๋, ๋์ ๋ฐฉ์์ ์ดํด๋ณด๊ณ ๋๋ ์ด์ ๊ณต๊ฐ์ด ๋ฉ๋๋ค.
์ด๋ ๋๊ด์ ๋ฝ์ผ๋ก ๋์ํ๋ ํด๋ผ์ด์ธํธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ๋น๊ตํด๋ณด๋ฉด ๊ทธ ์ฐจ์ด๊ฐ ๋ ๋ถ๋ช ํ๊ฒ ๋๋ฌ๋ฉ๋๋ค. ํด๋ผ์ด์ธํธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์์๋ ์ฐ๊ธฐ์ ๋ํ ๊ถํ์ ์ ์ ํ๋ ๊ฐ๋ ์ด ์กด์ฌํ์ง ์์ต๋๋ค. ๊ทธ์ ํธ๋์ญ์ ์ด ์ปค๋ฐ๋๋ ์๊ฐ ๋ฌธ์์ ๋ฒ์ ์ ๊ฐ์งํ์ฌ ๊ทธ ์ฌ์ด์ ๋ณ๊ฒฝ์ด ๋์๋ค๋ฉด ์ฌ์๋ํ๊ณ , ์๋๋ผ๋ฉด ์ ์ฅ์ ํ ๋ฟ์ ๋๋ค.
๊ทธ๋ผ์๋ ๋ถ๊ตฌํ๊ณ ๋ฌธ์๊ฐ ์๋ชป ์์ฑ๋์๋ค๋ ์ ์์๋ ๋งค์ฐ ๋์ํฉ๋๋ค. ์ด ๋ฌธ์ ๋ ์๋ฒ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ๋์์ด ๋๊ด์ ๋ฝ์ด๋ ๋น๊ด์ ๋ฝ์ด๋๋ฅผ ๋ ๋์์ต๋๋ค. Firestore์ ๊ณต์ ๋ฌธ์๋ ๋์์ ์์ํ๊ธฐ์ ์ด๋ ต๊ณ ์คํดํ ์ ์๋ ๋ง์ ์์ง๋ฅผ ๊ฐ๊ณ ์๋ ๊ฒ ๊ฐ์ต๋๋ค.