📚FOS Study
홈카테고리
홈카테고리

카테고리

  • AI 페이지로 이동
    • RAG 페이지로 이동
    • agents 페이지로 이동
    • custom-agents 페이지로 이동
    • Claude Code의 Skill 시스템 - 개발자를 위한 AI 자동화의 새로운 차원
    • 멀티모달 LLM (Multimodal Large Language Model)
  • architecture 페이지로 이동
    • 디자인 패턴
    • 분산 트랜잭션
    • 슬롯 게임 엔진 고도화 — 2025년 회고
  • css 페이지로 이동
    • FlexBox 페이지로 이동
  • database 페이지로 이동
    • mysql 페이지로 이동
    • opensearch 페이지로 이동
    • 김영한의-실전-데이터베이스-설계 페이지로 이동
    • 커넥션 풀 크기는 얼마나 조정해야할까?
    • 인덱스 - DB 성능 최적화의 핵심
  • devops 페이지로 이동
    • docker 페이지로 이동
    • k8s 페이지로 이동
    • k8s-in-action 페이지로 이동
    • monitoring 페이지로 이동
  • go 페이지로 이동
    • Go 언어 기본 학습
  • http 페이지로 이동
    • HTTP Connection Pool
  • interview 페이지로 이동
    • 210812 페이지로 이동
    • 뱅크샐러드 AI Native Server Engineer
    • CJ 올리브영 지원 문항
    • CJ 올리브영 커머스플랫폼유닛 Back-End 개발 지원 자료
    • 마이리얼트립 - Platform Solutions실 회원주문개발 Product Engineer
    • NHN 서비스개발센터 AI서비스개발팀
    • nhn gameenvil console backend 직무 인터뷰 준비
    • 면접을 대비해봅시다
    • Tossplace Node.js Developer
    • 토스플레이스 Node.js 백엔드 컬처핏
  • java 페이지로 이동
    • jdbc 페이지로 이동
    • opentelemetry 페이지로 이동
    • spring 페이지로 이동
    • spring-batch 페이지로 이동
    • Java의 로깅 환경
    • MDC (Mapped Diagnostic Context)
    • OpenTelemetry 란 무엇인가?
    • Virtual Thread와 Project Loom
  • javascript 페이지로 이동
    • Data_Structures_and_Algorithms 페이지로 이동
    • Heap 페이지로 이동
    • typescript 페이지로 이동
    • AbortController
    • Async Iterator와 제너레이터
    • CommonJS와 ECMAScript Modules
    • 제너레이터(Generator)
    • Http Client
    • Node.js
    • npm vs pnpm 선택기준은 무엇인가요?
    • `setImmediate()`
  • kafka 페이지로 이동
    • Kafka 기본
    • Kafka를 사용하여 **데이터 정합성**은 어떻게 유지해야 할까?
    • 메시지 전송 신뢰성
  • network 페이지로 이동
    • L2(스위치)와 L3(라우터)의 역할 차이
    • L4와 VIP(Virtual IP Address)
    • IP Subnet
  • react 페이지로 이동
    • JSX 페이지로 이동
    • VirtualDOM 페이지로 이동
    • v16 페이지로 이동
  • redis 페이지로 이동
    • Redis
    • Redis Hash와 Lua 스크립트로 잭팟 누적 구현하기
  • task 페이지로 이동
    • ai-service-team 페이지로 이동
    • nsc-slot 페이지로 이동
📚FOS Study

개발 학습 기록을 정리하는 블로그입니다.

바로가기

  • 홈
  • 카테고리

소셜

  • GitHub
  • Source Repository

© 2025 FOS Study. Built with Next.js & Tailwind CSS

목록으로 돌아가기
📁task/ nsc-slot

슬롯 스핀 성능 최적화 — AliasMethod와 Random 선택기

약 4분
2026년 3월 22일
GitHub에서 보기

슬롯 스핀 성능 최적화 — AliasMethod와 Random 선택기

진행 기간: 2025.01 ~ 2025.02


배경

시뮬레이터로 100만 스핀을 돌리다 보면 속도 차이가 꽤 크게 느껴진다. 슬롯 한 종류에 1~2분이면 끝나야 할 시뮬레이션이 10분 넘게 걸리는 경우도 있었다. 직접 기여한 비중이 크지는 않지만, 병목을 파악하면서 두 가지를 정리해두고 싶어서 기록으로 남긴다.


1. 가중치 랜덤 선택 — Alias Method

기존 방식

슬롯 릴은 심볼마다 등장 가중치가 다르다. "7" 심볼은 가중치 1, "체리"는 가중치 100 같은 식이다.

가중치 기반으로 하나를 선택하는 가장 직관적인 방법은 이렇다.

// 누적 합 방식 (O(n))
int totalWeight = 0;
for (int w : weights) totalWeight += w;

int pivot = random.nextInt(totalWeight);
int sum = 0;
for (int i = 0; i < weights.length; i++) {
    sum += weights[i];
    if (sum > pivot) return i;
}

가중치 배열 길이가 n이면 최악의 경우 n번 탐색해야 한다. 릴 하나에 심볼이 수십 개, 스핀 한 번에 릴이 5개, 시뮬레이터는 100만 스핀이라면 이 반복이 꽤 쌓인다.

Alias Method

Alias Method는 사전에 테이블을 만들어두고, 선택 시점에는 딱 2번의 랜덤으로 O(1) 선택을 한다.

아이디어는 이렇다. 가중치를 정규화해서 각 항목이 "평균"보다 많거나 적은 구역을 가지도록 쌍을 맞춘다.

가중치: [1, 3, 2]  →  총합 6, 평균 2

정규화:
  인덱스 0: 비율 1/2 → 부족 (Small)
  인덱스 1: 비율 3/2 → 초과 (Large)
  인덱스 2: 비율 2/2 → 정확

Small과 Large를 쌍으로 맞춰 alias 배열 구성:
  prob[0] = 1/2,  alias[0] = 1  ← 0을 선택했는데 확률 미달이면 1로 대체
  prob[1] = 1,    alias[1] = 1
  prob[2] = 1,    alias[2] = 2

선택 시:

// 항상 O(1)
int i = random.nextInt(n);           // 구역 선택
int r = random.nextInt(avg);         // 구역 내 위치
return r < prob[i] ? i : alias[i];  // 확률에 따라 원본 또는 대체 반환

실제 코드는 SlotAliasMethodMaker.of(int[] weights)에서 테이블을 생성하고, AliasTable.pick()에서 위 로직으로 선택한다.

// 테이블 생성 (한 번만)
SlotAliasMethod aliasMethod = SlotAliasMethodMaker.of(weights);

// 선택 (O(1), 매번)
public String pick() {
    final int i = ThreadLocalRandom.current().nextInt(alias.getIndices().length);
    final int r = ThreadLocalRandom.current().nextInt(avg);
    return r < probability.getFigures()[i]
        ? representative.getRepresentatives()[i]
        : representative.getRepresentatives()[alias.getIndices()[i]];
}

테이블 생성은 슬롯 초기화 시점에 한 번만 한다. 이후 스핀마다 호출되는 pick()은 항상 O(1)이다.


2. 게임에서 Random은 무엇을 써야 하는가

SecureRandom이 슬롯에 있었다

코드 안에 SecureRandom을 쓰는 부분이 있었다.

private static final SecureRandom RANDOM = new SecureRandom();
final int random = RANDOM.nextInt(maxRandom);

SecureRandom은 암호학적으로 안전한 난수 생성기다. 암호화 키 생성, 세션 토큰 생성처럼 "결과를 예측할 수 없어야" 하는 곳에 쓴다. OS의 엔트로피 풀을 사용하기 때문에 생성 비용이 높고, 멀티스레드 환경에서 내부적으로 synchronized 처리가 들어간다.

슬롯에서 암호학적 난수가 필요한가

슬롯은 서버가 결과를 결정하고 클라이언트에 전달하는 구조다. 유저는 서버의 난수 생성 과정에 접근할 수 없다. 클라이언트가 다음 스핀 결과를 예측할 방법이 없다.

암호학적 난수가 필요한 경우는 공격자가 내부 상태를 알아내서 다음 값을 예측하는 시나리오를 막아야 할 때다. 슬롯에서 랜덤 생성 내부 상태가 외부에 노출될 방법이 없으므로, SecureRandom의 보안 강도는 여기서는 과잉이다.

ThreadLocalRandom으로 충분하다.

JMH 벤치마크

실제로 얼마나 차이가 나는지 JMH로 측정했다. 1000만 번 랜덤을 뽑는 기준이다.

ThreadLocalRandom: 70.241 ops/s
SecureRandom:       1.197 ops/s

약 58배 차이다. 시뮬레이터에서 스핀 하나당 수십 번 랜덤을 뽑는 걸 감안하면 이 차이가 누적된다.

왜 ThreadLocalRandom이 빠른가

ThreadLocalRandom은 이름 그대로 스레드별 독립 인스턴스다. 각 스레드가 자신만의 상태를 가지기 때문에 스레드 간 경쟁이 없다.

SecureRandom은 내부적으로 synchronized 블록을 사용한다. 멀티스레드 환경에서 락 경합이 발생한다. 시뮬레이터는 멀티스레드로 스핀을 처리하기 때문에 이 비용이 더 크다.


3. ThreadLocalRandom 올바른 사용법

ThreadLocalRandom으로 바꾸면 끝이 아니다. 잘못 쓰면 의도한 대로 동작하지 않는다.

필드로 저장하면 안 된다

// 잘못된 방식
@Component
public class ThreadLocalRandomProvider {
    private final ThreadLocalRandom random = ThreadLocalRandom.current(); // ← 문제
    ...
}

ThreadLocalRandom.current()는 현재 스레드에 귀속된 인스턴스를 반환한다. 필드로 저장하면 저장 시점의 스레드(Spring 초기화 스레드)에 귀속된 인스턴스가 고정된다. 다른 스레드에서 이 필드를 쓰면 같은 인스턴스를 공유하게 되어 스레드 안전하지 않다.

실제로 이 패턴으로 작성된 코드에서 버그가 났다.

// 올바른 방식: 매번 current() 호출
private ThreadLocalRandom getRandom() {
    return ThreadLocalRandom.current(); // 호출 시점의 스레드 인스턴스 반환
}

ThreadLocalRandom.current()는 매번 호출해도 비용이 크지 않다. 스레드 로컬에서 조회하는 것뿐이다.


배운 것

암호학 도구는 암호학에만 쓰자. SecureRandom이 더 안전해 보여서 기본 Random 대신 쓰는 경우가 있다. 암호학적 보장이 필요하지 않은 곳에 쓰면 성능 비용만 지불하게 된다. "안전하다 = 더 좋다"는 아니다. 목적에 맞는 도구를 쓰는 게 맞다.

ThreadLocal 객체는 필드에 저장하면 안 된다. ThreadLocal의 핵심은 "호출 시점의 스레드"에 귀속된 값을 가져오는 것이다. 필드로 저장하면 이 특성이 깨진다. ThreadLocalRandom뿐 아니라 모든 ThreadLocal 계열 객체에 적용된다.


사용 기술

  • Java 17
  • JMH (Java Microbenchmark Harness)
task 카테고리의 다른 글 보기수정 제안하기

댓글

댓글을 불러오는 중...
목차
  • 슬롯 스핀 성능 최적화 — AliasMethod와 Random 선택기
  • 배경
  • 1. 가중치 랜덤 선택 — Alias Method
  • 기존 방식
  • Alias Method
  • 2. 게임에서 Random은 무엇을 써야 하는가
  • SecureRandom이 슬롯에 있었다
  • 슬롯에서 암호학적 난수가 필요한가
  • JMH 벤치마크
  • 왜 ThreadLocalRandom이 빠른가
  • 3. ThreadLocalRandom 올바른 사용법
  • 필드로 저장하면 안 된다
  • 배운 것
  • 사용 기술