개발기록

데이터베이스 쓰기 작업과 락킹: 언제 필요하고 언제 필요하지 않은가 본문

카테고리 없음

데이터베이스 쓰기 작업과 락킹: 언제 필요하고 언제 필요하지 않은가

Danuvibe 2024. 9. 17. 12:13

많은 개발자들이 데이터베이스 쓰기 작업을 할 때 항상 명시적인 락킹(낙관적 또는 비관적)이 필요하다고 오해하곤 합니다.
하지만 실제로는 그렇지 않습니다. 여기서 이 개념을 명확히 해보겠습니다.

기본 원칙

  1. 기본적인 쓰기 작업은 락이 필요하지 않습니다.

    • 데이터베이스 관리 시스템(DBMS)은 기본적으로 트랜잭션 격리와 동시성 제어를 자체적으로 관리합니다.
  2. ACID 속성은 기본적으로 보장됩니다.

    • 트랜잭션의 원자성(Atomicity), 일관성(Consistency), 격리성(Isolation), 지속성(Durability)은 DBMS가 보장합니다.

언제 추가적인 락킹이 필요한가?

  1. 동시성 문제가 예상되는 경우

    • 여러 트랜잭션이 동시에 같은 데이터를 수정하려 할 때
  2. 장기 실행 트랜잭션

    • 트랜잭션이 오랜 시간 동안 실행되어 다른 트랜잭션과 충돌할 가능성이 높을 때
  3. 비즈니스 로직 상 데이터 일관성이 매우 중요한 경우

    • 예: 은행 계좌 이체, 재고 관리 등

예시

1. 락이 필요 없는 일반적인 쓰기 작업

@Service
@Transactional
class UserService(private val userRepository: UserRepository) {
    fun updateUserName(userId: Long, newName: String) {
        val user = userRepository.findById(userId).orElseThrow()
        user.name = newName
        userRepository.save(user)
    }
}

이 경우, 명시적인 락킹 메커니즘 없이도 DBMS가 트랜잭션을 안전하게 처리합니다.

2. 낙관적 락이 필요한 경우

@Entity
class Product(
    @Id val id: Long,
    var name: String,
    var stock: Int,
    @Version var version: Long
)

@Service
@Transactional
class ProductService(private val productRepository: ProductRepository) {
    fun decreaseStock(productId: Long, quantity: Int) {
        val product = productRepository.findById(productId).orElseThrow()
        if (product.stock >= quantity) {
            product.stock -= quantity
            productRepository.save(product)
        } else {
            throw IllegalStateException("Not enough stock")
        }
    }
}

여기서 @Version 필드는 낙관적 락을 구현합니다. 동시에 여러 트랜잭션이 재고를 수정하려 할 때 유용합니다.

3. 비관적 락이 필요한 경우

@Repository
interface ProductRepository : JpaRepository<Product, Long> {
    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @Query("SELECT p FROM Product p WHERE p.id = :id")
    fun findByIdWithLock(@Param("id") id: Long): Product?
}

@Service
@Transactional
class ProductService(private val productRepository: ProductRepository) {
    fun decreaseStock(productId: Long, quantity: Int) {
        val product = productRepository.findByIdWithLock(productId) ?: throw NoSuchElementException()
        if (product.stock >= quantity) {
            product.stock -= quantity
            productRepository.save(product)
        } else {
            throw IllegalStateException("Not enough stock")
        }
    }
}

이 방식은 동시성 문제가 자주 발생하고, 데이터 일관성이 매우 중요한 경우에 사용합니다.

결론

  • 모든 쓰기 작업에 명시적인 락킹이 필요한 것은 아닙니다.
  • DBMS의 기본 동시성 제어 메커니즘을 신뢰하세요.
  • 특별한 동시성 요구사항이 있는 경우에만 추가적인 락킹 메커니즘을 고려하세요.
  • 락킹은 성능에 영향을 줄 수 있으므로, 꼭 필요한 경우에만 사용하세요.

적절한 락킹 전략 선택은 애플리케이션의 요구사항, 예상되는 동시성 수준, 그리고 성능 고려사항을 바탕으로 이루어져야 합니다.

Comments