DevLog ๐Ÿ˜ถ

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

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

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

dolmeng2 2023. 10. 17. 09:23

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

์กฐ๊ธˆ ์˜ค๋ž˜๋˜๊ธด ํ–ˆ์ง€๋งŒ ์ด์ „ ํฌ์ŠคํŒ…์—์„œ ๋„ค์ž„๋“œ๋ฝ์„ ํ†ตํ•œ ๋ถ„์‚ฐ๋ฝ ๊ตฌํ˜„์„ ์ง„ํ–‰ํ•˜์˜€๋‹ค. ๋งˆ์ง€๋ง‰ ํฌ์ŠคํŒ…์—์„œ ๋ ˆ๋””์Šค๋ฅผ ํ™œ์šฉํ•œ๋‹ค๊ณ  ๋งํ–ˆ์—ˆ๋Š”๋ฐ, ๊ณ„์† ๋ฏธ๋ฃจ๋‹ค๊ฐ€ ์ด๋ฒˆ์— ๋ ˆ๋””์Šค ๊ณต๋ถ€๋ฅผ ํ•˜๋ฉด์„œ ๊ฐ„๋‹จํ•˜๊ฒŒ ๊ตฌํ˜„์„ ์ง„ํ–‰ํ•ด๋ณด์•˜๋‹ค. ๋ถ„์‚ฐ๋ฝ์— ๋Œ€ํ•œ ๊ฐœ๋…์€ ์ด์ „ ํฌ์ŠคํŒ…์—์„œ ๋‹ค๋ฃจ์—ˆ๊ธฐ ๋•Œ๋ฌธ์—, ๋ ˆ๋””์Šค๋ฅผ ์ด์šฉํ•ด์„œ ๋ฐ”๋กœ ๋ถ„์‚ฐ๋ฝ์„ ๊ตฌํ˜„ํ•ด๋ณด์ž. ๋˜ํ•œ, ์ง€๋‚œ ํฌ์ŠคํŒ…์—์„œ๋Š” Jmeter๋ฅผ ํ™œ์šฉํ•˜์˜€์ง€๋งŒ, ์ด๋ฒˆ ํฌ์ŠคํŒ…์—์„œ๋Š” E2E ํ…Œ์ŠคํŠธ๋ฅผ ํ†ตํ•ด ์ง์ ‘ ๊ตฌํ˜„์„ ํ•ด๋ณด๊ณ ์ž ํ•œ๋‹ค.
 


 

๐ŸŒฑ ๊ตฌํ˜„ ์ƒํ™ฉ

์ •๋ง ๊ฐ„๋‹จํ•˜๊ฒŒ ์ž”์•ก์„ ์˜๋ฏธํ•˜๋Š” 'Balance'๋ผ๋Š” ๊ฐ’์—, 2๊ฐœ์˜ ์š”์ฒญ์ด ๋™์‹œ์— ๋“ค์–ด์™”์„ ๋•Œ ๋ ˆ๋””์Šค๋ฅผ ํ†ตํ•œ ๋ถ„์‚ฐ๋ฝ์„ ๊ตฌํ˜„ํ•˜์—ฌ ์ œ์–ดํ•ด๋ณด์ž.
์œ„์˜ ๊ฒฐ๊ณผ์—์„œ ์ตœ์•…์˜ ์ƒํ™ฉ์ด๋ผ๋ฉด Request 1, 2๋ฒˆ ๋ชจ๋‘ 80์ด๋ผ๋Š” ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•˜์—ฌ ์ •์ƒ์ ์œผ๋กœ ์ž”์•ก์ด ๊ฐฑ์‹ ๋˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ๋‹ค.
์šฐ๋ฆฌ๋Š” ์•„๋ž˜์™€ ๊ฐ™์€ ํ”Œ๋กœ์šฐ๋ฅผ ๊ตฌํ˜„ํ•  ์˜ˆ์ •์ด๋‹ค.

1. ๊ณต์œ  ์ž์›์„ ํ‘œํ˜„ํ•  ์ˆ˜ ์žˆ๋Š” ์ด๋ฆ„์„ ์ƒ์„ฑํ•˜์—ฌ ๋ฝ์˜ ํ‚ค๋ฅผ ๊ฒฐ์ •ํ•˜๊ธฐ
2. NX ์˜ต์…˜์„ ํ†ตํ•ด์„œ ํ•ด๋‹น ํ‚ค๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ์—๋งŒ ๋ฝ์„ ํš๋“ํ•˜๊ธฐ
3. ์ด๋ฏธ ๋ฝ์„ ํš๋“ ํ•œ ๊ฒฝ์šฐ nil์„ ๋ฐ˜ํ™˜ํ•˜์—ฌ ํ”„๋กœ์„ธ์Šค ๋Œ€๊ธฐํ•˜๊ธฐ
4. ์ž์›์˜ ์ž‘์—…์ด ๋๋‚˜๋ฉด DEL์„ ํ†ตํ•ด ๋ฝ ์ œ๊ฑฐํ•˜๊ธฐ

 ์—ฌ๊ธฐ์„œ NX์™€ DEL์€ ๋ ˆ๋””์Šค์—์„œ ์‚ฌ์šฉํ•˜๋Š” ๋ช…๋ น์–ด๋‹ค.
NX๋Š” 'IF NOT EXISTS SET'๊ณผ ๊ฐ™์ด, ํ‚ค๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์„ ๊ฒฝ์šฐ์—๋งŒ SET์„ ํ•ด์ค€๋‹ค. ์‹ฑ๊ธ€ ์Šค๋ ˆ๋“œ๋กœ ๋™์ž‘ํ•˜๋Š” ๋ ˆ๋””์Šค์—์„œ ํ‚ค๊ฐ€ ์กด์žฌํ•˜๋Š”์ง€ ํŒ๋‹จํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” GET์œผ๋กœ ํ™•์ธ ํ›„ SET์„ ํ•ด์•ผ ํ•œ๋‹ค. ํ•˜์ง€๋งŒ ๋ฉ€ํ‹ฐ ์Šค๋ ˆ๋“œ ํ™˜๊ฒฝ์ธ ์Šคํ”„๋ง ๊ฐ™์€ ๊ณณ์—์„œ ์‚ฌ์šฉํ•  ๋•Œ ์ •ํ•ฉ์„ฑ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์—, ํ•œ ๋ฒˆ์— ์ค‘๋ณต์„ ์ฒดํฌํ•˜๊ณ  ์‚ฝ์ž…๊นŒ์ง€ ์ง„ํ–‰ํ•  ์ˆ˜ ์žˆ๋„๋ก ๋งŒ๋“ค์–ด์ง„ ๋ช…๋ น์–ด๋ผ๊ณ  ์ƒ๊ฐํ•˜๋ฉด ์ดํ•ดํ•˜๊ธฐ ์‰ฝ๋‹ค.
DEL์˜ ๊ฒฝ์šฐ ์‰ฝ๊ฒŒ ์˜ˆ์ƒํ•  ์ˆ˜ ์žˆ๋“ฏ, 'DELETE'๋ฅผ ์˜๋ฏธํ•˜๋Š” ํ‚ค ์‚ญ์ œ ๊ธฐ๋Šฅ์„ ์˜๋ฏธํ•œ๋‹ค.

์šฐ๋ฆฌ๋Š” ์Šคํ•€ ๋ฝ (Spin Lock) ๋ฐฉ์‹์„ ํ†ตํ•ด ๋ฝ์„ ์ ์œ ํ•˜๊ณ  ์žˆ๋Š”์ง€ ๊ณ„์† ํ™•์ธํ•˜๋ฉด์„œ, ๋ฝ์„ ์žก์„ ์ˆ˜ ์žˆ๋‹ค๋ฉด ๋ฝ์„ ์žก๊ณ  ์•„๋‹ˆ๋ฉด ๋Œ€๊ธฐํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ๊ตฌํ˜„ํ•˜๊ณ ์ž ํ•œ๋‹ค. ๋ฝ์— ๋Œ€ํ•œ ํ‚ค๋ฅผ ๊ณ„์† ์ ์œ ํ•˜์ง€ ์•Š๋„๋ก ํ‚ค๋Š” ์ž„๊ณ„ ์˜์—ญ์„ ๋น ์ ธ๋‚˜์˜จ ํ›„ ์ œ๊ฑฐ๋ฅผ ํ•ด์ค˜์•ผ ํ•˜๋ฉฐ, ํ‚ค ์„ค์ • ์‹œ TTL๋„ ํ•จ๊ป˜ ๊ฑธ์–ด์ฃผ๋Š” ๊ฒƒ์ด ์ข‹๋‹ค.



 

๐ŸŒฑ ๊ตฌํ˜„ํ•˜๊ธฐ - ํ™˜๊ฒฝ ๊ตฌ์ถ•

ํ•˜๋‹จ์— ๋‚˜์˜จ ์„ค์ •๋“ค์€ ์•ž์œผ๋กœ redis ๊ด€๋ จ ํฌ์ŠคํŒ…์„ ํ•  ๋•Œ ๊ณ„์† ์‚ฌ์šฉํ•  ์˜ˆ์ •์ด๊ธฐ ๋•Œ๋ฌธ์—, ์ด๋ฒˆ ํฌ์ŠคํŒ…์—์„œ๋งŒ ํ™˜๊ฒฝ ๊ตฌ์ถ•์— ๋Œ€ํ•ด ์ž‘์„ฑํ•˜๊ณ ์ž ํ•œ๋‹ค.

 

implementation("org.springframework.boot:spring-boot-starter-data-redis")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
runtimeOnly("com.h2database:h2")
testImplementation("io.rest-assured:rest-assured")

๋จผ์ €, ๊ธฐ๋ณธ ๊ตฌํ˜„์„ ์œ„ํ•ด ํ•„์š”ํ•œ dependency๋“ค์ด๋‹ค. (์–ธ์–ด๋Š” Kotlin์œผ๋กœ ์ž‘์„ฑํ•  ์˜ˆ์ •์ด๋‹ค.)
์Šคํ”„๋ง์—์„œ ์ œ๊ณตํ•˜๋Š” redis ๋ฐ jpa, ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ ๊ตฌ์ถ•์„ ์œ„ํ•œ h2์™€ RestAssured๋ฅผ ์ถ”๊ฐ€ํ•ด์ฃผ์ž.

@Configuration
class RedisConfig {

    @Bean
    fun redisConnectionFactory(): RedisConnectionFactory =
        LettuceConnectionFactory()

    @Bean
    fun redisTemplate(): RedisTemplate<String, String> {
        return RedisTemplate<String, String>().apply {
            connectionFactory = redisConnectionFactory()
            keySerializer = StringRedisSerializer()
            valueSerializer = StringRedisSerializer()
        }
    }
}

์ด๋ฒˆ์—” ๋ ˆ๋””์Šค ๊ด€๋ จ ์„ค์ • ํด๋ž˜์Šค๋ฅผ ๋งŒ๋“ค์–ด์ฃผ์ž.
๋ ˆ๋””์Šค ํด๋ผ์ด์–ธํŠธ ์ค‘ ์ž๋ฐ” ๊ธฐ๋ฐ˜ ํ™˜๊ฒฝ์—์„œ ๋Œ€ํ‘œ์ ์œผ๋กœ ๋ฝ‘ํžˆ๋Š” Lettuce๋ฅผ ์‚ฌ์šฉํ•˜์˜€์œผ๋ฉฐ, ๋ ˆ๋””์Šค์— ์ €์žฅ๋  key, value๋Š” ํ˜„์žฌ ์ŠคํŽ™์—์„œ ๋ชจ๋‘ String์„ ์‚ฌ์šฉํ•˜๋ฉด ๋˜๊ธฐ ๋•Œ๋ฌธ์— StringRedisSerializer()๋ฅผ ์‚ฌ์šฉํ•ด์ฃผ์—ˆ๋‹ค. ์ทจํ–ฅ์— ๋งž๊ฒŒ ์ปค์Šคํ…€์„ ํ•ด์ฃผ๋ฉด ๋  ๊ฒƒ ๊ฐ™๋‹ค.

 

class RedisService(
    private val redisTemplate: RedisTemplate<String, String>
) : CacheService {
    override fun delete(key: String) {
        redisTemplate.delete(key)
    }
}

๋˜ํ•œ, ์บ์‹œ์˜ ํ‚ค๋ฅผ ์ œ๊ฑฐํ•ด์ฃผ๋Š” ํ•จ์ˆ˜๋ฅผ ๋ฏธ๋ฆฌ ์ž‘์„ฑํ•ด๋‘์—ˆ๋‹ค. (์ž์ฃผ ์‚ฌ์šฉ๋  ์˜ˆ์ •์ด๋ผ ์ œ์ผ ์ƒ๋‹จ์œผ๋กœ ๋นผ๋‘์—ˆ๋‹ค.)

 


 

๐ŸŒฑ ๊ตฌํ˜„ํ•˜๊ธฐ - ์„œ๋น„์Šค ๋กœ์ง

๋จผ์ €, ๊ณ„์ขŒ ์ •๋ณด๋ฅผ ์ €์žฅํ•˜๊ธฐ ์œ„ํ•ด ๊ฐ„๋‹จํ•œ ๋„๋ฉ”์ธ ๊ฐ์ฒด๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž.

@Entity
class Account(

    @Id
    val id: Long = 0L
) {
    var balance: Int = 0

    fun addBalance(amount: Int) {
        balance += amount
    }

    fun subtractBalance(amount: Int) {
        balance -= amount
    }
}

๊ณ„์ขŒ์˜ ์ž”์•ก์„ ์ถ”๊ฐ€ํ•ด์ฃผ๋Š” ๋ฉ”์„œ๋“œ์™€, ์ œ๊ฑฐํ•ด์ฃผ๋Š” ๋ฉ”์„œ๋“œ 2๊ฐ€์ง€๋ฅผ ๋งŒ๋“ค์—ˆ๋‹ค.

 

import org.springframework.data.jpa.repository.JpaRepository

fun AccountRepository.getAccountById(id: Long): Account {
    return findAccountById(id) ?: throw IllegalArgumentException("์กด์žฌํ•˜์ง€ ์•Š๋Š” ๊ณ„์ขŒ์ž…๋‹ˆ๋‹ค")
}

interface AccountRepository : JpaRepository<Account, Long> {
    fun findAccountById(id: Long): Account?
}

๊ทธ๋ฆฌ๊ณ  ๊ฐ„๋‹จํ•˜๊ฒŒ ์•„์ด๋””๋ฅผ ํ†ตํ•ด ๊ณ„์ขŒ๋ฅผ ์กฐํšŒํ•ด์ฃผ๋Š” ํ™•์žฅ ํ•จ์ˆ˜๋ฅผ ๋งŒ๋“ค์—ˆ๋‹ค.

๊ฐœ์ธ์ ์œผ๋กœ ํ™•์žฅ ํ•จ์ˆ˜๋ฅผ ํ™œ์šฉํ•ด์„œ ์œ„์™€ ๊ฐ™์ด ์˜ˆ์™ธ๋ฅผ ํ„ฐํŠธ๋ฆฌ๋Š” ๋ฐฉ์‹์„ ์„ ํ˜ธํ•˜๋Š”๋ฐ, ์–ด๋–ค ์•„ํ‚คํ…์ฒ˜๋ฅผ ์‚ฌ์šฉํ•˜๋Š”์ง€์— ๋”ฐ๋ผ์„œ ์˜ˆ์™ธ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ค๋Š” ๋ถ€๋ถ„๋„ ๋‹ฌ๋ผ์งˆ ๊ฒƒ ๊ฐ™๋‹ค. (์š”์ฆ˜์€ ์ƒ๊ฐ์ด ๋ฐ”๋€Œ์–ด์„œ, ์•„๋งˆ ๋‹ค๋ฅด๊ฒŒ ๊ตฌํ˜„ํ•˜์ง€ ์•Š์„๊นŒ ์‹ถ๋‹ค)

 

interface KeyGenerator {
    fun generateAccountKey(accountId: Long): String
}

@Component
class KeyGeneratorImpl : KeyGenerator {
    override fun generateAccountKey(accountId: Long): String {
        return "$accountId:account"
    }
}

๋‹ค์Œ์œผ๋กœ, ๋ ˆ๋””์Šค์˜ ํ‚ค๋ฅผ ์ƒ์„ฑํ•ด์ฃผ๋Š” ๋ฉ”์„œ๋“œ๋ฅผ ์ถ”๊ฐ€ํ•ด์ฃผ์—ˆ๋‹ค.

ํ‚ค๋Š” ๊ฒน์น˜์ง€ ์•Š๋Š” ๊ฒƒ์ด ์ œ์ผ ์ค‘์š”ํ•œ๋ฐ, ์—ฌ๊ธฐ์„œ๋Š” ๊ณ„์ขŒ์— ๋Œ€ํ•œ ๊ณ ์œ  ์•„์ด๋”” (PK) ๊ฐ’๊ณผ 'account'๋ฅผ ์•Œ๋ ค์ฃผ๋Š” String์„ ์ถ”๊ฐ€ํ•ด์„œ ๋งŒ๋“ค์–ด์ฃผ์—ˆ๋‹ค. ์‹ค์ œ๋กœ๋Š” ์–ด๋–ค ๊ธฐ๋Šฅ์„ ๊ฐœ๋ฐœํ• ์ง€์— ๋”ฐ๋ผ์„œ ํ‚ค๋ฅผ ๋งŒ๋“œ๋Š” ๋ฐฉ๋ฒ•๋„ ๋‹ค์–‘ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์œ ์˜ํ•ด์•ผ ๋œ๋‹ค.

 

@RestController
@RequestMapping("/accounts")
class AccountController(
    private val accountService: AccountService
) {

    @PostMapping("/create")
    fun createAccount(): ResponseEntity<AccountResponse> {
        val result = accountService.create()
        return ResponseEntity.ok(result)
    }

    @PostMapping("/deposit")
    fun deposit(@RequestBody request: DepositRequest): ResponseEntity<BalanceResponse> {
        val result = accountService.deposit(request)
        return ResponseEntity.ok(result)
    }

    @GetMapping("/balance/{accountId}")
    fun getBalance(@PathVariable accountId: Long): ResponseEntity<BalanceResponse> {
        val result = accountService.getBalance(accountId)
        return ResponseEntity.ok(result)
    }

    @PostMapping("/withdraw")
    fun withdraw(@RequestBody request: WithdrawRequest): ResponseEntity<BalanceResponse> {
        val result = accountService.withdraw(request)
        return ResponseEntity.ok(result)
    }
}

data class DepositRequest(
    val accountId: Long,
    val amount: Int
)

data class WithdrawRequest(
    val accountId: Long,
    val amount: Int
)

data class AccountResponse(
    val accountId: Long
)

data class BalanceResponse(
    val accountId: Long,
    val balance: Int
)
class AccountService(
    private val accountRepository: AccountRepository,
    private val keyGenerator: KeyGenerator,
    private val cacheService: CacheService
) {

    @Transactional
    fun create(): AccountResponse {
        val account = Account()
        val savedAccount = accountRepository.save(account)
        return AccountResponse(savedAccount.id)
    }

    @Transactional
    fun deposit(request: DepositRequest): BalanceResponse {
        val accountId = request.accountId
        val account = accountRepository.getAccountById(accountId)
        account.addBalance(request.amount)
        return BalanceResponse(account.id, account.balance)
    }

    @Transactional(readOnly = true)
    fun getBalance(accountId: Long): BalanceResponse {
        val account = accountRepository.getAccountById(accountId)
        return BalanceResponse(account.id, account.balance)
    }

    @Transactional
    fun withdraw(request: WithdrawRequest): BalanceResponse {
        val accountId = request.accountId
        val account = accountRepository.getAccountById(accountId)
        account.subtractBalance(request.amount)
        return BalanceResponse(account.id, account.balance)
    }
}

๊ทธ๋ ‡๋‹ค๋ฉด, ์‹ค์ œ API ํ˜ธ์ถœ์„ ์œ„ํ•ด ๊ฐ„๋‹จํ•œ ์ปจํŠธ๋กค๋Ÿฌ + ์„œ๋น„์Šค ์ฝ”๋“œ๋ฅผ ๋งŒ๋“ค์–ด์ฃผ์—ˆ๋‹ค.

๊ณ„์ขŒ๋ฅผ ์ƒ์„ฑํ•˜๊ณ , ์ž…๊ธˆํ•˜๊ณ , ์ถœ๊ธˆํ•˜๊ณ , ์กฐํšŒํ•˜๋Š” ๊ธฐ๋Šฅ์ด๋‹ค. 

์ด ๊ธฐ๋ณธ ์ฝ”๋“œ๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ๋ถ„์‚ฐ๋ฝ ์—†์ด ํ•œ ๋ฒˆ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด๋ณด์ž.

 


 

โœ” Case 1 - ์•„๋ฌด ๋ฝ๋„ ๊ฑธ์ง€ ์•Š์•˜์„ ๋•Œ 

class AccountControllerTest : IntegrationTestHelper() {

    @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)
            .isNotEqualTo(60)
    }

    private fun ๊ณ„์ขŒ_์ƒ์„ฑ(): Long {
        return RestAssured.given()
            .log().all()
            .contentType(MediaType.APPLICATION_JSON_VALUE)
            .`when`()
            .post("/accounts/create")
            .then()
            .log().all()
            .extract()
            .`as`(object : TypeRef<AccountResponse>() {})
            .accountId
    }

    private fun ์ดˆ๊ธฐ_์ž”์•ก_์„ค์ •(accountId: Long, amount: Int): BalanceResponse {
        val depositRequest = DepositRequest(accountId, amount)
        return RestAssured.given()
            .log().all()
            .contentType(MediaType.APPLICATION_JSON_VALUE)
            .`when`()
            .body(depositRequest)
            .post("/accounts/deposit")
            .then()
            .log().all()
            .extract()
            .`as`(object : TypeRef<BalanceResponse>() {})
    }

    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")
            .then()
            .log().all()
            .extract()
            .`as`(object : TypeRef<BalanceResponse>() {})
    }

    private fun ์ž”์•ก_์กฐํšŒ(accountId: Long): BalanceResponse {
        return RestAssured.given()
            .log().all()
            .`when`()
            .get("/accounts/balance/$accountId")
            .then()
            .log().all()
            .extract()
            .`as`(object : TypeRef<BalanceResponse>() {})
    }
}

์šฐ์„ , ์•„๋ฌด ๋ฝ๋„ ๊ฑธ์ง€ ์•Š์€ ์ƒํƒœ๋กœ ์š”์ฒญ์„ ํ•œ๋‹ค๋ฉด ์–ด๋–ป๊ฒŒ ๋ ๊นŒ? ์œ„ ํ…Œ์ŠคํŠธ ์‹œ๋‚˜๋ฆฌ์˜ค๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

1. ์ดˆ๊ธฐ ์ž”์•ก์„ 100์œผ๋กœ ์„ธํŒ…ํ•œ๋‹ค.
2. 2๊ฐœ์˜ ์š”์ฒญ์ด ๋™์‹œ์— 20์”ฉ ์ถœ๊ธˆ ์š”์ฒญ์„ ์ง„ํ–‰ํ•œ๋‹ค.
3. ์ž”์•ก์„ ์กฐํšŒํ–ˆ์„ ๋•Œ 100 - 20 - 20 = 60์ด ๋‚˜์˜ค๊ธฐ๋ฅผ ๊ธฐ๋Œ€ํ•œ๋‹ค.

2๊ฐœ์˜ ์š”์ฒญ์ด ๋™์‹œ์— ์˜ค๊ณ  ์žˆ์œผ๋ฉฐ, ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๊ฐ€ ๋จผ์ € ๋๋‚˜๋Š” ๊ฒƒ์„ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ๋ชจ๋“  ์š”์ฒญ์ด ์ข…๋ฃŒ๋  ๋•Œ๊นŒ์ง€ await()์œผ๋กœ ๋Œ€๊ธฐํ•  ์ˆ˜ ์žˆ๋„๋ก ๋งŒ๋“ค์—ˆ๋‹ค. ํ•˜์ง€๋งŒ, ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ๋ณด๋ฉด isNotEqualTo(60)์œผ๋กœ, 60์ด๋ผ๋Š” ๊ฐ’์ด ๋‚˜์˜ค์ง€ ์•Š์€ ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ๋‹ค.

์ด๋Š”, ๋™์‹œ์„ฑ ๋ฌธ์ œ๋กœ ์ธํ•ด์„œ '20 ์ถœ๊ธˆํ•˜๋ผ'๋ผ๋Š” ์š”์ฒญ์ด ๋™์‹œ์— ์˜ค๋ฉด์„œ ์ฒซ ๋ฒˆ์งธ ์š”์ฒญ, ๋‘ ๋ฒˆ์งธ ์š”์ฒญ ๋ชจ๋‘ 100 -> 80์œผ๋กœ ์ถœ๊ธˆ์„ ์ง„ํ–‰ํ–ˆ๊ธฐ ๋•Œ๋ฌธ์— ๋ฐœ์ƒํ•œ ๋ฌธ์ œ์˜€๋‹ค.

 


 

โœ” Case 2 - ๋ ˆ๋””์Šค์˜ get - set ํ™œ์šฉํ•˜๊ธฐ

๊ทธ๋ ‡๋‹ค๋ฉด, ๋ ˆ๋””์Šค๋ฅผ ํ™œ์šฉํ•ด ๋ณด๋ฉด ์–ด๋–จ๊นŒ?

์šฐ๋ฆฌ๋Š” ์ดˆ๊ธฐ์— ๊ตฌํ˜„ ์‹œ๋‚˜๋ฆฌ์˜ค์—์„œ 'ํ‚ค๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์„ ๊ฒฝ์šฐ ๋ฝ์„ ํš๋“ํ•˜๊ณ , ์กด์žฌํ•˜๋ฉด ๋ฝ์„ ํš๋“ํ•  ๋•Œ๊นŒ์ง€ ๋Œ€๊ธฐ'ํ•˜๋„๋ก ํ”Œ๋กœ์šฐ๋ฅผ ๊ตฌ์„ฑํ•˜๊ณ ์ž ํ–ˆ๋‹ค. ๊ฐ„๋‹จํ•˜๊ฒŒ ๋ ˆ๋””์Šค๋ฅผ ํ™œ์šฉํ•ด์„œ ํ‚ค๊ฐ€ ์กด์žฌํ•˜๋Š”์ง€ ํ™•์ธํ•˜๊ธฐ ์œ„ํ•ด get์œผ๋กœ ๊ฐ€์ ธ์˜ค๊ณ , ์กด์žฌํ•˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ์— set์„ ํ†ตํ•ด ๋ฝ์„ ํš๋“ํ•˜๋Š” ๋กœ์ง์„ ์ž‘์„ฑํ•ด๋ณด์ž.

interface KeyGenerator {
    fun generateAccountKey(accountId: Long): String
}

@Component
class KeyGeneratorImpl : KeyGenerator {
    override fun generateAccountKey(accountId: Long): String {
        return "$accountId:account"
    }
}

๋จผ์ €, ๋ ˆ๋””์Šค์˜ ํ‚ค๋ฅผ ์ƒ์„ฑํ•ด์ฃผ๋Š” ๋ฉ”์„œ๋“œ๋ฅผ ์ถ”๊ฐ€ํ•ด์ฃผ์—ˆ๋‹ค.    
ํ‚ค๋Š” ๊ฒน์น˜์ง€ ์•Š๋Š” ๊ฒƒ์ด ์ œ์ผ ์ค‘์š”ํ•œ๋ฐ, ์—ฌ๊ธฐ์„œ๋Š” ๊ณ„์ขŒ์— ๋Œ€ํ•œ ๊ณ ์œ  ์•„์ด๋”” (PK) ๊ฐ’๊ณผ 'account'๋ฅผ ์•Œ๋ ค์ฃผ๋Š” String์„ ์ถ”๊ฐ€ํ•ด์„œ ๋งŒ๋“ค์–ด์ฃผ์—ˆ๋‹ค. ์‹ค์ œ๋กœ๋Š” ์–ด๋–ค ๊ธฐ๋Šฅ์„ ๊ฐœ๋ฐœํ• ์ง€์— ๋”ฐ๋ผ์„œ ํ‚ค๋ฅผ ๋งŒ๋“œ๋Š” ๋ฐฉ๋ฒ•๋„ ๋‹ค์–‘ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์œ ์˜ํ•ด์•ผ ๋œ๋‹ค.

@Component
class RedisService(
    private val redisTemplate: RedisTemplate<String, String>
) : CacheService {
    override fun get(key: String): String? {
        return redisTemplate.opsForValue().get(key)
    }

    override fun set(key: String, value: String, duration: Duration) {
        redisTemplate.opsForValue().set(key, value, duration)
    }
}

๋‹ค์Œ์œผ๋กœ, redisTemplate์„ ํ™œ์šฉํ•˜์—ฌ String ๊ฐ’์„ ์กฐํšŒํ•˜๊ณ  ์„ค์ •ํ•˜๋Š” ํ•จ์ˆ˜๋ฅผ ์ถ”๊ฐ€ํ•˜์˜€๋‹ค.

๋‘˜ ๋ชจ๋‘ opsForValues().set()๊ณผ opsForValues().get()์„ ํ™œ์šฉํ•˜๋ฉด ๊ฐ„๋‹จํ•˜๊ฒŒ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋‹ค.

set์˜ ๊ฒฝ์šฐ ํ•ด๋‹น ํ‚ค๊ฐ€ ์–ธ์ œ๊นŒ์ง€ ์œ ํšจํ•˜๋„๋ก ๋งŒ๋“ค์ง€ TTL ๊ฐ’์„ ์„ค์ •ํ•ด์ค„ ์ˆ˜ ์žˆ๋Š”๋ฐ, Duration์„ ์‚ฌ์šฉํ•˜์—ฌ ์›ํ•˜๋Š” ๋งŒํผ ์ง€์ •ํ•ด์ค„ ์ˆ˜ ์žˆ๋‹ค. ๋ณดํ†ต ์˜ค๋ž˜ ๊ฑธ๋ฆฌ๋Š” ๋กœ์ง์ด ์•„๋‹ˆ๋ผ๋ฉด 30์ดˆ ~ 1๋ถ„ ์ •๋„๋กœ ์„ค์ •ํ•˜๋Š” ๊ฒฝ์šฐ๋ฅผ ๋งŽ์ด ๋ณธ ๊ฒƒ ๊ฐ™๋‹ค.

 

@PostMapping("/withdraw-non-atomic-lock")
fun withdrawUsingNonAtomicLock(@RequestBody request: WithdrawRequest): ResponseEntity<BalanceResponse> {
    val result = accountService.withdrawUsingNonAtomicLock(request)
    return ResponseEntity.ok(result)
}
@Transactional
fun withdrawUsingNonAtomicLock(request: WithdrawRequest): BalanceResponse? {
    val accountId = request.accountId
    val accountKey = keyGenerator.generateAccountKey(accountId)

    while (true) {
        if (cacheService.get(accountKey) == null) {
            cacheService.set(accountKey, "account-withdraw", Duration.ofSeconds(5))
            break;
        }
        Thread.sleep(1000)
    }

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

    // ์บ์‹œ ์ œ๊ฑฐ - TTL์„ ์งง๊ฒŒ ์žก๊ธฐ๋Š” ํ–ˆ์ง€๋งŒ ๋ช…์‹œ์ ์œผ๋กœ ์ œ๊ฑฐํ•จ
    cacheService.delete(accountKey)
    return BalanceResponse(account.id, account.balance)
}

๊ทธ๋ฆฌ๊ณ , ์‹ค์งˆ์ ์ธ ๋น„์ฆˆ๋‹ˆ์Šค ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด์ฃผ์—ˆ๋‹ค.

๋จผ์ €, get()์„ ํ†ตํ•ด์„œ ํ‚ค๊ฐ€ ์กด์žฌํ•˜๋Š”์ง€ ํ™•์ธํ•œ ๋‹ค์Œ, ์กด์žฌํ•˜์ง€ ์•Š๋Š”๋‹ค๋ฉด set์„ ํ•ด์ฃผ๊ณ , ์กด์žฌํ•˜๋Š” ๊ฒฝ์šฐ 1์ดˆ์”ฉ ๋Œ€๊ธฐํ•˜๋ฉฐ ์žฌ์‹œ๋„๋ฅผ ์ง„ํ–‰ํ•œ๋‹ค.

์ฐธ๊ณ ๋กœ, set์„ ํ†ตํ•ด ์„ธํŒ…ํ•ด์ฃผ๋Š” value ๊ฐ’์€ ์ž์œ ๋กญ๊ฒŒ ์„ค์ •ํ•ด์ฃผ์–ด๋„ ๋œ๋‹ค. (์˜๋ฏธ์—†๋Š” String ๊ฐ’๋„ ์ƒ๊ด€์—†๋‹ค.) 

์ค‘์š”ํ•œ ๊ฑด, ๋‹ค๋ฅธ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง๊ณผ ํ‚ค๋งŒ ๊ฒน์น˜์ง€ ์•Š๋„๋ก ๋งŒ๋“ค์–ด ์ฃผ๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•˜๋‹ค.

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

 

์šฐ๋ฆฌ๊ฐ€ ์›ํ•˜๋Š” ๋กœ์ง๋Œ€๋กœ ์ž‘์„ฑํ–ˆ๊ธฐ ๋•Œ๋ฌธ์—, ์ด์ œ ๊ฒฐ๊ณผ๊ฐ’์œผ๋กœ 60์ด ๋‚˜์˜ค์ง€ ์•Š์„๊นŒ?

@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)
        .isNotEqualTo(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-non-atomic-lock")
        .then()
        .log().all()
        .extract()
        .`as`(object : TypeRef<BalanceResponse>() {})
}

ํ•˜์ง€๋งŒ, ์˜ˆ์ƒ๊ณผ ๋‹ค๋ฅด๊ฒŒ ์—ฌ์ „ํžˆ isNotEqualTo()๋ฅผ ํ†ตํ•ด 60์ด ๋‚˜์˜ค์ง€ ์•Š์€ ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ๋‹ค. ์™œ ์ด๋Ÿด๊นŒ?

์‚ฌ์‹ค, ์•ž์—์„œ ์ด๋ฏธ ๋‹ต์„ ๋ƒˆ๋‹ค. 

์šฐ๋ฆฌ์˜ ์Šคํ”„๋ง ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์€ ๋ฉ€ํ‹ฐ ์Šค๋ ˆ๋“œ ๊ธฐ๋ฐ˜์ด๊ธฐ ๋•Œ๋ฌธ์—, ๋ ˆ๋””์Šค๊ฐ€ ์‹ฑ๊ธ€ ์Šค๋ ˆ๋“œ๋กœ ๋™์ž‘ํ•œ๋‹ค๊ณ  ํ•ด๋„ ํ•˜๋‚˜์˜ ์Šค๋ ˆ๋“œ๊ฐ€ get - set์„ ํ•˜๋Š” ์ค‘๊ฐ„์— ๋‹ค๋ฅธ ์Šค๋ ˆ๋“œ๊ฐ€ get์„ ์ถฉ๋ถ„ํžˆ ํ•  ์ˆ˜ ์žˆ๋Š” ์ƒํ™ฉ์ด ๋งŒ๋“ค์–ด์กŒ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

 

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

๊ทธ๋ ‡๋‹ค๋ฉด, GET - SET์„ ๋™์‹œ์— ํ•˜๋„๋ก = ์›์ž์„ฑ์„ ๋ณด์žฅํ•˜๋„๋ก ๋งŒ๋“ค ์ˆ˜ ์—†์„๊นŒ?

๋ฐ”๋กœ, ์•ž์„œ ๋งํ–ˆ๋˜ ๊ฒƒ์ฒ˜๋Ÿผ NX์„ ์‚ฌ์šฉํ•˜๋ฉด ๋œ๋‹ค.

 


 

โœ” Case 3 - ๋ ˆ๋””์Šค์˜ NX ํ™œ์šฉํ•˜๊ธฐ

override fun setNX(key: String, value: String, duration: Duration): Boolean {
    return redisTemplate.opsForValue().setIfAbsent(key, value, duration) ?: false
}

NX์˜ ๊ฒฝ์šฐ redisTemplate์—์„œ ์ œ๊ณตํ•˜๋Š” setIfAbsent()๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋œ๋‹ค.

์šฐ๋ฆฌ๊ฐ€ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋ ˆ๋ฒจ์—์„œ GET์„ ํ†ตํ•ด ํ‚ค๊ฐ€ ์กด์žฌํ•˜๋Š”์ง€ ํ™•์ธํ•˜๊ณ , SET์„ ํ†ตํ•ด์„œ ๊ฐ’์„ ์„ธํŒ…ํ•ด์ฃผ๋Š” ๊ณผ์ •์„ ๋ ˆ๋””์Šค ๋‚ด์—์„œ ๋งŒ๋“  ๋ช…๋ น์–ด๋ผ๊ณ  ์ƒ๊ฐํ•˜๋ฉด ๋œ๋‹ค. ๋งŒ์•ฝ ํ‚ค๊ฐ€ ์ด๋ฏธ ์กด์žฌํ•ด์„œ ๊ฐ’์„ ์„ธํŒ…ํ•˜์ง€ ๋ชปํ–ˆ๋‹ค๋ฉด false๋ฅผ, ํ‚ค๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์•„์„œ ๊ฐ’์„ ์„ธํŒ…ํ–ˆ๋‹ค๋ฉด true๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.

 

@PostMapping("/withdraw-atomic-lock")
fun withdrawUsingAtomicLock(@RequestBody request: WithdrawRequest): ResponseEntity<BalanceResponse> {
    val result = accountService.withdrawUsingAtomicLock(request)
    return ResponseEntity.ok(result)
}
@Transactional
fun withdrawUsingAtomicLock(request: WithdrawRequest): BalanceResponse {
    val accountId = request.accountId
    val accountKey = keyGenerator.generateAccountKey(accountId)

    while (!cacheService.setNX(accountKey, "account-withdraw", Duration.ofSeconds(5))) {
        Thread.sleep(1000)
    }

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

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

์‹ค์ œ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์— ์ ์šฉํ•ด๋ณด์ž. setNX๋ฅผ ํ†ตํ•ด true, false ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๊ฐ’์„ ์„ธํŒ…ํ•˜์ง€ ๋ชปํ–ˆ์„ ๊ฒฝ์šฐ 1์ดˆ์”ฉ ๋Œ€๊ธฐํ•˜๋„๋ก while๋ฌธ์„ ํ†ตํ•ด ์‰ฝ๊ฒŒ ์ œ์–ดํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋˜์—ˆ๋‹ค.

 

๊ทธ๋ ‡๋‹ค๋ฉด, ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ํ†ตํ•ด ๊ฒฐ๊ณผ๋ฅผ ํ™•์ธํ•ด๋ณด์ž.

@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-atomic-lock")
        .then()
        .log().all()
        .extract()
        .`as`(object : TypeRef<BalanceResponse>() {})
}

๋“œ๋””์–ด ์šฐ๋ฆฌ๊ฐ€ ์›ํ•˜๋Š”๋Œ€๋กœ 60์„ ๋ฐ˜ํ™˜ํ•˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค!

ํ•˜์ง€๋งŒ ์ด๋Œ€๋กœ ๋๋‚ด๊ธฐ์—๋Š” ๋ญ”๊ฐ€ ์•„์‰ฝ๊ธฐ ๋•Œ๋ฌธ์—, ์กฐ๊ธˆ ๋” ๊ท€์ฐฎ์€ ๋ฐฉ๋ฒ•์œผ๋กœ ์›์ž์„ฑ์„ ๋ณด์žฅํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์†Œ๊ฐœํ•ด๋ณด๊ณ ์ž ํ•œ๋‹ค.

 


 

๊ธ€์ด ์ƒ๊ฐ๋ณด๋‹ค ๋„ˆ๋ฌด ๊ธธ์–ด์ ธ์„œ 1, 2๋ถ€๋กœ ๋‚˜๋ˆ„์–ด์„œ ์ž‘์„ฑํ•˜์˜€์Šต๋‹ˆ๋‹ค.

๋‹ค์Œ ํฌ์ŠคํŒ…: https://cl8d.tistory.com/125

Comments