Study 과제 중 TDD를 기반으로 테스트 코드를 작성하는데, 포인트 충전/사용 시스템을 Kotlin 으로
개발하면서 요구 사항에 "동시성 문제 해결" 이 있었다. 실제 데이터베이스를 사용하는게 아닌
인메모리를 사용하게 되면서 DB의 Transaction 없이 동시성을 처리를 해야했다.
그래서 Kotlin, Java에 내장되어있는 lock 매커니즘을 이용해서 처리를 해서 정리를 해보려고 한다.
class PointService(
private val userPointTable: UserPointTable,
private val pointHistoryTable: PointHistoryTable
) {
fun chargeUserPoint(userId: Long, amount: Long): UserPoint {
val current = userPointTable.selectById(userId) // 현재: 1000
val newPoint = current.point + amount // 1000 + 100 = 1100
return userPointTable.insertOrUpdate(userId, newPoint)
}
}
만약 두 명의 사용자가 동시에 100 포인트를 충전한다면?
Thread A: selectById(1) → 1000 읽음
Thread B: selectById(1) → 1000 읽음
Thread A: 1000 + 100 = 1100
Thread B: 1000 + 100 = 1100
Thread A: insertOrUpdate(1, 1100)
Thread B: insertOrUpdate(1, 1100) 💥 덮어쓰기!
Spring Boot는 멀티 스레드 환경이기 때문에 각 HTTP 요청이 별도 스레드에서 처리되며, 같은 데이터에 여러 스레드가 동시 접근할 수 있다.
내장되어 있는 ConcurrentHashMap을 사용해서 해결을 했다.
// ❌ HashMap은 thread-safe하지 않음
private val userLocks = HashMap<Long, Any>()
// ✅ ConcurrentHashMap은 thread-safe
private val userLocks = ConcurrentHashMap<Long, Lock>()
if 조건문 으로 처리를 했던 코드는 require() 함수로 대체를 한다.
// Kotlin답게 - 간결하고 명확
require(amount > 0) { "충전 금액은 양수여야 합니다." }
require(amount % 100 == 0L) { "포인트 사용은 100 단위로만 가능합니다." }
// 조건이 true여야 할 것을 작성!
장점은 다음과 같다.
Kotlin으로 Spring Boot를 개발한다면, 언어의 특성을 살려 더 간결하고 안전한 동시성 제어를 구현할 수 있다. Redis나 외부 라이브러리를 이용하지 않고 동시성 제어를 할 수 있다.