저번 포스팅에서 해결한 데드락 문제에 이어서 이번에는 동시성 문제를 해결해보겠습니다.
1. 투표 참여 기능 설명
투표는 최대 1000명까지의 참여 인원 수를 설정할 수 있으며, 설정된 참여 인원 수가 모두 차면 투표는 즉시 종료됩니다. 회원은 투표 아이템 중 하나를 선택하여 투표에 참여할 수 있습니다.
투표 도메인은 votes(투표) 테이블과 voters(투표자) 테이블이 있습니다.

2. 문제 상황
투표 참여 기능의 성능 테스트 중 🚨 동시성 이슈가 발생했습니다.
테스트 조건
- 대상 투표 ID: 1번
- 설정된 제한 인원: 1000명
- 동시 참여 사용자 수: 1010명
테스트 결과
기대했던 1000명의 투표자 대신, 실제로는 1009명의 투표자가 등록되었습니다. 동시성 이슈가 발생한 것입니다. 😱

3. 동시성이 발생한 코드 분석 🧐
투표 참여 기능 흐름
- VoteService.participate()에서 투표가 진행 중인지 검사합니다.
- 투표가 아직 진행 중이라면, VoteManager.participate()를 호출합니다.
- VoteManager.participate()는 투표자를 생성하고, 투표 제한 인원 수에 도달하면 투표를 종료시킵니다.
VoteService 클래스
public void participate (
final Long voteId,
final Long itemId
) {
final Long memberId = memberUtils.getCurrentMemberId();
final Vote vote = voteReader.read(voteId);
if (!vote.isVoting()) { // 1. 투표가 진행 중인지 검사
throw new BusinessException(ErrorCode.VOTE_CANNOT_PARTICIPATE);
}
voteManager.participate(vote, memberId, itemId); // 2. VoteManager.participate() 호출
}
VoteManager 클래스
@Transactional
public void participate(
final Vote vote,
final Long memberId,
final Long itemId
) {
final Voter voter = new Voter(vote.getId(), memberId, itemId); // 3-1. 투표자 생성
voterRepository.save(voter);
final int participants = voterReader.count(vote.getId());
if (vote.reachMaximumParticipants(participants)) {
vote.close(LocalDateTime.now()); // 3-2. 투표 제한 인원 수에 도달하면 투표 종료
}
}
Vote 엔티티
@Entity
@Table(name = "votes")
public class Vote {
// 필요한 코드만 표시하였습니다.
public boolean reachMaximumParticipants(final int participants) {
return this.maximumParticipants <= participants;
}
public void close(final LocalDateTime now) {
this.endTime = now;
}
}
문제 발생
- 투표 종료 직전 투표자가 999명에 이르렀을 때, 여러 사용자가 동시에 투표 참여를 요청합니다.
- 이 요청들은 모두 VoteService에서 진행 중인 투표임을 확인하고, 문제없이 통과합니다.
- 검증을 통과한 후, 각 요청은 VoteManager의 participate() 메서드를 통해 처리되며, 이 과정에서 각 트랜잭션 별로 투표자가 생성됩니다.
- 결과적으로, 기대했던 1000명을 초과하는 투표자가 생성되는 동시성 문제가 발생합니다.

동시성 제어가 필요한 로직
동시성 제어 대상은 바로 VoteManager.paricipate() 전체입니다. 제 경우에는 insert 문에 대한 동시성 제어가 필요합니다. insert 문은 비교할 레코드가 없어 낙관적 락이나 비관적 락을 사용할 수 없습니다. 결과적으로, insert 문이 포함된 메서드 자체에 대해 동시성을 제어해야 합니다.
더 좋은 방법이 있다면 댓글로 알려주세요!
코드 로직 변경
동시성 문제를 해결하기 위해서, 먼저 투표 참여 로직을 수정할 필요가 있습니다.
VoteService에서 초기 검증을 마친 후, 이제 트랜잭션은 앞으로 적용될 락에 의해 VoteManger.participate() 메서드에 차례대로 접근하게 됩니다. 하지만 이 메서드에 접근하는 동안 새로운 투표자들이 생길 수 있습니다. 예를 들어, 투표자 수가 999명으로 아직 투표자 진행 중이라고 판단되어 초기 검증을 통과했지만, VoteManger.participate()에 접근하는 그 순간 사이에 새로운 투표자가 생겨 실제 투표자 수가 1000명을 초과할 수 있기 때문입니다. 따라서 새로운 투표자들에 대한 추가 검증이 이루어져야 합니다.
추가 검증을 위해 VoteManager의 participate() 메서드가 실행될 때, 투표 참여 인원 수를 확인하고, 이미 최대 인원 수에 도달했다면 비즈니스 예외를 발생시키도록 변경합니다. 또한, 새로운 투표자를 추가한 후의 총 투표자 수가 제한 인원을 초과했다면 즉시 투표를 종료하도록 합니다.
VoteManager 클래스
@Transactional
public void participate(
final Vote vote,
final Long memberId,
final Long itemId
) {
final int participants = voterReader.count(vote.getId());
if (vote.reachMaximumParticipants(participants)) { // 이미 최대 인원 수에 도달했다면 비즈니스 예외 발생
throw new BusinessException(ErrorCode.VOTE_CANNOT_PARTICIPATE);
}
final Voter voter = new Voter(vote.getId(), memberId, itemId);
voterRepository.save(voter);
if (vote.reachMaximumParticipants(participants + 1)) { // 새로운 투표자 추가 후의 총 투표자 수가 제한 인원 수를 초과했다면 투표 종료
vote.close(LocalDateTime.now());
}
}
4. 락을 통한 동시성 제어
이제 동시성을 제어해보겠습니다.
4-1. syncronized ❌
Synchronized 키워드는 Java에서 가장 간단하게 동시성을 제어할 수 있는 방법 중 하나입니다. 하지만, 여러 개의 투표를 동시에 관리하는 데에 한계가 있습니다.
동시 처리의 한계
예를 들어, ID가 1번인 투표와 ID가 2번인 투표가 동시에 진행되고 있다고 가정해봅시다. synchronized를 사용하면, 이 두 투표에 대한 참여 처리가 동시에 이루어질 수 없습니다. 왜냐하면 synchronized는 participate() 메서드(static 메서드 아님)에 대해 인스턴스 단위로 락을 걸기 때문입니다. 그리고 VoteManager은 단일 인스턴스(싱글톤 빈)로 존재하므로, 모든 요청은 순차적으로 처리될 수 밖에 없습니다. 이는 다른 투표 ID라 할지라도 동시 처리가 불가능하다는 것을 의미하며, 여러 사용자가 동시에 다른 투표에 참여하고자 할 때 성능 저하 및 사용자 경험 저하로 이어질 수 있습니다.
서버 간 동시성 해결 불가
또한, synchronized와 같은 애플리케이션 수준의 락은 단일 서버에서만 효과가 있습니다. 라임 서비스는 2대의 서버로 구성되어 있어서 한 서버에서의 락이 다른 서버에는 영향을 미치지 않아 동시성 문제가 여전히 발생할 수 있습니다. 따라서 synchronized로는 전체 시스템의 동시성을 제어할 수 없습니다.
4-2. ReentrantLock ❌
그 다음은 ReentrantLock과 ConcurrentHashMap을 활용한 방법입니다. 이 방법은 syncronized와 달리 ID별로 동시성 제어가 가능합니다. ConcurrentHashMap를 사용하여 투표 ID별로 독립적인 락을 관리하기 때문입니다.
@Component
@RequiredArgsConstructor
public class VoteLockManager {
private final VoteManager voteManager;
private final ConcurrentHashMap<String, Lock> lockMap = new ConcurrentHashMap<>();
public void excuteWithLock(
final Vote vote,
final Long memberId,
final Long itemId
) {
Lock lock = lockMap.computeIfAbsent(String.valueOf(vote.getId()), k -> new ReentrantLock());
try {
lock.lock();
voteManager.participate(vote, memberId, itemId);
} finally {
lock.unlock();
}
}
}
테스트 결과
동시성 문제가 발생하지 않았습니다. (야호~🥳)

서버 간 동시성 해결 불가
동시성 문제는 해결했지만, ReentrantLock을 최종적으로 채택하지 않았습니다. 이유는 syncronized와 같습니다. 라임 서비스는 2대의 서버로 운영되고 있는데, ReentrantLock은 단일 서버 환경에서만 동시성을 제어할 수 있기 때문입니다.
ReentrantLock & ConcurrentHashMap 사용 시 주의점
ReentrantLock과 ConcurrentHashMap을 이용하여 단일 서버 환경에서 동시성 문제를 해결하려 할 때, 주의해야 할 부분이 있습니다.
락 해제
락을 취득한 후 작업을 수행하는 과정에서 예외가 발생하더라도, 락이 안전하게 해제되도록 보장하기 위해서는 try-finally 블록을 사용해야 합니다.
ReentrantLock의 락을 적절히 해제하지 않으면, 해당 락에 대한 다른 스레드의 접근이 영원히 차단될 수 있습니다. 이는 데드락 상태로 이어질 수 있으며, 결국 서버의 메모리를 계속 점유하게 되어 메모리 누수 문제를 발생시킬 수 있습니다.
해시 충돌
HashMap은 키의 해시코드를 사용하여 데이터를 저장합니다. 이 과정에서 서로 다른 키가 같은 해시코드를 가지게 되면, 해시 충돌이 발생하여 같은 버킷 위치에 저장됩니다. 이러한 해시 충돌이 ReentrantLock 사용 시 락의 성능 저하로 이어질 수 있습니다.
예를 들어, ID가 1번과 3번인 두 투표가 있다고 가정해봅시다. 이 두 ID에 대한 해시코드가 우연히 같게 계산되어 해시 충돌이 발생한다면, HashMap 내부에서는 이 두 투표를 같은 버킷에 저장하게 됩니다. 만약 이 버킷에 대한 접근을 제어하기 위해 ReentrantLock을 사용한다면, ID 1번과 3번 투표에 대한 락이 동일하게 적용됩니다. 결과적으로, 이 두 투표의 처리가 서로 독립적으로 이루어져야 함에도 불구하고, 실제로는 동일한 락으로 인해 한 번에 하나의 처리만 이루어질 수 있게 됩니다.
4-3. 낙관적 락 & 비관적 락 ❌
낙관적 락과 비관적 락은 데이터베이스에서 자주 사용되는 두 가지 락 방식입니다. 하지만 제 경우, 이 두 방식을 사용할 수 없습니다. 저는 insert문에 대한 동시성 제어가 필요하기 때문입니다.
낙관적 락은 데이터베이스에 변경이 발생할 것으로 예상되지 않을 때 사용하는 방법입니다. 이 방식은 데이터를 읽을 때 락을 걸지 않고, 데이터를 실제로 변경할 때만 데이터가 변경되었는지를 확인합니다. 이 방법은 insert와 같이 새로운 데이터를 추가하는 상황에는 적합하지 않습니다.
비관적 락은 기존에 존재하는 레코드에 대한 변경이나 조회 시, 그 레코드를 락 걸어 다른 트랜잭션의 접근을 제한하는 방식입니다. 하지만 insert 작업은 새로운 레코드를 추가하는 것이므로, 락을 걸 '기준 레코드'가 존재하지 않습니다. 따라서 비관적 락을 사용하는 것이 불가능합니다.
결론적으로, insert 작업과 같이 새로운 데이터를 추가하는 경우에는 낙관적 락과 비관적 락 방식이 적합하지 않습니다.
4-4. MySQL의 네임드 락 ✅
네임드 락(Named Lock)은 GETLOCK() 함수를 이용해 임의의 문자열에 대해 잠금을 설정할 수 있습니다. 이 잠금의 특징은 대상이 테이블이나 레코드 또는 AUTO_INCREMENT와 같은 데이터베이스 객체가 아니라는 것입니다. 네임드 락은 단순히 사용자가 지정한 문자열에 대해 획득하고 반납(해제)하는 잠금입니다.
Real MySQL 8.0 (1권) 163 페이지
락 관련 함수
- GET_LOCK(str,timeout)
- str 이름의 락을 획득합니다. 한 세션이 보유하고 있는 동안 다른 세션은 동일한 이름의 잠금을 얻을 수 없습니다.
- timeout 값에 도달할 때까지 락을 얻기 위해 대기하며, 음수를 넣으면 무한 대기합니다.
- 잠금이 성공적으로 획득되면 1을 반환하고, 시도 시간이 초과되면 0을 반환하며, 오류가 발생하면 NULL을 반환합니다.
- RELEASE_LOCK(str)
- str 이름의 락을 해제합니다.
- 잠금이 해제되면 1을 반환하고, 이 스레드에 의해 잠금이 설정되지 않은 경우 0을 반환하며, 명명된 잠금이 존재하지 않으면 NULL을 반환합니다.
- 명시적으로 해제되거나 세션이 종료될 때 암시적으로 해제됩니다. 잠금은 트랜잭션이 커밋되거나 롤백될 때 해제되지 않습니다.
네임드 락 구현 시 주의점
네임드 락을 구현할 때 주의할 점이 있습니다.
1. 락의 트랜잭션 분리
네임드 락을 획득하고 해제하는 작업은 기존 로직의 트랜잭션과 분리되어야 합니다. 만약 같은 트랜잭션 범위 내에서 처리된다면, 데이터베이스에 커밋되기 전에 락이 해제될 수 있는 문제가 발생합니다. 이렇게 되면, 그 사이 새로운 트랜잭션이 락을 획득해 작업을 수행할 것이고 동시성를 해결할 수 없게 됩니다.
따라서, 락을 획득하고 해제하는 작업은 별도의 트랜잭션으로 처리해야 합니다. 이렇게 하면 데이터베이스에 커밋이 완료된 후에 락을 안전하게 해제할 수 있습니다.
2. 데이터소스 분리
같은 데이터소스를 사용한다면 같은 커넥션 풀을 사용합니다. 만약 락 관리를 위한 데이터 소스와 기존 로직을 수행하는 데이터소스가 같은 데이터소스를 사용한다면, 동시에 많은 요청이 들어올 경우 모든 커넥션을 락 획득에 사용하게 될 수 있습니다. 결국 커넥션 풀에 남은 커넥션이 없게 되어, 다른 DB 작업을 실행할 수 없게 됩니다.
따라서 락 관리와 기존 로직 작업을 위한 데이터소스는 서로 분리되어야 합니다.
3. 동일한 커넥션에서 락 획득과 해제
락을 획득하고 해제하는 과정은 반드시 같은 커넥션에서 이루어져야 합니다.
JDBC API 직접 구현
기존에 JPA를 사용 중이었기 때문에, 저는 세세한 커넥션 관리가 가능한 JDBC API를 직접 구현하기로 결정했습니다. 이를 통해 락 획득과 해제를 같은 커넥션에서 처리할 수 있습니다.
JDBC Template을 사용하지 않은 이유는 세밀한 커넥션 관리가 어렵기 때문입니다. JDBC Template을 사용할 경우, 같은 커넥션에서 작업을 진행하기 위해 @Transactional 어노테이션을 사용할 수 있지만, 이 경우 데이터소스마다 각각 다른 트랜잭션 매니저를 사용해야 하며, 이는 설정을 더 복잡하게 만듭니다.
application.yml
두 개의 데이터 소스 설정을 정의하고 있습니다. 하나는 JPA용이고, 다른 하나는 락 관리용입니다.
spring:
jpa:
hibernate:
ddl-auto: update
properties:
hibernate:
dialect: org.hibernate.dialect.MySQL8Dialect
format_sql: true
show_sql: true
datasource:
jpa:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/bucket_back?serverTimezone=Asia/Seoul&characterEncoding=UTF-8
username: root
password: password
hikari:
maximum-pool-size: 40
lock:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/bucket_back?serverTimezone=Asia/Seoul&characterEncoding=UTF-8
username: root
password: password
hikari:
maximum-pool-size: 20
DataSourceConfiguration 설정 클래스
@Configuration
public class DataSourceConfiguration {
@Bean
@Primary
@ConfigurationProperties("spring.datasource.jpa")
public DataSourceProperties dataSourceProperties() {
return new DataSourceProperties();
}
@Bean
@Primary
public DataSource dataSource(DataSourceProperties dataSourceProperties) {
return dataSourceProperties
.initializeDataSourceBuilder()
.type(HikariDataSource.class)
.build();
}
@Bean
@ConfigurationProperties("spring.datasource.lock")
public DataSourceProperties lockDataSourceProperties() {
return new DataSourceProperties();
}
@Bean
public DataSource lockDataSource(@Qualifier("lockDataSourceProperties") DataSourceProperties dataSourceProperties) {
return dataSourceProperties
.initializeDataSourceBuilder()
.type(HikariDataSource.class)
.build();
}
@Bean
public VoteLockDao voteLockDao(@Qualifier("lockDataSource") DataSource dataSource) {
return new VoteLockDao(dataSource);
}
}
VoteLockDao 클래스
@RequiredArgsConstructor
public class VoteLockDao {
private final DataSource dataSource;
public void executeWithLock(String key, Runnable runnable) {
try (Connection connection = dataSource.getConnection()) {
try {
getLock(connection, key);
runnable.run();
} finally {
releaseLock(connection, key);
}
} catch (SQLException e) {
throw new RuntimeException("락을 획득하는 중에 오류가 발생하였습니다.", e);
}
}
private void getLock(
final Connection connection,
final String key
) {
String sql = "SELECT GET_LOCK(?, ?)";
try (PreparedStatement stmt = connection.prepareStatement(sql)) {
stmt.setString(1, key);
stmt.setInt(2, -1);
stmt.executeQuery(); // 락 key 획득 시도, 무한 대기
} catch (SQLException e) {
e.printStackTrace();
}
}
private void releaseLock(
final Connection connection,
final String key
) {
String sql = "SELECT RELEASE_LOCK(?)";
try (PreparedStatement stmt = connection.prepareStatement(sql)) {
stmt.setString(1, key);
stmt.executeQuery(); // 락 key 해제
} catch (SQLException e) {
e.printStackTrace();
}
}
}
테스트 결과
동시성 문제가 발생하지 않았습니다.


4-5. Lettuce 스핀락 ✅
Redis의 기본 명령 중 하나인 SET 명령어를 활용하여 스핀락을 구현할 수 있습니다. 이 명령어는 Lettuce 라이브러리를 통해 사용할 수 있으며, NX 옵션을 통해 키가 아직 존재하지 않을 때만 값을 설정할 수 있습니다. 이러한 특성을 이용해, 락을 성공적으로 획득할 때까지 반복적으로 락 획득을 시도하는 스핀락을 구현할 수 있습니다.
스핀락 구현 시, Redis 서버에 과도한 부담을 주지 않기 위해 요청 간의 시간 간격을 조절하는 것이 중요합니다. 이를 위해 Thread.sleep() 메소드를 사용하여 각 시도 사이에 짧은 휴식 시간을 두어 서버의 부하를 관리하게 됩니다. 이 방법을 통해 레디스 서버에 미치는 영향을 최소화할 수 있습니다.
build.gradle
Lettuce를 사용하기 위해서는 spring-boot-starter-data-redis 의존성을 추가해야 합니다.
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
RedisConfig 설정 클래스
@Configuration
public 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;
@Bean
public RedisConnectionFactory redisConnectionFactory() {
final RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration(host, port);
configuration.setPassword(password);
return new LettuceConnectionFactory(configuration);
}
@Bean
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.setConnectionFactory(redisConnectionFactory());
return redisTemplate;
}
}
VoteLockManager 클래스
VoteLockManager 클래스에서는 스핀락을 사용하여 투표 참여 로직을 차례대로 진행할 수 있도록 제어합니다.
@Component
@RequiredArgsConstructor
public class VoteLockManager {
private final VoteManager voteManager;
private final VoteRedisManager voteRedisManager;
public void excuteWithLock(
final Vote vote,
final Long memberId,
final Long itemId
) throws InterruptedException
{
while (Boolean.FALSE.equals(voteRedisManager.lock(String.valueOf(vote.getId())))) {
Thread.sleep(100); // 락 획득까지 대기
}
try {
voteManager.participate(vote, memberId, itemId);
} finally {
voteRedisManager.unlock(String.valueOf(vote.getId()));
}
}
}
VoteRedisManager 클래스
VoteRedisManager 클래스에서는 Redis를 이용한 락의 획득과 해제 로직을 구현합니다.
@Component
@RequiredArgsConstructor
public class VoteRedisManager {
private final RedisTemplate<String, Object> redisTemplate;
public Boolean lock(final String key) {
return redisTemplate.opsForValue().setIfAbsent(key, "LOCK", Duration.ofSeconds(3));
}
public void unlock(final String key) {
redisTemplate.delete(key);
}
}
테스트 결과
동시성 문제가 발생하지 않았습니다.


4-6. Redission 분산락 ✅
Redis를 사용한 분산락 구현에는 Redission 라이브러리를 활용할 수 있습니다. Redission은 Redis 클라이언트 라이브러리 중 하나로, 분산락 기능을 제공합니다.
Redission의 분산락은 tryLock 메서드를 사용하여 락을 획득할 때까지 대기합니다. 내부적으로 pub/sub 메커니즘을 이용해, 락을 해제하는 이벤트가 발생하면 해당 이벤트를 구독하고 있는 다른 클라이언트들에게 신속하게 알림을 전송합니다. 이를 통해 대기 중인 클라이언트들은 락을 획득할 수 있는 기회를 얻게 됩니다. pub/sub 기반의 알림 시스템 덕분에, Redission 분산락은 Lettuce를 사용한 스핀락 방식에 비해 서버 부하를 현저히 줄일 수 있습니다.
RedissonClient를 통해, 손쉽게 분산락 기능을 활용할 수 있습니다.
build.gradle
implementation 'org.redisson:redisson-spring-boot-starter:3.27.2'
Redisson을 사용하기 위해서는 먼저 redisson-spring-boot-starter 의존성을 build.gradle 파일에 추가해야 합니다.
RedisConfig 클래스
@Configuration
public 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;
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer().setAddress("redis://" + host + ":" + port);
return Redisson.create(config);
}
}
VoteLockManager 클래스
VoteLockManager 클래스에서는 RedissonClient를 사용하여 키에 대한 락을 획득하고, 이 락을 통해 동시성 제어를 수행합니다.
@Slf4j
@Component
@RequiredArgsConstructor
public class VoteLockManager {
private final RedissonClient redissonClient;
public void excuteWithLock(
final String key,
final Runnable runnable
) throws InterruptedException {
final RLock lock = redissonClient.getLock(key);
try {
boolean available = lock.tryLock(10, 3, TimeUnit.SECONDS);
if (!available) {
log.error("Lock is not available");
return;
}
runnable.run();
} finally {
lock.unlock();
}
}
}
테스트 결과
동시성 문제가 발생하지 않았습니다.


5. MySQL 네임드락 vs Lettuce 스핀락 vs Redisson 분산락 🧐
최종적으로 MySQL의 네임드락을 선택하게 되었습니다. 이미 DB 서버로 MySQL을 사용하고 있었으며, 결정적으로 네임드락이 다른 락들에 비해 가장 높은 성능을 보여주었기 때문입니다. 네임드락을 사용함으로써 관리해야 할 설정이 늘어나는 점은 있지만, 이러한 설정은 한 번만 해두면 크게 변경할 필요가 없어서, 추가 설정의 부담은 크게 문제가 되지 않는다고 판단했습니다.
반면, Redisson을 사용하는 경우에는 새로운 의존성을 프로젝트에 추가하고 관련 설정을 해야 하는 번거로움이 있습니다. 또한, 스핀락 방식은 Redis 서버에 상당한 부담을 주며, 성능 면에서도 가장 느린 것으로 나타났습니다. 이러한 이유들로 우리 프로젝트에는 MySQL 네임드락이 가장 적합한 선택이라고 결론 내렸습니다.
TPS 비교
동시 참여자 수 1500명으로 다시 테스트를 진행했을 때, TPS(초당 트랜잭션 처리량) 비교한 결과 네임드 락이 가장 높은 성능을 보여주었습니다.
| 락 종류 | TPS |
| 네임드락 | 325.2 |
| Lettuce 스핀락 | 241.7 |
| Redission 분산락 | 258.2 |
6. 마치며
보통의 경우처럼 비관적 락을 사용해 동시성을 처리할 수 있다고 생각했지만, 제 경우에는 다르게 접근해야 해서 신기했습니다. 이번 기회를 통해 다양한 락에 대해 공부하고 적용해볼 수 있어서 정말 좋았습니다.
다음에는 투표 참여 시 마다 voters의 Count 쿼리를 실행하는 방식에서 벗어나, 반정규화를 통해 성능을 개선해볼 예정입니다.
읽어주셔서 감사합니다!
참고
- https://www.inflearn.com/course/%EB%8F%99%EC%8B%9C%EC%84%B1%EC%9D%B4%EC%8A%88-%EC%9E%AC%EA%B3%A0%EC%8B%9C%EC%8A%A4%ED%85%9C
- https://techblog.woowahan.com/2631/https://docs.spring.io/spring-boot/docs/current/reference/html/howto.html#howto.data-access.configure-two-datasources
- https://docs.spring.io/spring-boot/docs/current/reference/html/howto.html#howto.data-access.configure-two-datasources