2025. 7. 30. 18:27ㆍ개발
노래 게임인데 노래가 없다!!!??
노래부터 만듭시다
K-POP 자동 데이터셋 빌더 개발기
저는 실시간 6인 음악 퀴즈 게임(SingSongGame) 프로젝트를 진행하며, 곡 데이터를 효율적으로 수집하는 문제를 맞닥뜨렸습니다. 단순히 사람 손으로 곡과 가사를 입력하기에는 시간이 너무 많이 들었고, 게임의 핵심 기능인 AI 음악 퀴즈와 자동 채점 기능을 제대로 테스트하기 위해서는 수백~수천 개의 곡 데이터가 필요했습니다.
이를 해결하기 위해 YouTube, Genius, Spotify, Last.fm 등 다양한 API를 통합하여 K-POP 곡의 하이라이트 오디오 클립(30초)과 메타데이터를 자동으로 수집하는 데이터 파이프라인을 직접 설계하고 구현했습니다. 최종적으로 자동으로 CSV 데이터셋을 생성하여 DB 및 Pandas에서 바로 활용할 수 있도록 하였습니다.
프로젝트 목표
- 사람이 수작업으로 입력하지 않아도, 수백 곡의 데이터를 자동으로 수집
- 곡의 하이라이트(후렴) 구간만 30초 추출하여 게임 문제에 적합한 오디오 클립 생성
- 곡의 가사 스니펫, 장르 태그, 발매 연도 등을 함께 수집해 부가 기능 확장
- 중복/불필요한 영상(커버곡, 직캠 등)을 자동으로 걸러내고, 정확한 공식 영상만 다운로드
데이터 파이프라인 설계
1. YouTube 검색 → 오디오 다운로드
YouTube에는 하나의 곡에 대해 수많은 영상이 올라옵니다. 예를 들어 "Hype Boy - NewJeans"를 검색하면, 공식 MV부터 직캠, 커버곡, 반주 영상까지 혼재되어 있습니다.
이를 해결하기 위해 4-레벨 다단계 검색 + 스코어링 알고리즘을 적용했습니다. 검색을 단계적으로 좁은 범위 → 넓은 범위로 확장하며, 각 영상에 점수를 부여해 가장 정확한 영상을 선택합니다.
| 레벨 | 검색 쿼리 | 목적 |
|---|---|---|
| Lv1 | <title> <artist> official music video |
가장 정확도 높은 공식 MV 우선 탐색 |
| Lv2 | <title> <artist> MV |
MV 키워드만 포함한 검색 |
| Lv3 | <title> <artist> official audio / topic |
공식 음원, Topic 채널 탐색 |
| Lv4 | <title> <artist> |
마지막 fallback: 일반 검색 |
스코어링 알고리즘:
score = (제목·아티스트 적중도 × 10)
+ (채널 가점 × 5)
+ log10(조회수)
- 곡 제목과 아티스트가 정확히 포함되면 높은 점수 부여
- 채널 이름에 "official" 또는 아티스트명이 포함되면 추가 가점
- 조회수가 많을수록 신뢰도 증가
- 커버, 직캠, 반주 등은
BLACKLIST로 자동 제외
최종적으로 가장 높은 점수의 영상만 다운로드하여 64kbps MP3로 추출합니다.
2. 하이라이트(후렴) 구간 자동 감지
곡 전체를 사용하면 게임 플레이에 비효율적이므로, 후렴구 30초만 자동으로 추출합니다.
- pychorus 라이브러리로 반복 구간(후렴)을 탐지
- 탐지된 구간의 RMS(평균 에너지)가 너무 낮으면 무음으로 간주 → 백업 알고리즘 사용
- 백업 알고리즘: librosa로 전체 곡 중 에너지가 가장 높은 지점을 시작점으로 선택
- 두 방식 모두 실패 시 기본값
start_sec = 45초
3. 메타데이터 수집
- Genius API : 가사 스니펫 최대 15줄
- Spotify API : 발매 연도, 앨범명, 아티스트 정보
- Last.fm API : 장르 태그
- 모든 정보는 정제 후 CSV 파일로 저장
4. S3 업로드 (선택)
AWS 자격증명이 설정되어 있다면, 추출된 MP3를 S3에 업로드하여 공개 URL을 반환합니다. 없다면 로컬 파일 URI를 사용합니다.
힌트 및 정답 문자열 생성
게임 문제 출제를 위해, 곡 제목을 기반으로 힌트 문자열을 자동 생성합니다.
- 한글: 초성 힌트 (예: "사랑해" → ㅅㄹㅎ)
- 영어: 첫 단어만 보이고 나머지는 블랭크 처리 (예: "Hype Boy" → H____ B__)
사용 예시
# songs.txt : "제목|아티스트" 형식
python dataset_builder.py songs.txt
실행 후, dataset.csv 파일이 자동 생성되며 아래와 같은 정보를 포함합니다.
| title | artist | audio_url | lyrics | tags | hint | answer |
|---|---|---|---|---|---|---|
| Hype Boy | NewJeans | https://s3.../hype_boy_trimmed.mp3 | Cause I know what you like boy... | 케이팝, 2022 | ㅎ ㅂ | hypeboy |
그리고 내가 이번 키싱유 게임에서 메인으로 구현한 기능은 총 2가지였다.
- AI 평어 노래 맞추기 게임
- 빠른 대전 매칭 시스템
1. AI 평어 노래 맞추기 게임 – TTS 동기화 문제와 흐름 제어 리팩토링
AI 평어 노래 맞추기 게임에서 가장 까다로웠던 부분 중 하나는 모든 유저에게 동기화된 오디오(TTS) 재생을 보장하는 것이었습니다. 이 문제를 해결하기 위해 게임 흐름을 프론트엔드 주도에서 백엔드 주도로 전환하는 리팩토링 과정을 진행했습니다.
문제 상황
- 게임의 전체 진행 흐름(라운드 전환, TTS 재생 등)을 프론트엔드가 제어하고 있었습니다.
- 그 결과, 유저마다 오디오 시작 타이밍이 달라지는 동기화 문제가 발생했습니다.
- 특히 네트워크 지연이나 탭 비활성화 등의 환경적 이슈로 인해 불공정한 게임 진행이 일어날 수 있었습니다.
해결 방법 – 게임 흐름을 백엔드가 통제하도록 리팩토링
우리는 게임 흐름 제어 권한을 프론트에서 백엔드로 이동시키기 위해, 오디오 재생이 끝나는 시점을 서버에 알려주는 notifyTtsFinished() 함수를 도입했습니다.
// 오디오 재생 종료 후 서버에 알림
newAudio.onended = () => {
console.log("오디오 재생 종료됨");
setGameState((prev) => ({ ...prev, isReading: false }));
setShowRobot(false);
if (user.id === room.hostId) {
notifyTtsFinished(room.roomId);
}
};
// 서버에 "TTS 재생 끝났다" 알리는 함수
const notifyTtsFinished = (roomId: Number) => {
fetch(`/api/ai-game/${roomId}/tts-finished`, {
method: "POST",
})
.then(() => console.log("TTS 종료 알림 전송됨"))
.catch((err) => console.error("TTS 종료 알림 실패", err));
};
왜 실시간 게임 흐름은 백엔드가 주도해야 할까?
실시간 멀티플레이 게임에서 서버가 흐름을 주도해야 하는 이유는 다음과 같다.
- 동기화 보장: 서버 시계 기준으로 오디오 재생, 정답 입력, 라운드 전환 등의 타이밍을 통일할 수 있습니다.
- 치팅 방지: 클라이언트 조작을 통한 부정 행위를 방지할 수 있습니다.
- 신뢰성 확보: 서버에서 전적, 결과, 시간을 기록하면 명확한 근거가 남습니다.
- 호스트 기준 흐름 통일: TTS 종료 여부를 호스트 기준으로 판단하여, 진행 기준점을 하나로 통일할 수 있습니다.
리팩토링 효과
- 게임 로직의 예측 가능성 증가
- 유저 간 동기화 문제 해결 (오디오 재생 기준 통일)
- 프론트 코드 간소화 – 서버 응답에 따라 UI만 업데이트하면 됨
- 서버 기반 구조로 확장성 확보 – 관전 모드, 리플레이 기능 개발 기반 마련
2. 빠른 대전 매칭 시스템
싱송겜 프로젝트에서는 실시간 6인 다대다 형태의 빠른 대전 모드를 구현했습니다. 이를 위해 MMR 기반 매칭 시스템과 Glicko-1 레이팅 알고리즘을 적용하고, 실제 게임 환경에 맞게 경량화 및 커스터마이징했습니다.
매칭 흐름
public void tryMatch(User user) {
int baseMmr = user.getQuickMatchMmr();
int expand = 0;
while (expand <= 3000) {
int min = baseMmr - 50 - expand;
int max = baseMmr + 50 + expand;
Set candidates = quickMatchQueueService.getCandidatesInRange(min, max);
if (candidates.size() >= 3) {
List matchedUsers = selectTop6(candidates);
QuickMatchService.startQuickMatchGame(matchedUsers);
matchedUsers.forEach(u -> quickMatchQueueService.removeFromQueue(u.getId()));
return;
}
expand += 50;
}
}
유저가 매칭 큐에 진입하면, 기준 MMR을 중심으로 점차 탐색 범위를 확장하며 후보를 찾습니다. 조건을 만족하면 상위 6명을 선별해 게임을 시작합니다.
레이팅 갱신 – Glicko-1 기반
public void updatePlayer(GlickoPlayer player, List matches) {
...
double newRating = ...
double newRD = ...
player.setRating(newRating);
player.setRd(newRD);
}
각 게임 결과는 Glicko 알고리즘에 따라 점수와 RD(Rating Deviation)를 갱신합니다. RD는 해당 유저의 MMR 정확도를 나타내며, RD가 낮을수록 실력이 정밀하게 측정된 상태입니다.
RD 설계에 대한 고민
RD는 Glicko 시스템의 핵심 요소로, 일반적으로는 오랜 기간 게임을 하지 않으면 RD가 증가하여 불확실성이 커집니다.
그러나 이번 프로젝트에서는 다음과 같은 이유로 시간 기반 RD 증가 로직을 제거했습니다.
- 우리 게임은 일반적인 e스포츠처럼 실력이 급격히 퇴화하지 않습니다.
- 사용자 기반이 비교적 캐주얼하며, 플레이 패턴이 다양합니다.
- 실력보다 경험/운 요소가 더 클 수 있는 게임 구조입니다.
따라서 RD는 게임을 실제로 플레이했을 때만 갱신되며, 시간이 지나도 자동으로 증가하지 않습니다.
double newRD = Math.sqrt(1.0 / ((1.0 / (player.getRd() * player.getRd())) + (1.0 / d2)));
player.setRd(newRD);
위 코드처럼, RD는 매 게임 결과마다 수학적으로 다시 계산되며, 유저의 성능에 따라 점차 낮아지게 됩니다. 이를 통해 지속적인 게임 참여를 유도하고, 불필요한 불확실성 증가를 막을 수 있었습니다.
회고
빠른 대전 매칭 시스템을 구현하며 다대다 환경에서의 매칭 알고리즘 설계, 실시간 게임 흐름 제어, Glicko 알고리즘의 커스터마이징 방법을 깊이 있게 학습할 수 있었습니다. 앞으로는 장르 특화 레이팅 시스템, 비정형 유저 행동을 반영한 동적 MMR 조정 등 더욱 정교한 매칭 로직도 연구해보고 싶습니다.
3. Glicko-1 vs 커스터마이징 Glicko 시뮬레이션 결과
파이썬 기반 시뮬레이션을 통해 기존 Glicko-1 알고리즘과 커스터마이징 알고리즘의 성능을 비교 실험하였습니다.
- 실력 차 가중치 (diff_weight)
- 연속 승/패 보정 (streak_weight)
- 신규 유저 RD 조정 (초기 RD 400.0)

| 항목 | Glicko-1 | 커스터마이징 Glicko |
| 초기 반응 속도 | 비교적 느림 | RD 증가로 인해 더 빠르게 변화 |
| 실력차 영향도 | 동일 가중치 | 실력차 큰 상대일수록 결과 반영 큼 |
| 연승/연패 보정 | 없음 | 연승 시 급격히 상승, 연패 시 완화됨 |
| 그래프 형태 | 완만한 곡선 | 초기 가속, 중후분 안정 |
실험 결과 요약
- 초기 RD 증가 → 빠른 적응: 신규 유저가 적은 경기 수로도 자기 실력 근처로 수렴함
- 실력차 가중치 → 더 공정한 승점/패점 분배 가능
- 연승/연패 보정 → 유저 만족감 증가 및 탈락 방지
UX 중심 설계 가능성
- 기존 Glicko는 수학적으로 정밀하나 사용자 경험 최적화 부족
- 커스터마이징은 캐주얼 게임, 다대다 매칭에 효과적
주의점
- 과도한 보정은 실력 기반 매칭을 왜곡할 수 있음
- 데이터 기반 미세 조정 필요
결론
커스터마이징된 알고리즘은 다대다 게임이나 UX 중심 매칭 시스템에서 유의미한 성능 개선을 보여줍니다.
특히 초기 반응성, 연패 완화, 실력차 반영 측면에서 사용자 친화적인 설계를 가능하게 합니다.
'개발' 카테고리의 다른 글
| Outbox + CQRS + Projection으로 읽기 성능을 분리한 Mini CDC 구현기 (StackOps) (0) | 2026.02.03 |
|---|---|
| 나만의 무기 4편 - 회고 (2) | 2025.07.31 |
| 나만의 무기 2편 – 기획 아이디어 (3) | 2025.07.30 |
| 나만의 무기 1편: 개발 보다 더 어려운 기획 회의 (1) | 2025.07.30 |
| Pintos VM Project - merge-mm / stk / pa 테스트 트러블슈팅 정리 (0) | 2025.06.15 |