ํธ๋์ญ์ ์ด๋ ๋ฌด์์ธ๊ฐ
์ํ์์ ๊ณ์ข ์ด์ฒด๋ฅผ ํ๋ค๊ณ ์์ํด๋ณด์.
A์ ๊ณ์ข์์ ๋์ด ๋น ์ ธ๋๊ฐ๊ณ ,
B์ ๊ณ์ข๋ก ๋์ด ๋ค์ด๊ฐ์ผ ํ๋ค.
๋ง์ฝ ์ค๊ฐ์ ์ค๋ฅ๊ฐ ๋ฐ์ํ๋ฉด?
A์ ๋๋ง ๋น ์ง๊ณ B๋ ๋ชป ๋ฐ๋ ์ํฉ ๋ฐ์!
ํธ๋์ญ์
์ ์ด๋ฐ ๋ฌธ์ ๋ฅผ ๋ฐฉ์งํ๋ค.
โ๋ชจ๋ ์ฑ๊ณตํ๊ฑฐ๋, ๋ชจ๋ ์คํจํ๊ฑฐ๋โ
์ค๊ฐ ์ํ๋ ์ ๋ ํ์ฉํ์ง ์๋๋ค.
์ ํธ๋์ญ์ ์ ๋ฐฐ์์ผ ํ ๊น?
์ด์ 1: ๋ฐ์ดํฐ ๋ฌด๊ฒฐ์ฑ
๋์ด ์ฆ๋ฐํ๊ฑฐ๋ ๋ณต์ ๋๋ ๊ฒ์ ๋ฐฉ์ง
์ด์ 2: ๋์์ฑ ๋ฌธ์ ํด๊ฒฐ
์ฌ๋ฌ ์ฌ์ฉ์๊ฐ ๋์์ ์ ๊ทผํ ๋ ์ถฉ๋ ๋ฐฉ์ง
์ด์ 3: ์ค๋ฌด ํ์
๊ธ์ต, ์ปค๋จธ์ค ๋ฑ ๋ชจ๋ ์๋น์ค์์ ์ฌ์ฉ
์ด์ 4: ๋ฉด์ ๋จ๊ณจ
ACID, ๊ฒฉ๋ฆฌ ์์ค, ๋ฝ์ ๋ฉด์ ๋จ๊ณจ ์ง๋ฌธ
๊ธฐ๋ณธ ๊ฐ๋ ์์ฝ
๐ท๏ธ ACID ์์ฑ
1. Atomicity (์์์ฑ)
๊ฐ๋
: All or Nothing
BEGIN TRANSACTION;
UPDATE accounts SET balance = balance - 100000 WHERE id = 1;
UPDATE accounts SET balance = balance + 100000 WHERE id = 2;
COMMIT; -- ๋ชจ๋ ์ฑ๊ณต
-- ๋๋ ROLLBACK; -- ๋ชจ๋ ์ทจ์
2. Consistency (์ผ๊ด์ฑ)
๊ฐ๋
: ํธ๋์ญ์
์ ํ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ ์ผ๊ด๋ ์ํ ์ ์ง
์ด์ฒด ์ : A=100๋ง์, B=50๋ง์ (์ด 150๋ง์)
์ด์ฒด ํ: A=90๋ง์, B=60๋ง์ (์ด 150๋ง์) โ
3. Isolation (๊ฒฉ๋ฆฌ์ฑ)
๊ฐ๋
: ๋์ ์คํ ํธ๋์ญ์
์ ์๋ก ์ํฅ ์ ์ค
4. Durability (์ง์์ฑ)
๊ฐ๋
: ์ปค๋ฐ๋ ํธ๋์ญ์
์ ์๊ตฌ ์ ์ฅ
๐ท๏ธ ํธ๋์ญ์ ๊ฒฉ๋ฆฌ ์์ค
๋์์ฑ ๋ฌธ์ 3๊ฐ์ง
1. Dirty Read: ์ปค๋ฐ ์ ๋ ๋ฐ์ดํฐ ์ฝ๊ธฐ
2. Non-repeatable Read: ๊ฐ์ ๋ฐ์ดํฐ ์กฐํ ์ ๋ค๋ฅธ ๊ฐ
3. Phantom Read: ๊ฐ์ ์กฐ๊ฑด ์กฐํ ์ ๋ค๋ฅธ ๊ฒฐ๊ณผ ๊ฐ์
๊ฒฉ๋ฆฌ ์์ค ๋น๊ต
| ๊ฒฉ๋ฆฌ ์์ค | Dirty Read | Non-repeatable | Phantom Read |
|---|---|---|---|
| READ UNCOMMITTED | โญ | โญ | โญ |
| READ COMMITTED | โ | โญ | โญ |
| REPEATABLE READ | โ | โ | โญ |
| SERIALIZABLE | โ | โ | โ |
๊ถ์ฅ: READ COMMITTED (์ฑ๋ฅ๊ณผ ์ผ๊ด์ฑ ๊ท ํ)
๐ท๏ธ ๋ฝ(Lock) ๋ฉ์ปค๋์ฆ
๋ฝ ์ข ๋ฅ
Shared Lock (S-Lock): ์ฝ๊ธฐ ์ ์ฉ, ์ฌ๋ฌ ํธ๋์ญ์
๊ณต์ ๊ฐ๋ฅ
Exclusive Lock (X-Lock): ์ฝ๊ธฐ/์ฐ๊ธฐ, ๋จ๋
์ ์
๋น๊ด์ ๋ฝ vs ๋๊ด์ ๋ฝ
| ๊ตฌ๋ถ | ๋น๊ด์ ๋ฝ | ๋๊ด์ ๋ฝ |
|---|---|---|
| ๋ฝ ์์ | ์ฝ๊ธฐ ์์ | ์ปค๋ฐ ์์ |
| ์ถฉ๋ ์ ๋ต | ๋๊ธฐ | ์ฌ์๋ |
| ๋์์ฑ | ๋ฎ์ | ๋์ |
| ์ฌ์ฉ ์์ | ์ฌ๊ณ ๊ด๋ฆฌ | ๊ฒ์๊ธ ์์ |
์ค์ ์์
๐ท๏ธ Spring @Transactional
@Service
public class AccountService {
@Transactional
public void transfer(Long fromId, Long toId, BigDecimal amount) {
Account from = accountRepository.findById(fromId).orElseThrow();
Account to = accountRepository.findById(toId).orElseThrow();
if (from.getBalance().compareTo(amount) < 0) {
throw new IllegalArgumentException("์์ก ๋ถ์กฑ");
}
from.setBalance(from.getBalance().subtract(amount));
to.setBalance(to.getBalance().add(amount));
accountRepository.save(from);
accountRepository.save(to);
// ์ ์ ์ข
๋ฃ โ COMMIT, ์์ธ ๋ฐ์ โ ROLLBACK
}
// ๊ฒฉ๋ฆฌ ์์ค ์ค์
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void calculateTotal(Long orderId) {
// ํธ๋์ญ์
๋ด์์ ๋ฐ์ดํฐ ์ผ๊ด์ฑ ๋ณด์ฅ
}
// ์ฝ๊ธฐ ์ ์ฉ ์ต์ ํ
@Transactional(readOnly = true)
public List<Order> getOrders() {
return orderRepository.findAll();
}
}
๐ท๏ธ ๋น๊ด์ ๋ฝ ๊ตฌํ
@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT p FROM Product p WHERE p.id = :id")
Optional<Product> findByIdWithLock(@Param("id") Long id);
}
@Service
public class OrderService {
@Transactional
public void purchaseProduct(Long productId, int quantity) {
// SELECT ... FOR UPDATE
Product product = productRepository.findByIdWithLock(productId)
.orElseThrow();
if (product.getStock() < quantity) {
throw new IllegalArgumentException("์ฌ๊ณ ๋ถ์กฑ");
}
product.setStock(product.getStock() - quantity);
productRepository.save(product);
}
}
๐ท๏ธ ๋๊ด์ ๋ฝ ๊ตฌํ
@Entity
public class Product {
@Id
private Long id;
private int stock;
@Version // ๋๊ด์ ๋ฝ ๋ฒ์ ์ปฌ๋ผ
private Long version;
}
@Service
public class OrderService {
@Transactional
public void purchaseProduct(Long productId, int quantity) {
try {
Product product = productRepository.findById(productId).orElseThrow();
if (product.getStock() < quantity) {
throw new IllegalArgumentException("์ฌ๊ณ ๋ถ์กฑ");
}
product.setStock(product.getStock() - quantity);
productRepository.save(product);
// UPDATE ... SET version = version + 1 WHERE id = ? AND version = ?
} catch (OptimisticLockException e) {
throw new IllegalStateException("๋ค๋ฅธ ์ฌ์ฉ์๊ฐ ๋จผ์ ๊ตฌ๋งคํ์ต๋๋ค.");
}
}
@Retryable(
value = OptimisticLockException.class,
maxAttempts = 3,
backoff = @Backoff(delay = 100)
)
public void purchaseWithRetry(Long productId, int quantity) {
purchaseProduct(productId, quantity);
}
}
๐ท๏ธ ๋ฐ๋๋ฝ ๋ฐฉ์ง
@Service
public class TransferService {
@Transactional
public void transfer(Long fromId, Long toId, BigDecimal amount) {
// ๋ฝ ์์ ํต์ผ๋ก ๋ฐ๋๋ฝ ๋ฐฉ์ง
Long firstId = Math.min(fromId, toId);
Long secondId = Math.max(fromId, toId);
Account first = accountRepository.findByIdWithLock(firstId);
Account second = accountRepository.findByIdWithLock(secondId);
Account from = (fromId.equals(firstId)) ? first : second;
Account to = (fromId.equals(firstId)) ? second : first;
from.withdraw(amount);
to.deposit(amount);
}
}
์ค์ ์ฒดํฌ๋ฆฌ์คํธ
โ ํธ๋์ญ์ ์ค๊ณ
- @Transactional ์ ์ ํ ์ฌ์ฉ
- ํธ๋์ญ์ ๋ฒ์ ์ต์ํ
- ์ฝ๊ธฐ ์ ์ฉ์ readOnly = true
- ์ ์ ํ ๊ฒฉ๋ฆฌ ์์ค ์ ํ
โ ๋์์ฑ ์ ์ด
- ์ถฉ๋ ๋น๋์ ๋ฐ๋ผ ๋ฝ ์ ํ
- ๋น๊ด์ ๋ฝ: ์ฌ๊ณ , ๊ฒฐ์
- ๋๊ด์ ๋ฝ: ๊ฒ์๊ธ, ํ๋กํ
- ๋๊ด์ ๋ฝ ์ฌ์๋ ๋ก์ง
โ ๋ฐ๋๋ฝ ๋ฐฉ์ง
- ๋ฝ ์์ ํต์ผ
- ํ์์์ ์ค์
- ๋ฐ๋๋ฝ ์ฌ์๋ ๋ก์ง
- ํธ๋์ญ์ ์๊ฐ ์ต์ํ
โ ์ฑ๋ฅ ์ต์ ํ
- ์ธ๋ฑ์ค ํ์ฉ
- ๋ถํ์ํ ๋ฝ ์ต์ํ
- ๋ฐฐ์น ์ฒ๋ฆฌ ๊ณ ๋ ค
- ์ปค๋ฅ์ ํ ๊ด๋ฆฌ
์์ฝ
ํธ๋์ญ์
์ ๋ฐ์ดํฐ ๋ฌด๊ฒฐ์ฑ๊ณผ ๋์์ฑ์ ๋ณด์ฅํ๋ ํต์ฌ ๋ฉ์ปค๋์ฆ์ด๋ค.
๐ ํต์ฌ ํฌ์ธํธ:
- ACID: ์์์ฑ, ์ผ๊ด์ฑ, ๊ฒฉ๋ฆฌ์ฑ, ์ง์์ฑ
- ๊ฒฉ๋ฆฌ ์์ค: READ COMMITTED ๊ถ์ฅ
- ๋น๊ด์ ๋ฝ: ์ถฉ๋ ๋น๋ฒํ ๋
- ๋๊ด์ ๋ฝ: ์ถฉ๋ ๋๋ฌผ ๋
- ๋ฐ๋๋ฝ ๋ฐฉ์ง: ๋ฝ ์์ ํต์ผ
- @Transactional: Spring ํธ๋์ญ์ ๊ด๋ฆฌ
๐ ์ ํ ๊ธฐ์ค:
| ์ํฉ | ๊ถ์ฅ ๋ฐฉ๋ฒ |
|---|---|
| ์ฌ๊ณ ๊ด๋ฆฌ | ๋น๊ด์ ๋ฝ |
| ๊ฒฐ์ ์ฒ๋ฆฌ | ๋น๊ด์ ๋ฝ + SERIALIZABLE |
| ๊ฒ์๊ธ ์์ | ๋๊ด์ ๋ฝ |
| ํต๊ณ ์กฐํ | ์ฝ๊ธฐ ์ ์ฉ ํธ๋์ญ์ |
| ๋๋ ์ ๋ฐ์ดํธ | ๋ฐฐ์น ์ฒ๋ฆฌ |
๐ Best Practices:
- ํธ๋์ญ์ ์ ์งง๊ฒ ์ ์ง
- ๊ฒฉ๋ฆฌ ์์ค์ ํ์ํ ๋งํผ๋ง
- ๋ฐ๋๋ฝ์ ์๋ฐฉ์ด ์ต์
- ์ฌ์๋ ๋ก์ง์ ํ์
- ๋ชจ๋ํฐ๋ง๊ณผ ๋ก๊น ์ค์
ํธ๋์ญ์
์ ์ ์ดํดํ๊ณ ์ ์ฉํ๋ฉด,
๋ฐ์ดํฐ ์ ํฉ์ฑ์ ๋ณด์ฅํ๋ฉด์๋
๋์ ๋์์ฑ์ ๋ฌ์ฑํ ์ ์๋ค.