안녕하세요, 오늘은 최종프로젝트를 진행하면서 동시성제어에 대해 학습한 내용을 정리해보았습니다.
분산 락 (Distributed Lock)

즉, 공유자원 자체에 락을 거는 것이 아니라, 어떤 행위가 발생하는 임계구역(Critical Section)에 락을 거는 방식이고 주로 Redis를 활용하기에 그냥 분산 락 → Redis 쓰겠다 라고 생각해도 무방합니다.
(물론 분산 락을 MySQL로도 구현이 가능하나 Redis에 비해 성능이 크게 떨어집니다.)
번외) RedisTemplate
분산 락 예제 (Redisson)
build.gradle
// Redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.redisson:redisson-spring-boot-starter:3.27.1'
Counter Service
package org.example.concurrency;
import lombok.RequiredArgsConstructor;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.concurrent.TimeUnit;
@Service
@RequiredArgsConstructor
public class CounterService {
private final CounterRepository counterRepository;
private final RedissonClient redissonClient;
private static final String LOCK_KEY = "counterLock";
@Transactional
public void save(Counter counter) {
counterRepository.save(counter);
}
@Transactional
public void reset() {
Counter counter = counterRepository.findById(1L).orElseThrow();
counter.setCount(100);
counterRepository.save(counter);
}
@Transactional
public void decreaseCount() {
Counter counter = counterRepository.findById(1L).orElseThrow();
counter.setCount(counter.getCount() - 1);
counterRepository.save(counter);
}
public void decreaseCountUsingLock() {
RLock lock = redissonClient.getFairLock(LOCK_KEY);
try {
boolean isLocked = lock.tryLock(10, 60, TimeUnit.SECONDS);
if (isLocked) {
try {
Counter counter = counterRepository.findById(1L).orElseThrow();
counter.setCount(counter.getCount() - 1);
counterRepository.save(counter);
} finally {
lock.unlock();
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
public void printCount() {
System.out.println("\n\n\n====================\n");
System.out.println(counterRepository.findById(1L).orElseThrow().getCount());
System.out.println("\n====================\n\n\n");
}
}
Redis Config
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
class RedisConfig {
@Value("${spring.data.redis.host}")
private String host;
@Value("${spring.data.redis.port}")
private int port;
@Value("${spring.data.redis.password}")
private String password;
private static final String REDISSON_PREFIX = "redis://";
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer()
.setAddress(REDISSON_PREFIX + host + ":" + port)
.setPassword(password);
return Redisson.create(config);
}
}
application.yml
spring:
data:
redis:
host: 127.0.0.1
port: 6379
password: 1234
Test
package org.example.concurrency;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.stream.IntStream;
@SpringBootTest
public class CounterServiceTest {
@Autowired
private CounterService counterService;
@BeforeEach
void setUp() {
counterService.save(new Counter(100));
}
@AfterEach
void tearDown() {
counterService.reset();
}
@Test
void concurrencyTest() {
IntStream.range(0, 100).parallel().forEach(i -> counterService.decreaseCount());
System.out.println("\n\n\n\n[concurrencyTest]");
counterService.printCount();
}
@Test
void concurrencyTestUsingLock() {
System.out.println("\n\n\n\n[concurrencyTestUsingLock]");
IntStream.range(0, 100).parallel().forEach(i -> counterService.decreaseCountUsingLock());
counterService.printCount();
}
}
Redis의 싱글 스레드 특성을 이용한 동시성 제어
public void initializeCounter() {
ValueOperations<String, String> ops = this.stringRedisTemplate.opsForValue();
ops.set("counter", "100");
}
public void decrementCounter() {
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
ops.decrement("counter");
}
public void getCounter() {
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
String value = ops.get("counter");
System.out.println(value);
}
@Test @DisplayName("Redis의 싱글 스레드 특성을 이용한 동시성 제어")
void concurrencyTestWithRedis() {
System.out.println("\n\n\n\n[concurrencyTestNoLockWithRedis]");
IntStream.range(0, 100).parallel().forEach(i -> counterService.decrementCounter());
counterService.getCounter();
}