개발한 대기열 시스템을 Jmeter를 사용하여 성능 테스트를 진행해보려합니다.
public Mono<Void> registerUserToWaitQueue(
String userId,
String queueType
) {
String queueKey = queueType + WAIT_QUEUE;
return Mono.zip(
searchUserRanking(userId, queueType, "wait"),
searchUserRanking(userId, queueType, "allow"),
(inWait, inAllow) -> new Long[]{ inWait, inAllow }
)
.flatMap(arr -> {
Long inWait = arr[0];
Long inAllow = arr[1];
boolean isRegistered = (inWait != -1 || inAllow != -1);
if (isRegistered) {
return Mono.error(new ReserveException(
HttpStatus.BAD_REQUEST,
ErrorCode.ALREADY_REGISTERED_USER
));
}
return generateScore()
.flatMap(score ->
reactiveRedisTemplate.opsForZSet()
.add(queueKey, userId, score)
.flatMap(added -> {
if (!added) {
return Mono.error(new ReserveException(
HttpStatus.BAD_REQUEST,
ErrorCode.ALREADY_REGISTERED_USER
));
}
return updateQueue(queueType, userId, QueueStatus.QUEUE_REGISTERED);
})
.doOnNext(m -> log.info("{}님 사용자 대기열 등록 성공", userId))
);
})
.doOnError(e ->
log.error("대기열 등록 오류: userId={}, {}", userId, e.getMessage())
)
.then();
}
public Mono<Long> generateScore() {
long timestampMicros = Instant.now().toEpochMilli() * 1000;
return reactiveRedisTemplate.opsForValue()
.increment("queue:seq") // 전역 증가값
.map(seq -> {
long seqMasked = seq & 0xFFFFF; // 하위 20bit만 사용
return (timestampMicros << 20) | seqMasked;
});
}
기존의 Instance.now() 기반 방식은 짧은 시간 안에 대량의 요청이 몰릴 경우 동일한 timestamp가 생성될 수 있어, 단순 timestamp만으로는 요청 순서를 안정적으로 보장하기 어렵습니다.
반면 timestamp + Redis INCR 방식은 timestamp를 상위 비트로 사용해 시간 순서를 유지하고, INCR로 생성한 고유한 sequence 값을 하위 비트에 결합하여 동일한 시간대에서도 중복되지 않는 score를 만들어내기 때문에 중복된 timestamp 값을 갖지 않아 순번이 잘 유지될 수 있습니다.
Instance.now() : 현재 시점의 UTC 기준 시간을 나타내는 Java의 시간 API
Redis의 INCR는 정수를 1 증가시키는 명령어이며, INCR의 결과로 나온 증가된 숫자 자체가 sequence
중복 없는 user_id 생성을 위해 ${__threadNum}을 사용하여 각 요청이 고유 사용자로 처리되도록 테스트 설정


스파이크 테스트
1초에 500명의 사용자가 대기열 등록을 시도하는 부하 테스트


1초에 1000명의 사용자가 대기열 등록을 시도하는 부하 테스트


짧은 시간 안에 대량의 요청이 몰리는 스파이크 테스트에서는 초기 대기열 순서에 최대 10명 정도의 오차가 발생함을 확인할 수 있습니다.
이를 완전히 제거하려면 락 등을 적용해 순서를 강제해야 하지만, 이러한 방식은 성능 저하를 초래하므로 이 정도의 편차는 정책적으로 허용하는 것이 적절한 것 같습니다.
부하 테스트
1분간 4000명의 사용자를 점진적으로 증가시켜 대기열 등록을 시도하는 테스트


스파이크 테스트에 비해 순서가 잘 지켜짐을 확인할 수 있으며, 평균 응답 시간도 10ms로 빠름을 확인할 수 있습니다.
중복 사용자가 동시에 대기열에 진입하는 상황을 검증하기 위해 Thread 수를 5, Ramp-Up 시간을 1초, Loop Count를 1로 설정하고 user_id 값을 동일하게 고정하여 여러 스레드가 동시에 동일 사용자로 대기열에 등록하도록 요청을 전송했습니다.

첫 번째로 등록된 사용자를 제외하고, 이후 동일 사용자로 요청된 모든 등록 시도가 정상적으로 실패된 것을 확인할 수 있습니다.
