하나의 프로세스에 있는 자원을 여러개의 스레드가 공유하며 작업을 나누어 처리
간단한 입금 처리를 위해 공유 자원을 여러개의 스레드가 사용 할 때의 문제점을 알아봅시다.
1.공유 자원인 계좌 잔액이 10만원 이라고 가정한다
2.10개의 쓰레드에서 동시에 5만원씩 입금한다
예상 되는 최종 잔액은 60만원인데 실제 멀티 스레드에서는 다른 결과값이 나오게 됩니다. 위 시나리오 대로 코드를 작성해봅시다.
@SpringBootTest
class AccountApplicationTests {
private val log = LoggerFactory.getLogger(this.javaClass)!!
var amount = 100000
@Test
fun test() {
// ThreadPool 구성
val executor = Executors.newFixedThreadPool(10)
// 작업이 완료될 때 가지 기다릴 수 있도록 설정
val countDownLatch = CountDownLatch(10)
for (i in 1..10) {
executor.submit { execute(countDownLatch) }
}
countDownLatch.await()
log.info("result : ${this.amount}")
assertEquals(600000, this.amount)
}
fun execute(countDownLatch: CountDownLatch) {
try {
this.amount += 50000
log.info("amount : ${this.amount}")
} finally {
countDownLatch.countDown()
}
}
}
결과

@SpringBootTest
class AccountApplicationTests {
private val log = LoggerFactory.getLogger(this.javaClass)!!
var amount = 100000
@Test
fun test() {
// ThreadPool 구성
val executor = Executors.newFixedThreadPool(10)
// 작업이 완료될 때 가지 기다릴 수 있도록 설정
val countDownLatch = CountDownLatch(10)
for (i in 1..10) {
executor.submit { execute(countDownLatch) }
}
countDownLatch.await()
log.info("result : ${this.amount}")
assertEquals(600000, this.amount)
}
fun execute(countDownLatch: CountDownLatch) {
synchronized(this) {
try {
this.amount += 50000
log.info("amount : ${this.amount}")
} finally {
countDownLatch.countDown()
}
}
}
}
결과

java의 Sychronized는 하나의 프로세스 안에서만 보장이 되어서 여러개의 서버가 존재할 때는 동시성 보장이 안됩니다.
Redis란 key-value 구조의 비정형 데이터를 저장하고 관리하기 위한 오픈 소스 기반의 비 관계형 인메모리 DBMS
java 의 redisClient 는 아래 3가지가 존재하며
해당 테스트는 Redisson 방식을 사용합니다.
채널을 만들고, 락을 점유 중인 스레드가, Lock을 해제했음을, 대기 중인 스레드에게 알려주면 대기 중인 스레드가 Lock 점유를 시도하는 방식
테스트에 필요한 Redis를 실행해줍니다.
docker-compose.yml Redis 파일 작성 후 실행(docker-compose up -d --build)
version: '3.7'
services:
redis:
image: redis:alpine
command: redis-server --port 6379
container_name: redis
hostname: redis_boot
labels:
- "name=redis"
- "mode=standalone"
ports:
- 6379:6379
gradle 의존성 추가
dependencies {
implementation("org.redisson:redisson:3.17.6")
}
Redis Config
@Configuration
class RedisConfig {
@Bean(destroyMethod = "shutdown")
fun redissonClient(): RedissonClient {
val config = Config()
config.useSingleServer().address = "redis://localhost:6379"
return Redisson.create(config)
}
}
Redis Test Service
interface RedissonTestService {
fun diposit(id: Long, amount: Long)
}
@Service
class RedissonTestServiceImpl(
private val redissonClient: RedissonClient,
private val accountService: AccountService
): RedissonTestService {
override fun diposit(id: Long, amount: Long) {
// 식별자를 키로 갖는 lock 정의
val lock = redissonClient.getLock(id.toString())
try {
// 락 획득 시도 3초동안 획득 시도, 3초간 락상태
val available = lock.tryLock(3, 3, TimeUnit.SECONDS)
if (!available) {
println("lock 획득 실패")
return
}
val account = accountService.deposit(id, amount)
println("update amount : ${account.amount}")
} catch (e: Exception) {
e.printStackTrace()
} finally {
// 작업이 끝나면 락 해제
lock.unlock()
}
}
}
Account Service
interface AccountService {
fun deposit(id: Long, amount: Long): Account
}
@Service
class AccountServiceImpl(
private val accountRepository: AccountRepository
): AccountService {
override fun deposit(id: Long, amount: Long): Account {
val account = accountRepository.findById(id).orElseThrow()
account.update(amount)
accountRepository.saveAndFlush(account)
return account
}
}
Test Code
@SpringBootTest
class AccountApplicationTests {
private val log = LoggerFactory.getLogger(this.javaClass)!!
@Autowired
lateinit var accountRepository: AccountRepository
@Autowired
lateinit var redissonTestService: RedissonTestService
@Test
fun redisTest() {
// ThreadPool 구성
val executor = Executors.newFixedThreadPool(10)
// 작업이 완료될 때 가지 기다릴 수 있도록 설정
val countDownLatch = CountDownLatch(10)
val testAccount = Account(amount = 100000)
val entity = accountRepository.saveAndFlush(testAccount)
log.info("init amount : ${entity.amount}")
for (i in 1..10) {
executor.submit { entity.id?.let { execute(it, countDownLatch) } }
}
countDownLatch.await()
val result = entity.id?.let { accountRepository.findById(it).orElseThrow() }
log.info("result : ${result?.amount}")
assertEquals(600000, result?.amount)
}
fun execute(id: Long, countDownLatch: CountDownLatch) {
try {
redissonTestService.diposit(id, 50000)
} finally {
countDownLatch.countDown()
}
}
}
결과

간단한 테스트로 RedisClient 락을 생성하고 해당 대기시간, 유지 시간 설정으로 동시성 문제를 해결해보았습니다.