DevLog ๐Ÿ˜ถ

[Redis] Redis๋Š” ์–ธ์ œ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ์„๊นŒ? 1ํŽธ - ๋ถ„์‚ฐ๋ฝ ๊ตฌํ˜„ํ•˜๊ธฐ (2) ๋ณธ๋ฌธ

๊ฐœ๋ฐœ์ผ์ง€

[Redis] Redis๋Š” ์–ธ์ œ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ์„๊นŒ? 1ํŽธ - ๋ถ„์‚ฐ๋ฝ ๊ตฌํ˜„ํ•˜๊ธฐ (2)

dolmeng2 2023. 10. 17. 09:24

๐ŸŒฑ ๋“ค์–ด๊ฐ€๊ธฐ ์ „

์ง€๋‚œ ํฌ์ŠคํŒ…์—์„œ๋Š” ๋ ˆ๋””์Šค๋ฅผ ํ™œ์šฉํ•ด ๋ถ„์‚ฐ๋ฝ์„ ๊ตฌํ˜„ํ•˜๋Š” ๋‹ค์–‘ํ•œ ๋ฐฉ๋ฒ•๋“ค์„ ์‚ดํŽด๋ณด์•˜๋‹ค.

์ด๋ฒˆ ํฌ์ŠคํŒ…์—์„œ๋Š” ์กฐ๊ธˆ ๋” ์ƒ‰๋‹ค๋ฅธ ๋ฐฉ๋ฒ•์œผ๋กœ ๋ ˆ๋””์Šค๋ฅผ ํ™œ์šฉํ•˜์—ฌ ๋ถ„์‚ฐ๋ฝ์„ ๊ตฌํ˜„ํ•ด๋ณด๊ณ ์ž ํ•œ๋‹ค.

 


 

โœ” Case 4 - Lua Script ํ™œ์šฉํ•˜๊ธฐ

Redis 2.6 ๋ฒ„์ „์—์„œ ๋ฃจ์•„ ์Šคํฌ๋ฆฝํŠธ ์—”์ง„์ด ์ถ”๊ฐ€๋˜๋ฉด์„œ, ๋ ˆ๋””์Šค ์„œ๋ฒ„์—์„œ ๋ฃจ์•„ ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋˜์—ˆ๋‹ค.

๋ ˆ๋””์Šค ๋‚ด๋ถ€์—์„œ๋Š” EVAL (ํ˜น์€ EVALSHA)์ด๋ผ๋Š” ๋ช…๋ น์–ด๋ฅผ ํ†ตํ•ด ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

EVAL script numkeys [key [key ...]] [arg [arg ...]]

๋งŒ์•ฝ ์Šคํฌ๋ฆฝํŠธ๊ฐ€ ๊ธธ์–ด์ง€๊ฒŒ ๋œ๋‹ค๋ฉด ์Šคํฌ๋ฆฝํŠธ ์ „์ฒด๋ฅผ EVAL๋กœ ์ „์†กํ•˜๊ธฐ์—๋Š” ๋„คํŠธ์›Œํฌ ๋Œ€์—ญ์˜ ๋น„์šฉ์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์—, ๋ ˆ๋””์Šค์—์„œ๋Š” SCRIPT LOAD ๋ช…๋ น์–ด๋ฅผ ํ†ตํ•ด ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์„œ๋ฒ„ ์ธก์— ์บ์‹ฑํ•œ ๋‹ค์Œ์—, ์บ์‹ฑ ํ›„ ๋ฐ˜ํ™˜๋œ ํ‚ค๋ฅผ ํ™œ์šฉํ•ด EVALSHA๋กœ ์Šคํฌ๋ฆฝํŠธ๋ฅผ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค.

(๋ฌผ๋ก  ์ด๊ฒƒ๋„ ๋งˆ๋ƒฅ ์ข‹์€ ๊ฑด ์•„๋‹ˆ๊ณ , ๋ ˆ๋””์Šค ๋…ธ๋“œ๊ฐ€ ๋งŽ์•„์ง„๋‹ค๋ฉด ์บ์‹ฑ๋„ ์ „์ฒด ๋…ธ๋“œ์—์„œ ํ•ด์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋ฌธ์ œ๊ฐ€ ์žˆ์„ ์ˆ˜ ์žˆ๋‹ค.)

 

๋ฃจ์•„(Lua)๋Š” ์ผ์ข…์˜ ํ”„๋กœ๊ทธ๋ž˜๋ฐ ์–ธ์–ด๋กœ, ์ ˆ์ฐจ์ง€ํ–ฅ์  ์–ธ์–ด์ด๋‹ค. ์‚ฌ์‹ค C, C++์™€ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋งŽ์ง€๋งŒ, ๋ ˆ๋””์Šค์—์„œ ์‚ฌ์šฉํ•˜๊ฒŒ ๋˜๋ฉด ์‚ฌ์šฉ์ž๊ฐ€ ๋ ˆ๋””์Šค ๋ช…๋ น์–ด๋ฅผ ์ง์ ‘ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ํ•ด๋‹น ์Šคํฌ๋ฆฝํŠธ๊ฐ€ ์‹คํ–‰๋˜๋Š” ๋™์•ˆ์—๋Š” ์›์ž์„ฑ์„ ๋ณด์žฅํ•˜๊ฒŒ ๋œ๋‹ค.

์ด๋Š” ๋งˆ๋ƒฅ ์ข‹์€ ์ ์€ ์•„๋‹Œ ๊ฒŒ, ์Šคํฌ๋ฆฝํŠธ์— ๋Œ€ํ•œ ์‹คํ–‰ ์š”์ฒญ์ด ๋“ค์–ด์™”์„ ๋•Œ ๋‹ค๋ฅธ ๋ ˆ๋””์Šค ๋ช…๋ น์–ด๋“ค์€ ์‹คํ–‰์ด ๋ถˆ๊ฐ€๋Šฅํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์Šคํฌ๋ฆฝํŠธ์˜ ์‹คํ–‰ ์‹œ๊ฐ„์ด ๋ฐ˜๋“œ์‹œ ๊ณ ๋ ค๋˜์–ด์•ผ ํ•œ๋‹ค. ํ•˜๋‚˜์˜ ์Šคํฌ๋ฆฝํŠธ์— ์—ฌ๋Ÿฌ ๋ ˆ๋””์Šค ๋ช…๋ น์–ด๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด์„œ ์›์ž์„ฑ์„ ๋ณด์žฅํ•˜๊ณ  ์‹ถ์„ ๋•Œ ์ฃผ๋กœ ์‚ฌ์šฉํ•œ๋‹ค.

 

override fun executeScript(key: String, value: String, duration: String, script: RedisScript<Boolean>): Boolean {
    return redisTemplate.execute(script, listOf(key), value, duration)
}

redisTemplate๋Š” ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๋Š” execute() ๋ฉ”์„œ๋“œ๋ฅผ ์ œ๊ณตํ•˜๊ณ  ์žˆ์œผ๋ฉฐ, ์‹ค์ œ๋กœ ๋‚ด๋ถ€ ๊ตฌํ˜„์ฒด๋ฅผ ์‚ดํŽด๋ณด๋ฉด eval()์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ์—ˆ๋‹ค. ์•„๋ž˜์˜ ์ฝ”๋“œ๋ฅผ ์ดํ•ดํ•  ํ•„์š”๋Š” ์—†๊ณ , ๊ทธ๋ƒฅ eval์„ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๊ตฌ๋‚˜... ์ •๋„๋กœ๋งŒ ๋„˜์–ด๊ฐ€์ž!

๋˜ํ•œ, key์˜ ๋ฆฌ์ŠคํŠธ์™€ ๋‚ด๋ถ€ ์Šคํฌ๋ฆฝํŠธ์—์„œ ์‚ฌ์šฉํ•  ๋ณ€์ˆ˜๋“ค์„ ๊ฐ€๋ณ€ ์ธ์ž๋กœ (args) ๋ฐ›์„ ์ˆ˜ ์žˆ๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

 

์•„๋ฌดํŠผ, ์ด์— ๋งž์ถฐ์„œ ํ•œ ๋ฒˆ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ์ƒ์„ฑํ•ด๋ณด์ž.

@PostMapping("/withdraw-lua")
fun withdrawUsingLuaScript(@RequestBody request: WithdrawRequest): ResponseEntity<BalanceResponse> {
    val result = accountService.withdrawUsingLuaScript(request)
    return ResponseEntity.ok(result)
}
@Transactional
fun withdrawUsingLuaScript(request: WithdrawRequest): BalanceResponse {
    val accountId = request.accountId
    val accountKey = keyGenerator.generateAccountKey(accountId)
    // ํŒŒ์ผ๋กœ ๋กœ๋“œํ•ด๋„ ๋˜์ง€๋งŒ ์ง๊ด€์ ์œผ๋กœ ์—ฌ๊ธฐ์— ๋„ฃ์—ˆ๋‹ค.
    val script = """
        local key = KEYS[1]
        local value = ARGV[1]
        local expiration = tonumber(ARGV[2])

        local existingValue = redis.call('GET', key)
        if not existingValue then
            redis.call('SET', key, value, 'EX', expiration)
            return true
        else
            return false
        end
    """.trimIndent()
    val redisScript = RedisScript<Boolean>(script)

    while (!cacheService.executeScript(accountKey, "account-withdraw", "5", redisScript)) {
        Thread.sleep(1000)
    }

    val account = accountRepository.getAccountById(accountId)
    account.subtractBalance(request.amount)

    // ์บ์‹œ ์ œ๊ฑฐ
    cacheService.delete(accountKey)
    return BalanceResponse(account.id, account.balance)
}

์Šคํฌ๋ฆฝํŠธ์˜ ๊ฒฝ์šฐ ํŒŒ์ผ๋กœ ๊ด€๋ฆฌํ•ด๋„ ๋˜์ง€๋งŒ, ๊ทธ๋ ‡๊ฒŒ ๊ธธ์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ๋ฌธ์ž์—ด์„ ์‚ฌ์šฉํ•˜์—ฌ ๋ฐ”๋กœ ๋กœ๋“œํ•˜์˜€๋‹ค.

local key = KEYS[1]
local value = ARGV[1]
local expiration = tonumber(ARGV[2])

local existingValue = redis.call('GET', key)
if not existingValue then
    redis.call('SET', key, value, 'EX', expiration)
    return true
else
    return false
end

๋‚ด๋ถ€ ์Šคํฌ๋ฆฝํŠธ๋ฅผ ํ•œ ๋ฒˆ ์‚ดํŽด๋ณด์ž.

KEYS๋ฅผ ํ†ตํ•ด listOf(key)๋กœ ๋„˜๊ฒจ์ค€ ํ‚ค์˜ ์ฒซ ๋ฒˆ์งธ ๊ฐ’์„ ๋ฐ›์•„์˜ค๊ณ , ARGV๋ฅผ ํ†ตํ•ด ๊ฐ๊ฐ ์ธ์ž๋กœ ๋„˜๊ฒจ์ค€ ๋ณ€์ˆ˜๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.

์ด๋•Œ, ์ˆซ์ž๊ฐ’์„ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด์„œ tonumber()๋ฅผ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

๋‚ด๋ถ€ ๋กœ์ง์˜ ๊ฒฝ์šฐ GET์„ ํ†ตํ•ด ๋ฐ›์•„์˜ค๊ณ , ๋งŒ์•ฝ ๊ฐ’์ด ์กด์žฌํ•˜์ง€ ์•Š์œผ๋ฉด SET์„ ํ†ตํ•ด ๊ฐ’์„ ์ง€์ •ํ•˜๊ณ , ์•„๋‹ˆ๋ฉด false๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ๊ฐ„๋‹จํ•œ ๋กœ์ง์ด๋‹ค. ์‚ฌ์‹ค์ƒ ์•ž์„œ Case 2๋ฒˆ์˜ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋ ˆ๋ฒจ์—์„œ ์ง„ํ–‰ํ–ˆ๋˜ ํ–‰์œ„๋ฅผ ๋ฃจ์•„ ์Šคํฌ๋ฆฝํŠธ๋ฅผ ํ†ตํ•ด์„œ ์›์ž์„ฑ์„ ๋ณด์žฅํ•˜๋„๋ก ๋งŒ๋“ค์–ด์ค€ ๊ฒƒ์ด๋‹ค. 

 

@Test
fun ๋ฃจ์•„์Šคํฌ๋ฆฝํŠธ๋ฅผ_ํ†ตํ•œ_๋™์‹œ_์ถœ๊ธˆ์š”์ฒญ() {
    // given
    val accountId = ๊ณ„์ขŒ_์ƒ์„ฑ()
    ์ดˆ๊ธฐ_์ž”์•ก_์„ค์ •(accountId, 100)
    val requestCount = 2

    // when
    // ๋™์‹œ์— 20์”ฉ ์ถœ๊ธˆ์„ ์š”์ฒญํ•จ
    val executor = Executors.newFixedThreadPool(requestCount)
    val latch = CountDownLatch(requestCount)
    repeat(requestCount) {
        executor.submit {
            ๋ฃจ์•„์Šคํฌ๋ฆฝํŠธ๋ฅผ_ํ†ตํ•œ_์ถœ๊ธˆ_์š”์ฒญ(accountId, 20)
            latch.countDown()
        }
    }

    latch.await()

    // then
    val balanceResponse = ์ž”์•ก_์กฐํšŒ(accountId)

    // ๋ฝ์œผ๋กœ ์ธํ•ด์„œ ์ˆœ์ฐจ์ ์œผ๋กœ ์ฒ˜๋ฆฌ๋˜๊ธฐ ๋•Œ๋ฌธ์— ์ •์ƒ์ ์œผ๋กœ 60 ๋ฐ˜ํ™˜
    assertThat(balanceResponse.balance)
        .isEqualTo(60)
}

 private fun ๋ฃจ์•„์Šคํฌ๋ฆฝํŠธ๋ฅผ_ํ†ตํ•œ_์ถœ๊ธˆ_์š”์ฒญ(accountId: Long, amount: Int): BalanceResponse {
    val withdrawRequest = WithdrawRequest(accountId, amount)

    return RestAssured.given()
        .log().all()
        .contentType(MediaType.APPLICATION_JSON_VALUE)
        .`when`()
        .body(withdrawRequest)
        .post("/accounts/withdraw-lua")
        .then()
        .log().all()
        .extract()
        .`as`(object : TypeRef<BalanceResponse>() {})
}

ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ๋Œ๋ ค๋ณด๋ฉด ์—ญ์‹œ ์ž˜ ๋™์ž‘ํ•˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค.

 


 

โœ” Case 5 - ์Šคํ•€๋ฝ ๋Œ€์‹  pub-sub ๊ตฌ์กฐ ํ™œ์šฉํ•˜๊ธฐ (Redisson)

ํ•˜์ง€๋งŒ, ์•ž์„  ๋ฐฉ์‹๋“ค์€ ๋ชจ๋‘ ์Šคํ•€๋ฝ์„ ์‚ฌ์šฉํ•˜๋‹ค ๋ณด๋‹ˆ ๋ฝ์„ ์–ป๊ธฐ ์œ„ํ•ด์„œ ๋ ˆ๋””์Šค ์„œ๋ฒ„๋กœ ๊ณ„์† ์š”์ฒญ์„ ๋ณด๋‚ด์•ผ ํ•œ๋‹ค๋Š” ๋ถ€๋‹ด์ด ์žˆ๋‹ค.

๋˜ํ•œ, ์•ž์„  ์˜ˆ์ œ๋“ค์—์„œ๋Š” ํ‚ค์— ๋Œ€ํ•ด TTL๋„ ๊ฑธ์–ด์ฃผ๊ณ , ๋ช…์‹œ์ ์œผ๋กœ delete๋„ ์ง€์ •ํ•ด์ฃผ์—ˆ๊ธฐ ๋•Œ๋ฌธ์— ํฌ๊ฒŒ ๋ฌธ์ œ๊ฐ€ ๋˜์ง€ ์•Š์•˜์ง€๋งŒ, ๊ฐœ๋ฐœ์ž๊ฐ€ ์ง์ ‘ ์Šคํ•€๋ฝ์„ ๊ตฌํ˜„ํ•˜๋‹ค ๋ณด๋‹ˆ ๋งŒ์•ฝ TTL์„ ๋นผ๋จน๊ฑฐ๋‚˜, ํ‚ค์— ๋Œ€ํ•œ ์‚ญ์ œ ์ฒ˜๋ฆฌ๋ฅผ ์ง„ํ–‰ํ•˜์ง€ ์•Š์œผ๋ฉด ๊ณ„์† ๋ฝ์„ ์ ์œ ํ•˜๋Š” ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜๋„ ์žˆ๋‹ค.

 

๊ทธ๋ž˜์„œ, Lettuce ๋Œ€์‹ ์— Redission์—์„œ ์ œ๊ณตํ•˜๋Š” ๋ถ„์‚ฐ๋ฝ์„ ์‚ฌ์šฉํ•˜์—ฌ ์ด๋ฅผ ํ•ด๊ฒฐํ•ด๋ณด์ž.

Redisson์˜ ๊ฒฝ์šฐ ๊ธฐ๋ณธ์ ์œผ๋กœ Pub-Sub ๊ธฐ๋ฐ˜์˜ ๊ตฌ์กฐ์ด๊ธฐ ๋•Œ๋ฌธ์— ๊ณ„์† ์—ฐ๊ฒฐ์„ ์‹œ๋„ํ•˜์ง€ ์•Š๋Š”๋‹ค.

Publish-Subscribe (Pub-Sub) ๊ตฌ์กฐ๋Š” Publisher์™€ Subscriber๊ฐ€ ์„œ๋กœ ์•Œ์ง€ ๋ชปํ•ด๋„ ํ†ต์‹ ์ด ๊ฐ€๋Šฅํ•˜๋„๋ก ๋งŒ๋“ค์–ด์ง„ ํŒจํ„ด์ด๋‹ค.
Publisher๋Š” Subscriber์—๊ฒŒ ์ง์ ‘์ ์œผ๋กœ ๋ฉ”์‹œ์ง€๋ฅผ ๋ณด๋‚ด์ง€ ์•Š๊ณ , Channel์— publish๋ฅผ ์ง„ํ–‰ํ•˜๊ฒŒ ๋œ๋‹ค.
Subscriber๋Š” ๊ด€์‹ฌ์ด ์žˆ๋Š” ์ฑ„๋„์„ ํ•„์š”์— ๋”ฐ๋ผ Subscribe๋ฅผ ํ†ตํ•ด ๋ฉ”์‹œ์ง€๋ฅผ ์ˆ˜์‹ ํ•  ์ˆ˜ ์žˆ๋‹ค.

ํ•ด๋‹น ์ฑ„๋„์„ Subscribe๋ฅผ ํ•˜๊ณ  ์žˆ๋Š” ์Šค๋ ˆ๋“œ๋“ค์ด Publish๋œ ๋ฉ”์‹œ์ง€๋ฅผ ๋ฐ›์•„์•ผ ๋ฝ ์ ์œ ๋ฅผ ์‹œ๋„ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋น„๊ต์  ๋ถ€ํ•˜๊ฐ€ ๋œํ•˜๋‹ค.

๋ ˆ๋””์Šค์—์„œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋ช…๋ น์–ด๋ฅผ ์ œ๊ณตํ•˜๊ณ  ์žˆ๋‹ค.

// order๋ผ๋Š” ์ฑ„๋„์— ๋Œ€ํ•ด์„œ new-order๋ผ๋Š” ๋ฉ”์‹œ์ง€ ๋ฐœํ–‰ ์˜ˆ์ œ
PUBLISH order new-order

// order, delivery๋ผ๋Š” ์ฑ„๋„์— ๋Œ€ํ•ด์„œ ๋ฉ”์‹œ์ง€ ๊ตฌ๋… ์˜ˆ์ œ
SUBSCRIBE order, delivery

publish ์‹œ ์ฑ„๋„๊ณผ ๋ฐœํ–‰ํ•  ๋ฉ”์‹œ์ง€๋ฅผ ์ž…๋ ฅํ•˜๊ณ , subscribe ์‹œ ๊ตฌ๋…ํ•˜๊ณ  ์‹ถ์€ ์ฑ„๋„ ์ด๋ฆ„์„ ๋‚˜์—ดํ•˜๋ฉด ๋œ๋‹ค.

publish ํ›„ ์„ฑ๊ณต์ ์œผ๋กœ ๋ฉ”์‹œ์ง€๋ฅผ ๋ฐœํ–‰ํ•˜๋ฉด 1์„ ๋ฐ˜ํ™˜ํ•˜๊ณ , ๋งŒ์•ฝ ์•„๋ฌด๋„ ๊ตฌ๋…ํ•˜๊ณ  ์žˆ์ง€ ์•Š๋Š” ์ฑ„๋„์— ๋ฐœํ–‰์„ ํ•˜๋ฉด 0์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.

 

 

์ด๋ฅผ ์ฝ”๋“œ๋กœ ๊ตฌํ˜„ํ•˜๊ธฐ ์œ„ํ•ด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์˜์กด์„ฑ์„ ์ถ”๊ฐ€ํ•ด์ฃผ๋„๋ก ํ•˜์ž.

implementation("org.redisson:redisson-spring-boot-starter:3.23.5")

 

๊ทธ๋ฆฌ๊ณ , ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ์ž‘์„ฑํ•ด๋ณด์ž.

@PostMapping("withdraw-redisson")
fun withdrawUsingRedisson(@RequestBody request: WithdrawRequest): ResponseEntity<BalanceResponse> {
    val result = accountService.withdrawUsingRedisson(request)
    return ResponseEntity.ok(result)
}
@Service
class AccountService(
    // RedissonClient์— ๋Œ€ํ•ด ์˜์กด์„ฑ ์ฃผ์ž… ํ•„์š”
    private val redissonClient: RedissonClient,
    private val accountRepository: AccountRepository,
    private val keyGenerator: KeyGenerator
) {

    @Transactional
    fun withdrawUsingRedisson(request: WithdrawRequest): BalanceResponse {
        val accountId = request.accountId
        val accountKey = keyGenerator.generateAccountKey(accountId)

        // ๋ฝ ํš๋“
        val lock = redissonClient.getLock(accountKey)
        try {
            val canLock = lock.tryLock(3, 5, TimeUnit.SECONDS)
            if (canLock.not()) {
                throw RuntimeException("๋ฝ ํš๋“์— ์‹คํŒจํ•˜์˜€์Šต๋‹ˆ๋‹ค.")
            }
            val account = accountRepository.getAccountById(accountId)
            account.subtractBalance(request.amount)
            return BalanceResponse(account.id, account.balance)
            
        } finally {
            // ๋ฝ ํ•ด์ œ
            lock.unlock()
        }
    }
}

redissionClient์—์„œ๋Š” getLock()์ด๋ผ๋Š” ๋ฉ”์„œ๋“œ๋ฅผ ํ†ตํ•ด ์ธ์ž๋กœ ๋„˜๊ฒจ์ค€ ์ด๋ฆ„์„ ๊ฐ€์ง„ Lock instance๋ฅผ ์ƒ์„ฑํ•œ๋‹ค.

์ด๋•Œ, ์Šค๋ ˆ๋“œ๊ฐ€ ๋ฝ์„ ํš๋“ํ•˜๋Š” ์ˆœ์„œ๋ฅผ ๋ณด์žฅํ•˜์ง€ ์•Š๋Š”๋‹ค. (non-fair locking์ด๋ผ๊ณ  ํ•˜๋Š”๋ฐ, ์กฐ๊ธˆ ๋” ์ •ํ™•ํ•˜๊ฒŒ๋Š” ํŠน์ • ์Šค๋ ˆ๋“œ๊ฐ€ ๋จผ์ € ๋“ค์–ด์™€์„œ ์˜ค๋žซ๋™์•ˆ ๋ฝ์„ ๋Œ€๊ธฐํ•˜๊ณ  ์žˆ์„ ๋•Œ, ์ƒˆ๋กœ์šด ์Šค๋ ˆ๋“œ๊ฐ€ ์™€์„œ ์š”์ฒญ์„ ๋ณด๋‚ด๋ฉด ์˜›๋‚  ์Šค๋ ˆ๋“œ๊ฐ€ ์•„๋‹ˆ๋ผ ์ƒˆ๋กœ์šด ์Šค๋ ˆ๋“œ๊ฐ€ ๋ฝ์„ ์ ์œ ํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ์˜๋ฏธ์ธ ๊ฒƒ ๊ฐ™๋‹ค. ์ฐธ๊ณ  ๋งํฌ)

 

์ดํ›„, ๊ฐ€์ ธ์˜จ ์ธ์Šคํ„ด์Šค๊ฐ€ ๋ฝ์„ ํš๋“ํ•  ์ˆ˜ ์žˆ๋Š” ์ƒํ™ฉ์ด๋ผ๋ฉด (tryLock) ์ ์œ ํ•˜๊ณ , ์•„๋‹ˆ๋ผ๋ฉด ์˜ˆ์™ธ๋ฅผ ๋ฐœ์ƒํ•˜๋„๋ก ๋งŒ๋“ค์—ˆ๋‹ค.

ํ•ญ์ƒ ๋ฝ์— ๋Œ€ํ•œ ํ•ด์ œ ๋กœ์ง์€ ๋Œ์•„์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์— try-finally ๊ตฌ๋ฌธ์„ ํ†ตํ•ด unlock์„ ๊ผญ ํ•ด์ฃผ๋„๋ก ํ•˜์ž.

 

ํ•œ ๋ฒˆ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋กœ ํ™•์ธํ•ด๋ณด์ž.

@Test
fun ๋ ˆ๋””์Šจ์„_ํ†ตํ•œ_๋™์‹œ_์ถœ๊ธˆ์š”์ฒญ() {
    // given
    val accountId = ๊ณ„์ขŒ_์ƒ์„ฑ()
    ์ดˆ๊ธฐ_์ž”์•ก_์„ค์ •(accountId, 100)
    val requestCount = 2

    // when
    // ๋™์‹œ์— 20์”ฉ ์ถœ๊ธˆ์„ ์š”์ฒญํ•จ
    val executor = Executors.newFixedThreadPool(requestCount)
    val latch = CountDownLatch(requestCount)
    repeat(requestCount) {
        executor.submit {
            ๋ ˆ๋””์Šจ์„_ํ†ตํ•œ_์ถœ๊ธˆ_์š”์ฒญ(accountId, 20)
            latch.countDown()
        }
    }

    latch.await()

    // then
    val balanceResponse = ์ž”์•ก_์กฐํšŒ(accountId)

    // ๋ฝ์œผ๋กœ ์ธํ•ด์„œ ์ˆœ์ฐจ์ ์œผ๋กœ ์ฒ˜๋ฆฌ๋˜๊ธฐ ๋•Œ๋ฌธ์— ์ •์ƒ์ ์œผ๋กœ 60 ๋ฐ˜ํ™˜
    assertThat(balanceResponse.balance)
        .isEqualTo(60)
}

private fun ๋ ˆ๋””์Šจ์„_ํ†ตํ•œ_์ถœ๊ธˆ_์š”์ฒญ(accountId: Long, amount: Int): BalanceResponse {
    val withdrawRequest = WithdrawRequest(accountId, amount)

    return RestAssured.given()
        .log().all()
        .contentType(MediaType.APPLICATION_JSON_VALUE)
        .`when`()
        .body(withdrawRequest)
        .post("/accounts/withdraw-redisson")
        .then()
        .log().all()
        .extract()
        .`as`(object : TypeRef<BalanceResponse>() {})
}

์•„์ฃผ ์ž˜ ๋Œ์•„๊ฐ€๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

 

์ด๋Œ€๋กœ ๋๋‚ด๋ฉด ์•„์‰ฝ๊ธฐ ๋•Œ๋ฌธ์—, ๋‚ด๋ถ€์ ์œผ๋กœ ์–ด๋–ป๊ฒŒ pub-sub ๊ตฌ์กฐ๋ฅผ ํ†ตํ•ด ๋ฝ์„ ํš๋“ํ•˜๋Š”์ง€ ํ™•์ธํ•ด๋ณด์ž.

boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException;

๋จผ์ €, tryLock์˜ ๊ฒฝ์šฐ ๋‚ด๋ถ€์ ์œผ๋กœ 3๊ฐœ์˜ ์ธ์ž๋ฅผ ๋ฐ›๋Š”๋‹ค.

waitTime์€ ๋ฝ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์„ ๋•Œ๊นŒ์ง€ ๋Œ€๊ธฐํ•˜๋Š” ์‹œ๊ฐ„, leaseTime์€ ๋ฝ์„ ์ ์œ ํ•˜๋Š” ์‹œ๊ฐ„, unit์€ ์‹œ๊ฐ„์— ๋Œ€ํ•œ ๋‹จ์œ„์ด๋‹ค.

์ด๋Š” ์ฆ‰, leaseTime์ด ์ง€๋‚˜๊ฒŒ ๋˜๋ฉด ์ž๋™์œผ๋กœ ๋ฝ์ด ํ•ด์ œ๋˜๋ฉฐ, ์ด๋ฏธ ๋ฝ์„ ์ ์œ ํ•˜๊ณ  ์žˆ๋Š” ์Šค๋ ˆ๋“œ๊ฐ€ ์กด์žฌํ•  ๋•Œ ๋‹ค๋ฅธ ์Šค๋ ˆ๋“œ๊ฐ€ ์š”์ฒญ์„ ๋ณด๋‚ด๋ฉด waitTime๊นŒ์ง€ ๋Œ€๊ธฐํ•œ๋‹ค๋Š” ์˜๋ฏธ์ด๋‹ค.

 

tryLock์˜ ๋‚ด๋ถ€ ๋กœ์ง์„ ์‚ด์ง ์‚ดํŽด๋ณด๋ฉด ํ•œ ๊ฐ€์ง€ ํฅ๋ฏธ๋กœ์šด ์ ์„ ๋ฐœ๊ฒฌํ•  ์ˆ˜ ์žˆ๋Š”๋ฐ, ๋ฐ”๋กœ ๋ฃจ์•„ ์Šคํฌ๋ฆฝํŠธ๋ฅผ ํ™œ์šฉํ•œ๋‹ค๋Š” ์ ์ด๋‹ค.

์œ„์™€ ๊ฐ™์ด ํ‚ค์˜ ์กด์žฌ์— ๋Œ€ํ•œ ์—ฌ๋ถ€ ํ™•์ธ ๋ฐ ํ•ด๋‹น ํ‚ค์˜ ๊ฐ’์— ๋Œ€ํ•œ ์ฆ๊ฐ€๋Š” ๋ฃจ์•„์Šคํฌ๋ฆฝํŠธ๋ฅผ ํ†ตํ•ด ์ง„ํ–‰ํ•˜๊ณ  ์žˆ์—ˆ๋‹ค.

๋ฝ์ด ์กด์žฌํ•˜์ง€ ์•Š๋Š”๋‹ค๋ฉด ๋ฝ์— ๋Œ€ํ•œ ํ‚ค์™€ ์Šค๋ ˆ๋“œ ์•„์ด๋””๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๊ฐ’์„ 1 ์ฆ๊ฐ€์‹œํ‚ค๊ณ , TTL์„ ์„ค์ •ํ•˜๋Š” ๋กœ์ง์ด๋‹ค.

 

๊ทธ๋ฆฌ๊ณ , tryLock ๋ฉ”์„œ๋“œ์˜ ํ•˜๋‹จ์œผ๋กœ ๋‚ด๋ ค๊ฐ€๋ณด๋ฉด ์•„๋ž˜์™€ ๊ฐ™์ด ๋ฝ์— ๋Œ€ํ•ด ๋Œ€๊ธฐํ•˜๋Š” ๋กœ์ง์ด ๋‚˜์˜จ๋‹ค.

์ด๋•Œ, subscribe ๋‚ด๋ถ€ ๋กœ์ง์„ ๋ณด๋ฉด ์„ธ๋งˆํฌ์–ด๋ฅผ ํ™œ์šฉํ•˜๊ณ  ์žˆ๋‹ค.

์•ž์„œ ๋งํ–ˆ๋˜ ๊ฒƒ์ฒ˜๋Ÿผ, ๋ฉ€ํ‹ฐ ์Šค๋ ˆ๋“œ ํ™˜๊ฒฝ์ธ ์Šคํ”„๋ง๋ถ€ํŠธ๊ฐ€ ์‹ฑ๊ธ€ ์Šค๋ ˆ๋“œ ๊ธฐ๋ฐ˜์ธ ๋ ˆ๋””์Šค๋ฅผ ํ˜ธ์ถœํ•˜๊ฒŒ ๋˜๋ฉด ๋™์‹œ์„ฑ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์—, ์„ธ๋งˆํฌ์–ด๋ฅผ ํ™œ์šฉํ•ด์„œ ํ•ญ์ƒ ํ•˜๋‚˜์˜ ์Šค๋ ˆ๋“œ๋งŒ ์ ‘๊ทผ์ด ๊ฐ€๋Šฅํ•˜๋„๋ก ๋งŒ๋“ค๊ณ  ์žˆ๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

subscribe๋ฅผ ์‹คํŒจํ•˜๋ฉด (์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด) false๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋ฉฐ ๋กœ์ง์„ ์ข…๋ฃŒํ•œ๋‹ค.

 

์ดํ›„, while๋ฌธ์„ ๋Œ๋ฉด์„œ ๋‚จ์•„ ์žˆ๋Š” ์‹œ๊ฐ„ ๋™์•ˆ ๋ฝ์„ ํš๋“ํ•˜๊ธฐ ์œ„ํ•ด ์žฌ์‹œ๋„๋ฅผ ์ง„ํ–‰ํ•œ๋‹ค.

์—ฌ๊ธฐ์„œ tryAcquire()์— ์˜ํ•ด ๋ฐ˜ํ™˜๋˜๋Š” ttl ๊ฐ’์€ ํ˜„์žฌ ์Šค๋ ˆ๋“œ๊ฐ€ ์ ์œ ๋ฅผ ์‹œ๋„ํ•˜๊ณ  ์žˆ๋Š” ๋ฝ์ด ๋‹ค๋ฅธ ์Šค๋ ˆ๋“œ์— ์˜ํ•ด ์–ธ์ œ ํ•ด์ œ๋ ์ง€ ๋‚˜ํƒ€๋‚ด๋Š” ๊ฐ’์„ (= ๋ฝ์ด ์–ผ๋งˆ๋‚˜ ๋” ์œ ์ง€๋ ์ง€) ์˜๋ฏธํ•˜๋ฉฐ, null์ด๋ผ๋ฉด ๋ฝ์„ ํš๋“ํ•  ์ˆ˜ ์žˆ๋Š” ์ƒํƒœ์ด๊ธฐ ๋•Œ๋ฌธ์— true๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ฒŒ ๋œ๋‹ค.

๋งŒ์•ฝ ๋ฝ ํš๋“์— ์‹คํŒจํ–ˆ๋‹ค๋ฉด time (waitTime์„ ๋‚˜ํƒ€๋‚ด๋ฉฐ, ๋Œ€๊ธฐ ์‹œ๊ฐ„์„ ์˜๋ฏธํ•œ๋‹ค)์˜ ๊ฐ’์„ ๊ฐฑ์‹ ํ•ด์ฃผ๊ณ , ๊ฐฑ์‹ ๋œ ๊ฐ’์ด ๋งŒ๋ฃŒ๋˜์—ˆ๋‹ค๋ฉด ๋ฝ ํš๋“์— ์‹คํŒจํ–ˆ๋‹ค๊ณ  ๊ฐ„์ฃผํ•˜์—ฌ false๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ฒŒ ๋œ๋‹ค.

 

์ดํ›„, ์•„์ง ๋Œ€๊ธฐ ์‹œ๊ฐ„์ด ๋‚จ์•„ ์žˆ๋Š” ์ƒํƒœ๋ผ๋ฉด ํ˜„์žฌ ์‹œ๊ฐ„์„ ๋‹ค์‹œ ๊ฐฑ์‹ ํ•œ๋‹ค.

๋งŒ์•ฝ ttl (๋ฝ์— ๋Œ€ํ•ด ๋‚จ์€ ์œ ํšจ ์‹œ๊ฐ„)์ด 0๋ณด๋‹ค ํฌ๊ฑฐ๋‚˜ ๊ฐ™์œผ๋ฉด์„œ time (๋‚จ์€ ๋Œ€๊ธฐ ์‹œ๊ฐ„)๋ณด๋‹ค ์ž‘์œผ๋ฉด ttl ์‹œ๊ฐ„ ๋™์•ˆ ๋‹ค์‹œ ๋ฝ ํš๋“์„ ๋Œ€๊ธฐํ•˜๋ฉฐ, ๊ทธ๊ฒŒ ์•„๋‹ˆ๋ผ๋ฉด ๋‚จ์€ ๋Œ€๊ธฐ ์‹œ๊ฐ„ ๋™์•ˆ ๊ธฐ๋‹ค๋ฆฌ๊ฒŒ ๋œ๋‹ค. (= ๊ทธ๋ƒฅ ํš๋“์„ ttl, time๋งŒํผ ๋Œ€๊ธฐํ•˜๋ฉฐ ์‹œ๋„ํ•œ๋‹ค๊ณ  ์ƒ๊ฐํ•˜๊ธฐ)

์ตœ์ข…์ ์œผ๋กœ ๋Œ€๊ธฐ ์‹œ๊ฐ„์ธ time์„ ๊ฐฑ์‹ ํ•˜๊ณ , ๋งŒ๋ฃŒ๋˜์—ˆ๋‹ค๋ฉด ๋ฝ ํš๋“์— ์‹คํŒจํ–ˆ๋‹ค๊ณ  ๊ฐ„์ฃผํ•˜๊ณ  false๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ฒŒ ๋œ๋‹ค.

 

๊ทธ๋ฆฌ๊ณ  ๋งˆ์ง€๋ง‰์œผ๋กœ ์ด๋Ÿฌํ•œ ๋ฝ ํš๋“ ์‹œ๋„๊ฐ€ ์ข…๋ฃŒ๋˜๋ฉด unsubscribe๋ฅผ ํ†ตํ•ด ๊ตฌ๋…์„ ํ•ด์ œํ•œ๋‹ค.

๋งˆ์ฐฌ๊ฐ€์ง€๋กœ ๊ตฌ๋… ํ•ด์ œ ์‹œ์—๋„ ์ด๋ ‡๊ฒŒ ์„ธ๋งˆํฌ์–ด๋ฅผ ํ™œ์šฉํ•˜๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ๋‹ค.

 


 

๐ŸŒฑ ๋งˆ๋ฌด๋ฆฌ

๋ ˆ๋””์Šค ๊ด€๋ จํ•ด์„œ ๊ณ„์† ์จ์•ผ์ง€, ์จ์•ผ์ง€ ํ–ˆ๋Š”๋ฐ ์–ด์ฉŒ๋‹ค ๋ณด๋‹ˆ ๊ณ„์† ๋ฏธ๋ฃจ๋‹ค๊ฐ€ ์ด์ œ์„œ์•ผ ์ž‘์„ฑํ•˜๊ฒŒ ๋˜์—ˆ๋‹ค.

10์›”์—๋Š” ๊ธ€ 2๊ฐœ ์ •๋„๋Š” ์˜ฌ๋ฆฌ๋„๋ก ๋…ธ๋ ฅํ•ด๋ด์•ผ๊ฒ ๋‹ค... ๐Ÿฅฒ ๋‚˜ํƒœํ•ด์ง„ ๋‚˜ ๋ฐ˜์„ฑํ•ด... 

 

+ ๋ฌด์กฐ๊ฑด ๋ ˆ๋””์Šค๋ฅผ ํ†ตํ•ด ๋ถ„์‚ฐ๋ฝ์„ ๋„์ž…ํ•ด์•ผ ํ•œ๋‹ค๊ณ ๋Š” ์ƒ๊ฐํ•˜์ง€ ์•Š๋Š”๋‹ค.

๋ชจ๋“  ๊ธฐ์ˆ ์˜ ๋„์ž…์—๋Š” ์ด์œ ๊ฐ€ ์žˆ์–ด์•ผ ํ•œ๋‹ค๊ณ  ์ƒ๊ฐํ•˜๊ธฐ ๋•Œ๋ฌธ์—... ๋ ˆ๋””์Šค๋ฅผ ํ™œ์šฉํ•˜์ง€ ์•Š๋”๋ผ๋„ ์–ผ๋งˆ๋“ ์ง€ ๋™์‹œ์„ฑ ์ œ์–ด๋Š” ๊ฐ€๋Šฅํ•˜๋‹ค.

์•ž์œผ๋กœ ๋„์ž…์„ ํ•˜๊ฒŒ ๋œ๋‹ค๋ฉด ๋ฌด์ž‘์ • ๋„์ž…ํ•˜์ง€ ์•Š๊ณ , ์ด๋Ÿฌํ•œ ์ ๋“ค์— ๋Œ€ํ•ด trade-off๋ฅผ ์ž˜ ๊ณ ๋ฏผํ•ด๋ด์•ผ๊ฒ ๋‹ค ๐Ÿ˜ถ

Comments