Sam Baek, The Dev's Corner

๐Ÿ”’ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํŠธ๋žœ์žญ์…˜๊ณผ ๋™์‹œ์„ฑ ์ œ์–ด ์™„๋ฒฝ ๊ฐ€์ด๋“œ (ACID, ๊ฒฉ๋ฆฌ ์ˆ˜์ค€, ๋ฝ)

10 Nov 2025

ํŠธ๋žœ์žญ์…˜์ด๋ž€ ๋ฌด์—‡์ธ๊ฐ€


์€ํ–‰์—์„œ ๊ณ„์ขŒ ์ด์ฒด๋ฅผ ํ•œ๋‹ค๊ณ  ์ƒ์ƒํ•ด๋ณด์ž.
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
  • ์ ์ ˆํ•œ ๊ฒฉ๋ฆฌ ์ˆ˜์ค€ ์„ ํƒ


โœ… ๋™์‹œ์„ฑ ์ œ์–ด


  • ์ถฉ๋Œ ๋นˆ๋„์— ๋”ฐ๋ผ ๋ฝ ์„ ํƒ
  • ๋น„๊ด€์  ๋ฝ: ์žฌ๊ณ , ๊ฒฐ์ œ
  • ๋‚™๊ด€์  ๋ฝ: ๊ฒŒ์‹œ๊ธ€, ํ”„๋กœํ•„
  • ๋‚™๊ด€์  ๋ฝ ์žฌ์‹œ๋„ ๋กœ์ง


โœ… ๋ฐ๋“œ๋ฝ ๋ฐฉ์ง€


  • ๋ฝ ์ˆœ์„œ ํ†ต์ผ
  • ํƒ€์ž„์•„์›ƒ ์„ค์ •
  • ๋ฐ๋“œ๋ฝ ์žฌ์‹œ๋„ ๋กœ์ง
  • ํŠธ๋žœ์žญ์…˜ ์‹œ๊ฐ„ ์ตœ์†Œํ™”


โœ… ์„ฑ๋Šฅ ์ตœ์ ํ™”


  • ์ธ๋ฑ์Šค ํ™œ์šฉ
  • ๋ถˆํ•„์š”ํ•œ ๋ฝ ์ตœ์†Œํ™”
  • ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ๊ณ ๋ ค
  • ์ปค๋„ฅ์…˜ ํ’€ ๊ด€๋ฆฌ


์š”์•ฝ


ํŠธ๋žœ์žญ์…˜์€ ๋ฐ์ดํ„ฐ ๋ฌด๊ฒฐ์„ฑ๊ณผ ๋™์‹œ์„ฑ์„ ๋ณด์žฅํ•˜๋Š” ํ•ต์‹ฌ ๋ฉ”์ปค๋‹ˆ์ฆ˜์ด๋‹ค.

๐Ÿ’Ž ํ•ต์‹ฌ ํฌ์ธํŠธ:

  1. ACID: ์›์ž์„ฑ, ์ผ๊ด€์„ฑ, ๊ฒฉ๋ฆฌ์„ฑ, ์ง€์†์„ฑ
  2. ๊ฒฉ๋ฆฌ ์ˆ˜์ค€: READ COMMITTED ๊ถŒ์žฅ
  3. ๋น„๊ด€์  ๋ฝ: ์ถฉ๋Œ ๋นˆ๋ฒˆํ•  ๋•Œ
  4. ๋‚™๊ด€์  ๋ฝ: ์ถฉ๋Œ ๋“œ๋ฌผ ๋•Œ
  5. ๋ฐ๋“œ๋ฝ ๋ฐฉ์ง€: ๋ฝ ์ˆœ์„œ ํ†ต์ผ
  6. @Transactional: Spring ํŠธ๋žœ์žญ์…˜ ๊ด€๋ฆฌ


๐Ÿ“Œ ์„ ํƒ ๊ธฐ์ค€:

์ƒํ™ฉ ๊ถŒ์žฅ ๋ฐฉ๋ฒ•
์žฌ๊ณ  ๊ด€๋ฆฌ ๋น„๊ด€์  ๋ฝ
๊ฒฐ์ œ ์ฒ˜๋ฆฌ ๋น„๊ด€์  ๋ฝ + SERIALIZABLE
๊ฒŒ์‹œ๊ธ€ ์ˆ˜์ • ๋‚™๊ด€์  ๋ฝ
ํ†ต๊ณ„ ์กฐํšŒ ์ฝ๊ธฐ ์ „์šฉ ํŠธ๋žœ์žญ์…˜
๋Œ€๋Ÿ‰ ์—…๋ฐ์ดํŠธ ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ


๐Ÿš€ Best Practices:

  • ํŠธ๋žœ์žญ์…˜์€ ์งง๊ฒŒ ์œ ์ง€
  • ๊ฒฉ๋ฆฌ ์ˆ˜์ค€์€ ํ•„์š”ํ•œ ๋งŒํผ๋งŒ
  • ๋ฐ๋“œ๋ฝ์€ ์˜ˆ๋ฐฉ์ด ์ตœ์„ 
  • ์žฌ์‹œ๋„ ๋กœ์ง์€ ํ•„์ˆ˜
  • ๋ชจ๋‹ˆํ„ฐ๋ง๊ณผ ๋กœ๊น… ์ค‘์š”


ํŠธ๋žœ์žญ์…˜์„ ์ž˜ ์ดํ•ดํ•˜๊ณ  ์ ์šฉํ•˜๋ฉด,
๋ฐ์ดํ„ฐ ์ •ํ•ฉ์„ฑ์„ ๋ณด์žฅํ•˜๋ฉด์„œ๋„
๋†’์€ ๋™์‹œ์„ฑ์„ ๋‹ฌ์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค.