λ™μ‹œμ„± 처리 μ‰½κ²Œ μ΄ν•΄ν•˜κΈ° (synchronized, reentrantLock)

2024. 9. 29. 15:17Β·πŸ’» 개발/πŸ€ Spring
 

1. λ™μ‹œμ„± μ²˜λ¦¬λŠ” μ™œ ν•˜λŠ”κ±ΈκΉŒ?

λ™μ‹œμ„± 처리λ₯Ό ν•˜μ§€ μ•ŠμœΌλ©΄ μœ μ €κ°€ 포인트λ₯Ό λ™μ‹œμ— μΆ©μ „ λ˜λŠ” μ‚¬μš©ν•  λ•Œ 데이터 뢈일치 λ¬Έμ œκ°€ λ°œμƒν•©λ‹ˆλ‹€. μ΄λŠ” μ—¬λŸ¬ μŠ€λ ˆλ“œκ°€ 같은 λ¦¬μ†ŒμŠ€μ— μ ‘κ·Όν•˜λ©΄μ„œ λ°œμƒν•˜λŠ” Race Condition(경쟁 μƒνƒœ) ν˜„μƒμ΄λΌ λ³Ό 수 μžˆλŠ”λ°, Race Conditionμ΄λž€ 곡유 μžμ›μ— λŒ€ν•΄ μ—¬λŸ¬ ν”„λ‘œμ„ΈμŠ€κ°€ λ™μ‹œμ— 접근을 μ‹œλ„ν•  λ•Œ, μ‹€ν–‰ μˆœμ„œλ‚˜ μ‹œκ°„μ— μ˜ν•΄ κ²°κ³Ό 값에 영ν–₯을 쀄 수 μžˆλŠ” μƒνƒœλ₯Ό λ§ν•©λ‹ˆλ‹€.

 

μ•„λž˜ 그림을 톡해 Race Condition에 λŒ€ν•œ μ˜ˆμ‹œλ₯Ό λ“€μ–΄λ³΄κ² μŠ΅λ‹ˆλ‹€.

μœ„ κ·Έλ¦Όκ³Ό 같이 2개의 μŠ€λ ˆλ“œμ—μ„œ μ‹€ν–‰λœ 각각의 νƒœμŠ€ν¬κ°€ pointλΌλŠ” λ³€μˆ˜μ— λ™μ‹œμ— μ ‘κ·Όν•˜μ—¬ μ΄ˆκΈ°κ°’ 0을 μ½μ–΄μ™”μŠ΅λ‹ˆλ‹€. 그리고 각각 500을 λ”ν•˜λŠ” 연산을 μˆ˜ν–‰ν•˜κ³ , point에 κ²°κ³Ό 값을 λŒ€μž…ν–ˆμŠ΅λ‹ˆλ‹€. κ·ΈλŸ¬λ‚˜ κΈ°λŒ€μ™€λŠ” λ‹€λ₯΄κ²Œ 1000이 μ•„λ‹Œ 500이 λ‚˜μ˜€κ²Œ λ©λ‹ˆλ‹€.

 

ν•΄λ‹Ή 문제 상황을 ν…ŒμŠ€νŠΈ μ½”λ“œλ‘œ μ‚΄νŽ΄λ³΄κ² μŠ΅λ‹ˆλ‹€.

class RaceConditionTest {

    inner class PointManager {
        private var point = 0

        fun add(amount: Int) {
            Thread.sleep(100)
            point += amount
        }

        fun getPoint(): Int {
            return point
        }
    }

    @Test
    fun `λ™μ‹œμ— 포인트λ₯Ό 500μ”© μΆ©μ „ν•˜λ©΄ ν¬μΈνŠΈκ°€ 1000원이 λœλ‹€`() {
        // given
        val pointManager = PointManager()
        val executor = Executors.newFixedThreadPool(2)

        val tasks = listOf(
            Callable { pointManager.add(500) },
            Callable { pointManager.add(500) },
        )

        // when
        executor.invokeAll(tasks)
        executor.shutdown()

        // then
        assertEquals(1000, pointManager.getPoint())
    }
}
 

μœ„ μ½”λ“œλŠ” μŠ€λ ˆλ“œ 풀에 2개의 μŠ€λ ˆλ“œλ₯Ό μƒμ„±ν•˜κ³ , 포인트λ₯Ό 500 증가 μ‹œν‚€λŠ” νƒœμŠ€ν¬ 2개λ₯Ό μˆ˜ν–‰ν•œ μ˜ˆμ œμž…λ‹ˆλ‹€. μœ„ ν…ŒμŠ€νŠΈλ₯Ό μˆ˜ν–‰ν•΄λ³΄λ©΄ μ„±κ³΅ν•˜λŠ” κ²½μš°λ„ μžˆμ§€λ§Œ μ‹€νŒ¨ν•˜λŠ” κ²½μš°λ„ μƒκΈ°κ²Œ λ©λ‹ˆλ‹€. κ·Έ μ΄μœ λŠ” μŠ€λ ˆλ“œκ°€ μˆ˜ν–‰ν•˜λŠ” μˆœμ„œλ‚˜ 타이밍이 맀번 달라져 결과값이 달라지기 λ•Œλ¬Έμž…λ‹ˆλ‹€.

μ„±κ³΅ν•˜κ±°λ‚˜
μ‹€νŒ¨ν•œλ‹€

 

 

그럼 μš°λ¦¬λŠ” μ–΄λ–»κ²Œ ν•΄μ•Ό ν…ŒμŠ€νŠΈλ₯Ό 항상 μ„±κ³΅μ μœΌλ‘œ μˆ˜ν–‰ κ°€λŠ₯ν•˜κ²Œ λ§Œλ“€ 수 μžˆμ„κΉŒμš”?



 

 

 

2. synchronizedλ₯Ό μ΄μš©ν•œ μ ‘κ·Ό μ œν•œ

 

μ΄λ²ˆμ—λ„ λ‹€λ₯Έ 예제의 그림을 λ³΄λ©΄μ„œ λ‹€μ‹œ 생각해 λ³΄κ² μŠ΅λ‹ˆλ‹€.

 

μ΄λ²ˆμ—λŠ” point에 잠금 μž₯치λ₯Ό λΆ€μ—¬ν–ˆμŠ΅λ‹ˆλ‹€. μ—΄μ‡ λ₯Ό κ°€μ§€κ³  μžˆλŠ” μŠ€λ ˆλ“œλ§Œ 접근을 ν—ˆμš©ν•˜κ³ , μ—΄μ‡ λ₯Ό κ°€μ§€κ³  μžˆμ§€ μ•Šμ€ μŠ€λ ˆλ“œλŠ” point λ‘œλΆ€ν„° μ—΄μ‡ λ₯Ό μ–»κΈ° μ „κΉŒμ§€λŠ” λŒ€κΈ° μƒνƒœμ— λΉ μ§€κ²Œ λ©λ‹ˆλ‹€. 이후 μ—΄μ‡ λ₯Ό λ°›κ²Œ 되면 λ‘œμ§μ„ μˆ˜ν–‰ν•˜κ²Œ λ©λ‹ˆλ‹€.

 

μ΄λ ‡κ²Œ 되면 이전에 λ°œμƒν•œ λ™μ‹œμ„± 문제λ₯Ό μ‰½κ²Œ ν•΄κ²°ν•  수 μžˆμŠ΅λ‹ˆλ‹€. point와 같은 곡유 μžμ›μ—κ²Œ μŠ€λ ˆλ“œκ°€ 순차적으둜 μ ‘κ·Όν•˜λ„λ‘ 보μž₯ν•˜λ©΄μ„œ μ›ν•˜λŠ” 결과값을 λ„μΆœν•  수 있게 λ˜μ—ˆμŠ΅λ‹ˆλ‹€. 이와 같이 λ™μ‹œμ ‘κ·Ό ν•˜λ €κ³  ν•˜λŠ” μžμ›μ—μ„œ λ¬Έμ œκ°€ λ°œμƒν•˜μ§€ μ•Šκ²Œ 독점을 보μž₯ν•΄μ€˜μ•Ό ν•˜λŠ” μ˜μ—­μ„ μž„계 μ˜μ—­μ΄λΌκ³  ν•©λ‹ˆλ‹€.

 

Java μ§„μ˜μ—μ„  μž„계 μ˜μ—­μ„ μ‰½κ²Œ 섀정해쀄 수 μžˆμŠ΅λ‹ˆλ‹€. synchronized λΌλŠ” ν‚€μ›Œλ“œλ₯Ό μ‚¬μš©ν•  수 μžˆλŠ”λ°, 이 ν‚€μ›Œλ“œλ₯Ό μ‚¬μš©ν•˜μ—¬ μž„κ³„μ˜μ—­μ„ μ„€μ •ν•˜λ©΄, λ™μ‹œμ— μ—¬λŸ¬ μŠ€λ ˆλ“œκ°€ 같은 λ¦¬μ†ŒμŠ€μ— μ ‘κ·Όν•˜λŠ” 문제λ₯Ό ν•΄κ²°ν•  수 μžˆμŠ΅λ‹ˆλ‹€. κ°„λ‹¨ν•˜κ²Œ μž„κ³„ μ˜μ—­μ„ μ§€μ •ν•  λ©”μ„œλ“œ μœ„μ—@Synchronizedμ–΄λ…Έν…Œμ΄μ…˜λ§Œ μ„ μ–Έν•΄μ£Όλ©΄ λ©λ‹ˆλ‹€.

 

class RaceConditionTest {

    inner class PointManager {
        private var point = 0

	@Synchronized
        fun add(amount: Int) {
            Thread.sleep(10)
            point += amount
        }

        fun getPoint(): Int {
            return point
        }
    }

    @Test
    fun `λ™μ‹œμ— 포인트λ₯Ό 500μ”© μΆ©μ „ν•˜λ©΄ ν¬μΈνŠΈκ°€ 1000원이 λœλ‹€`() {
        // given
        val pointManager = PointManager()
        val executor = Executors.newFixedThreadPool(2)

        val tasks = listOf(
            Callable { pointManager.add(500) },
            Callable { pointManager.add(500) },
        )

        // when
        executor.invokeAll(tasks)
        executor.shutdown()

        // then
        assertEquals(1000, pointManager.getPoint())
    }
}
 

첫 번째둜 봀던 μ˜ˆμ‹œ μ½”λ“œμ™€ 비ꡐ해보면 add() λ©”μ„œλ“œ 에 @Synchronized λ§Œ μΆ”κ°€ν•œ 것 λΉΌκ³€ μ™„μ „ λ™μΌν•œ μ½”λ“œμž…λ‹ˆλ‹€. μž„κ³„ μ˜μ—­μ„ μ„€μ •ν•¨μœΌλ‘œμ¨ 곡유 μžμ›μ— λŒ€ν•΄ μ—¬λŸ¬ μŠ€λ ˆλ“œκ°€ 접근해도 λ™μΌν•œ 결과값을 받을 수 있게 λ˜μ—ˆμŠ΅λ‹ˆλ‹€.

 

그럼 이제 각 μœ μ € λ³„λ‘œ 포인트λ₯Ό κ΄€λ¦¬ν•˜λ„λ‘ 예제λ₯Ό λ³€κ²½ν•΄λ³΄κ² μŠ΅λ‹ˆλ‹€.

 

μœ μ € IDλ₯Ό ν‚€λ‘œ κ°€μ§€λŠ” HashMap μ„ μ‚¬μš©ν•΄ μœ μ € λ³„λ‘œ 포인트λ₯Ό κ΄€λ¦¬ν•˜λ„λ‘ λ³€κ²½ν–ˆμŠ΅λ‹ˆλ‹€. μ•„λž˜λŠ” λ³€κ²½ν•œ μ½”λ“œμž…λ‹ˆλ‹€.

class RaceConditionTest {

    inner class PointManager {
        private val userPoints: MutableMap<Int, Int> = HashMap()

        @Synchronized
        fun add(userId: Int, amount: Int) {
            Thread.sleep(100)
            val point = userPoints.getOrDefault(userId, 0)
            userPoints[userId] = point + amount
        }

        fun getPoint(userId: Int): Int {
            return userPoints.getOrDefault(userId, 0)
        }
    }

    @Test
    fun `두λͺ…μ˜ μœ μ €μ—κ²Œ 포인트λ₯Ό 100μ”© 10번 μΆ©μ „ν•˜λ©΄ ν¬μΈνŠΈκ°€ 각 1000원이 λœλ‹€`() {
        // given
        val pointManager = PointManager()
        val executor = Executors.newFixedThreadPool(10)

        val tasks = mutableListOf<Callable<Unit>>()

        repeat(10) {
            tasks.add(Callable { pointManager.add(1, 100) }) // μœ μ €1
            tasks.add(Callable { pointManager.add(2, 100) }) // μœ μ €2
        }

        // when
        executor.invokeAll(tasks)
        executor.shutdown()

        // then
        assertEquals(1000, pointManager.getPoint(1))
        assertEquals(1000, pointManager.getPoint(2))
    }
}
 

 

Synchronized λ•뢄에 μœ„μ™€ 같이 λ™μ‹œμ— 좩전해도 각 μœ μ € λ³„λ‘œ 1000원이 좩전이 잘 λ˜λŠ” 것을 확인할 수 μžˆμŠ΅λ‹ˆλ‹€.

 

그런데 μ΄μƒν•œ 점이 μžˆμŠ΅λ‹ˆλ‹€. μ˜ˆλ¦¬ν•œ κ΄€μ°°λ ₯이라면 이미 λ°œκ²¬ν–ˆμ„ μˆ˜λ„ μžˆλŠ”λ°, λ°”λ‘œ ν…ŒμŠ€νŠΈ 결과의 μˆ˜ν–‰ μ‹œκ°„μž…λ‹ˆλ‹€. add() λ₯Ό μˆ˜ν–‰ν•˜λŠ”λ° μ•½ 0.1μ΄ˆκ°€ κ±Έλ¦°λ‹€λ©΄ 각 μœ μ € λ³„λ‘œ 1μ΄ˆκ°€ κ±Έλ¦¬λ‹ˆ ν…ŒμŠ€νŠΈλ„ μ•½ 1μ΄ˆκ°€ μˆ˜ν–‰λ˜μ–΄μ•Ό ν•©λ‹ˆλ‹€. ν•˜μ§€λ§Œ μœ μ €1κ³Ό μœ μ €2λŠ” μ„œλ‘œ κ°€μ Έκ°„ 락을 λ°›κΈ° μœ„ν•΄ λŒ€κΈ° μƒνƒœμ— λΉ μ§€κ³  κ²°κ΅­ μˆ˜ν–‰ μ‹œκ°„μ΄ 2μ΄ˆκ°€ λ„˜μ–΄κ°€λŠ” 것을 확인할 수 μžˆμŠ΅λ‹ˆλ‹€. μ—¬κΈ°μ„œ Synchronized μ˜ 단점을 λ°œκ²¬ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

 

synchronized μ˜ 단점

Synchronized λŠ” μ „μ—­μ μœΌλ‘œ 락을 κ±ΈκΈ° λ•Œλ¬Έμ— 두 λͺ… μ΄μƒμ˜ μœ μ €κ°€ ν•΄λ‹Ή 블둝에 μ ‘κ·Όν•  경우, μ„œλ‘œμ˜ 락에 κ±Έλ €λ²„λ¦¬λŠ” 상황이 μƒκΉλ‹ˆλ‹€. μš°λ¦¬κ°€ μ›ν•˜λŠ” 것은 λ‹€λ₯Έ μœ μ €μ˜ μΆ©μ „ μš”μ²­μ— 상관없이 μ•½ 1초 λ§Œμ— 포인트λ₯Ό μΆ©μ „ν•˜λŠ” κ²ƒμž…λ‹ˆλ‹€.



3. μœ μ € 별 Lock 객체 κ΅¬ν˜„

 

Synchronized λŠ” Java 1.0λΆ€ν„° μ œκ³΅λ˜λŠ” κΈ°λŠ₯이며, Java 1.5λΆ€ν„° java,util.concurrentλΌλŠ” λ™μ‹œμ„± 문제 해결을 μœ„ν•œ νŒ¨ν‚€μ§€κ°€ μΆ”κ°€λ˜μ—ˆμŠ΅λ‹ˆλ‹€. ν•΄λ‹Ή νŒ¨ν‚€μ§€μ—λŠ” Lock μΈν„°νŽ˜μ΄μŠ€μ™€ ReentrantLock κ΅¬ν˜„체가 μ œκ³΅λ©λ‹ˆλ‹€.

ReentrantLock λ„ Synchronized μ™€ λ§ˆμ°¬κ°€μ§€λ‘œ μž„κ³„ μ˜μ—­μ„ μ„€μ •ν•˜μ§€λ§Œ, 보닀 μ •κ΅ν•˜κ²Œ μ‚¬μš©μ΄ κ°€λŠ₯ν•©λ‹ˆλ‹€. μš°λ¦¬μ—κ²Œ ν•„μš”ν•œ 것은 μœ μ € λ³„λ‘œ 락을 κ΄€λ¦¬ν•˜λŠ” κ²ƒμž…λ‹ˆλ‹€. λ‹€μŒ μ˜ˆμ‹œλ₯Ό μ•„λž˜ 그림을 톡해 μ•Œμ•„λ³΄κ² μŠ΅λ‹ˆλ‹€.

 

 

Lock을 λ‹΄λ‹Ήν•˜λŠ” userLocksλ₯Ό λ§Œλ“€μ–΄ 이제 μœ μ €λ§ˆλ‹€ 락을 κ΄€λ¦¬ν•˜λ„λ‘ λ³€κ²½ν–ˆμŠ΅λ‹ˆλ‹€. λ³€κ²½λœ μ½”λ“œλ₯Ό μž‘μ„±ν•΄λ³΄κ² μŠ΅λ‹ˆλ‹€.

class RaceConditionTest {

    inner class LockManager {
        private val userLocks: MutableMap<Int, Lock> = HashMap()

        fun getLock(userId: Int): Lock {
            return userLocks.computeIfAbsent(userId) { ReentrantLock() }
        }
    }

    inner class PointManager(private val lockManager: LockManager) {
        private val userPoints: MutableMap<Int, Int> = HashMap()

        fun add(userId: Int, amount: Int) {
            val lock = lockManager.getLock(userId)
            lock.lock()
            try {
                Thread.sleep(100)
                val point = userPoints.getOrDefault(userId, 0)
                userPoints[userId] = point + amount
            } finally {
                lock.unlock()
            }
        }

        fun getPoint(userId: Int): Int {
            return userPoints.getOrDefault(userId, 0)
        }
    }

    @Test
    fun `두λͺ…μ˜ μœ μ €μ—κ²Œ 포인트λ₯Ό 100μ”© 10번 μΆ©μ „ν•˜λ©΄ ν¬μΈνŠΈκ°€ 각 1000원이 λœλ‹€`() {
        // given
        val lockManager = LockManager()
        val pointManager = PointManager(lockManager)
        val executor = Executors.newFixedThreadPool(2)

        val tasks = mutableListOf<Callable<Unit>>()

        repeat(10) {
            tasks.add(Callable { pointManager.add(1, 100) }) // μœ μ €1
            tasks.add(Callable { pointManager.add(2, 100) }) // μœ μ €2
        }

        // when
        executor.invokeAll(tasks)
        executor.shutdown()

        // then
        assertEquals(1000, pointManager.getPoint(1))
        assertEquals(1000, pointManager.getPoint(2))
    }
}
 

LockManager ν΄λž˜μŠ€λ₯Ό λ§Œλ“€μ–΄ μœ μ € 별 ReentrantLock μ„ κ΄€λ¦¬ν•˜λ„λ‘ ν–ˆμŠ΅λ‹ˆλ‹€. 그리고 μ‹€ν–‰ κ²°κ³ΌλŠ” μ•„λž˜μ™€ κ°™μŠ΅λ‹ˆλ‹€.

 

이둜써 저희가 μ›ν–ˆλ˜ λŒ€λ‘œ λ‹€λ₯Έ μœ μ €μ˜ 포인트 μΆ©μ „ μš”μ²­κ³ΌλŠ” 상관없이 μΆ©μ „ μ†Œμš” μ‹œκ°„μ΄ μ•½ 1초 κ±Έλ¦¬λŠ” 것을 λ³Ό 수 있게 λ˜μ—ˆμŠ΅λ‹ˆλ‹€!

μ €μž‘μžν‘œμ‹œ (μƒˆμ°½μ—΄λ¦Ό)

'πŸ’» 개발 > πŸ€ Spring' μΉ΄ν…Œκ³ λ¦¬μ˜ λ‹€λ₯Έ κΈ€

μ„œλΉ„μŠ€ ν™•μž₯을 μœ„ν•œ νŠΈλžœμž­μ…˜ 뢄리와 이벀트 기반 섀계  (6) 2024.11.15
Redis 기반의 캐싱 및 λŒ€κΈ°μ—΄ 관리λ₯Ό ν†΅ν•œ μ½˜μ„œνŠΈ μ˜ˆμ•½ μ„œλΉ„μŠ€ μ„±λŠ₯ κ°œμ„   (4) 2024.11.07
Spring Security λ‚΄λΆ€ 흐름 μ΄ν•΄ν•˜κΈ°  (0) 2024.02.13
μŠ€ν”„λ§ λΆ€νŠΈμ— OpenAI Whisper API μ μš©ν•˜κΈ°  (1) 2023.08.24
'πŸ’» 개발/πŸ€ Spring' μΉ΄ν…Œκ³ λ¦¬μ˜ λ‹€λ₯Έ κΈ€
  • μ„œλΉ„μŠ€ ν™•μž₯을 μœ„ν•œ νŠΈλžœμž­μ…˜ 뢄리와 이벀트 기반 섀계
  • Redis 기반의 캐싱 및 λŒ€κΈ°μ—΄ 관리λ₯Ό ν†΅ν•œ μ½˜μ„œνŠΈ μ˜ˆμ•½ μ„œλΉ„μŠ€ μ„±λŠ₯ κ°œμ„ 
  • Spring Security λ‚΄λΆ€ 흐름 μ΄ν•΄ν•˜κΈ°
  • μŠ€ν”„λ§ λΆ€νŠΈμ— OpenAI Whisper API μ μš©ν•˜κΈ°
EastShine_
EastShine_
더 λ‚˜μ€ κ°œλ°œμžκ°€ 되기 μœ„ν•œ λ‚˜μ˜ 기둝 πŸ“
  • EastShine_
    개발.LOG πŸ’»
    EastShine_
  • 전체
    였늘
    μ–΄μ œ
  • 06-25 00:37
    • λΆ„λ₯˜ 전체보기 (27)
      • πŸ’» 개발 (21)
        • πŸ–₯️ 운영체제 (3)
        • 🌏 λ„€νŠΈμ›Œν¬ (0)
        • πŸ’Ύ Database (3)
        • πŸŽ› Java (0)
        • πŸ–² Javascript (0)
        • πŸ€ Spring (5)
        • 🎸 ETC (4)
        • πŸ“ˆ μ•Œκ³ λ¦¬μ¦˜ (3)
        • πŸ“– TIL (Today I Learned) (3)
      • 🏠 일상 (6)
        • πŸ““ 일상 일기 (6)
  • 인기 κΈ€

  • νƒœκ·Έ

    νŠΈλžœμž­μ…˜ 뢄리
    e-book pdf λ³€ν™˜
    μ•Œκ³ λ¦¬μ¦˜
    λŒ€κΈ°μ—΄
    transactionaleventlistener
    Python
    λ°±μ—”λ“œ
    Whisper API
    spring
    회고
    μ½˜μ„œνŠΈμ˜ˆμ•½μ„œλΉ„μŠ€
    λ™μ‹œμ„±μ²˜λ¦¬
    μ½”λ”©ν…ŒμŠ€νŠΈ
    e-book pdf μΆ”μΆœ
    ν”„λ‘œκ·Έλž˜λ¨ΈμŠ€
    비관적락
    redis
    6κΈ°
    낙관적락
    ν•­ν•΄ν”ŒλŸ¬μŠ€
  • 졜근 λŒ“κΈ€

  • 졜근 κΈ€

  • hELLOΒ· Designed Byμ •μƒμš°.v4.10.1
EastShine_
λ™μ‹œμ„± 처리 μ‰½κ²Œ μ΄ν•΄ν•˜κΈ° (synchronized, reentrantLock)
μƒλ‹¨μœΌλ‘œ

ν‹°μŠ€ν† λ¦¬νˆ΄λ°”