Files
chocomae/doc/plan/db/07-alternative-approaches.md
T
2026-09-15 10:38:05 +09:00

36 KiB

기록 DB 성능 문제의 다른 해결 방법 연구

개요 문서 | 작성일: 2026-09-14 안1~안5(MariaDB 안에서 인덱스·집계·아카이빙·파티셔닝·코드 개선)와 전혀 다른 방향의 해결책을 검토한 문서입니다. 검토 대상: 학교(선생님) 단위 테이블 분할, 다른 저장소(Redis·MongoDB·Kafka·분석 DB·S3), DB 실행 환경 변경, 애플리케이션·제품 정책 변경


0. 결론 먼저

방법 성능에 도움? 이 프로젝트 적합도 한 줄 평가
MariaDB 설정 튜닝 (buffer pool 등) 클 가능성 높음 ★★★★★ 설정이 기본값(128MB)이면 가장 싸고 빠른 개선. 측정부터
랭킹 5초 폴링 방식 변경 큼 (반복 조회 제거) ★★★★★ DB를 바꾸지 않고 요청 수 자체를 줄임
PHP 실행 환경 점검 (연결 재사용, OPcache) 중간 ★★★★ 요청마다 DB 새 연결 + 추가 쿼리 발생 중
서버 사양·스토리지 점검 (CPU 크레딧, gp3) 상황에 따라 큼 ★★★★ 수업 시간대만 느리다면 1순위 의심
Redis로 랭킹 처리 랭킹 화면에 한해 큼 ★★★ 랭킹 자료구조와 딱 맞지만 운영 대상이 하나 늘어남
RDS(관리형 MariaDB)로 이전 성능보다는 운영 편의 ★★★ "DB 관리 부담을 줄이고 싶다"가 목적일 때
DB 서버 분리 / 읽기 복제본 중간 ★★ 웹·DB가 자원을 두고 경쟁할 때만
오래된 기록을 S3 파일로 보관 용량에 도움 ★★ 과거 기록 실시간 조회가 필요 없어질 때
학교(선생님) 단위 테이블 분할 인덱스와 거의 같은 효과 인덱스가 같은 일을 훨씬 싸게 함. 테이블 수 폭증 문제 큼
MongoDB로 기록 이전 거의 없음 같은 인덱스·집계 작업이 필요하고, 이전 비용·DB 2개 운영
Kafka 도입 없음 (읽기 문제엔 무관) 쓰기 폭주용 도구. 방금 저장한 기록이 랭킹에 늦게 보이는 부작용
분석 전용 DB (ClickHouse 등) 현재 기능엔 없음 대규모 통계 대시보드가 생길 때 검토

핵심 판단: 현재 문제는 데이터가 많아서가 아니라, 데이터를 읽는 방식 때문입니다. best_record 120만 행은 MariaDB 기준으로 작은 편이며, 적절한 인덱스가 있으면 수억 행에서도 같은 종류의 조회가 빠르게 동작합니다. 다른 기술로 옮겨도 "필요한 범위만 읽게 하기(인덱스)"와 "미리 계산해 두기(집계)"는 똑같이 필요하므로, 새 기술은 문제를 옮길 뿐 없애지 않습니다. 반면 운영할 서버·백업·장애 지점은 늘어납니다.

그래서 추천 조합은 다음과 같습니다.

  1. 환경 측정 → MariaDB 설정 튜닝 (4장, 비용 거의 0)
  2. 안1 (인덱스 + 쿼리 조건)
  3. 랭킹 폴링 개선 + PHP 연결 점검 (5장, 6장)
  4. 그래도 부족하거나 규모가 커지면 → Redis 랭킹 또는 RDS 이전 검토

1. 판단 기준 — 이 프로젝트의 현실

항목 현재 상황 의미
데이터 규모 best_record 1,206,768행, 서비스 약 7년 (2019년 데이터 존재) → 연 약 17만 행 증가(예상) DB 입장에서는 소규모. 10년 뒤에도 수백만 행 수준
쓰기 부하 게임 1판 종료 시 기록 1건 저장 쓰기가 병목일 가능성 낮음
읽기 부하 게임 결과 화면 랭킹 3종, 랭킹 화면 5초마다 재조회(Ranking.REFRESH_TIME_SEC = 5), 메뉴 N+1 읽기 방식이 병목
부하 패턴 수업 시간(학교 시간표)에 몰림 평균이 아니라 수업 시간대 최대치 기준으로 봐야 함
서버 구성 운영 DB 설정이 localhost → Apache·PHP·MariaDB가 같은 EC2에서 동작하는 것으로 추정 웹과 DB가 CPU·메모리를 나눠 씀
운영 인력 1인, DB 최적화 경험 적음 운영할 구성 요소가 늘어나는 것 자체가 큰 비용
코드 구조 PHP 7 절차형 코드, composer 없음, 요청마다 new mysqli 외부 라이브러리 도입 시 설치·배포 방식부터 바꿔야 함

평가 기준: ① 실제로 느린 원인을 해결하는가 ② 추가로 운영해야 할 것이 늘어나는가 ③ 문제가 생겼을 때 혼자 복구할 수 있는가 ④ 비용


2. 학교(선생님) 단위로 기록 테이블 쪼개기

2-1. 전제: 이 스키마의 "학교"

현재 스키마에는 학교 테이블이 없고, 학생(player)은 선생님(maestro)에게 속합니다. 따라서 "학교 단위 분할"은 실제로는 선생님 단위 분할입니다.

현재:  best_record  (모든 선생님의 기록 120만 행)
분할:  best_record_m1, best_record_m2, ..., best_record_m{MaestroID}

2-2. 성능에 도움이 될까?

도움이 되는 부분

  • 인덱스가 없는 지금 상태에서는 "선생님 123의 오늘 랭킹"을 구할 때 120만 행이 아니라 그 선생님 테이블만 훑으므로 빨라집니다.

하지만 인덱스가 같은 효과를 이미 준다

  • (MaestroID, AppID, RecordDateTime) 인덱스는 "선생님 123 → 앱 5 → 오늘" 구간으로 바로 이동합니다. 즉 인덱스는 DB가 자동으로 관리해 주는 "선생님별 칸막이" 입니다.
  • 테이블 크기에 따른 차이는 인덱스 트리 깊이 정도입니다. InnoDB는 한 페이지(16KB)에 수백 개의 키가 들어가므로, 120만 행이어도 3단계 정도면 원하는 위치에 도달합니다(예상). 작은 테이블과 비교해 페이지 1~2개를 더 읽는 차이이며, 이 페이지들은 대부분 메모리(buffer pool)에 올라가 있습니다.
  • 결론: 인덱스를 만든 뒤에는 테이블 분할의 추가 성능 이득이 거의 없습니다. 인덱스 없이 분할만 하면, 큰 학교의 테이블은 여전히 전체를 훑습니다.

2-3. 학교 수만큼 테이블이 늘어날 때의 문제점

선생님 수를 먼저 확인해 보세요.

SELECT COUNT(*) AS maestro_count FROM maestro;
SELECT COUNT(DISTINCT MaestroID) AS maestro_with_records FROM best_record;

-- 기록이 한쪽에 몰려 있는지 (상위 10명의 비중)
SELECT MaestroID, COUNT(*) AS cnt,
       ROUND(COUNT(*) * 100 / (SELECT COUNT(*) FROM best_record), 1) AS pct
FROM best_record
GROUP BY MaestroID
ORDER BY cnt DESC
LIMIT 10;

선생님이 N명이고 기록 테이블이 2종(best_record, typing_exam_record)이면 테이블은 2N개, 최고기록·집계 테이블까지 쪼개면 4~6N개가 됩니다.

영역 문제 구체적 증상
DB 서버 자원 테이블마다 파일(.ibd)과 메타데이터가 생김 table_open_cache, table_definition_cache, open_files_limit 한도에 걸리면 테이블을 열고 닫기를 반복해 오히려 느려짐. 데이터 사전 메모리 증가
스키마 변경 인덱스 1개 추가 = 테이블 수만큼 ALTER TABLE 수천 번 실행 중 일부만 실패하면 테이블마다 구조가 달라지는 상태(schema drift)가 생기고, 어떤 테이블이 다른지 추적해야 함
보안 테이블 이름은 ? 바인딩이 불가능 "SELECT ... FROM best_record_m" . $maestro_id 같은 문자열 연결이 모든 기록 쿼리에 생김 → SQL Injection 위험 지점이 오히려 늘어남. 숫자 검증·화이트리스트가 필수
권한 선생님 가입 시 CREATE TABLE 필요 웹 서비스 DB 계정에 DDL 권한을 줘야 함 (해킹 시 피해 범위 확대). 생성 실패 시 가입 처리 꼬임
전체 조회 불가 여러 테이블을 한 번에 조회하려면 UNION ALL을 테이블 수만큼 관리자 전체 통계, 앱별 전체 이용량, "전국 랭킹" 같은 기능이 사실상 불가능
데이터 이동 학생이 다른 선생님으로 옮기거나 선생님 계정을 합칠 때 테이블 간 행 이동 코드가 필요
외래 키 테이블마다 외래 키를 달아야 함 누락되면 무결성 보호가 테이블마다 제각각
백업·도구 mariadb-dump, information_schema 조회, phpMyAdmin 목록 테이블 수에 비례해 느려지고 사람이 보기 어려워짐
불균형 기록의 대부분이 소수의 큰 학교에 몰려 있으면 느린 선생님의 테이블은 여전히 크고, 작은 테이블 수천 개만 늘어남
코드 전체 수정 기록 테이블을 쓰는 모든 PHP 파일 현황 문서의 쿼리 전부를 동적 테이블 이름으로 바꿔야 함

장점도 있습니다

  • 선생님 계정 삭제·백업·복원이 테이블 단위로 쉬움 (현재 선생님 단위 백업/삭제 스크립트가 하는 일이 단순해짐)
  • 계약상 "학교 데이터를 물리적으로 분리해 달라"는 요구가 있으면 설명하기 쉬움

2-4. 변형안 비교

변형 방식 테이블 수 문제 동적 이름·전체 조회 문제 평가
A. 선생님마다 테이블 best_record_m123 매우 큼 있음 비권장
B. 선생님 그룹(해시)별 고정 개수 best_record_00 ~ _15 (MaestroID % 16) 없음 (16개 고정) 있음 인덱스보다 나은 점이 거의 없음
C. MariaDB 내장 파티셔닝 (선생님 기준) PARTITION BY KEY(MaestroID) PARTITIONS 16 없음 (DB가 관리) 없음 (테이블 이름 하나) 가장 나은 형태지만, 안4의 제약(외래 키 불가, 기본 키 변경) 동일. PlayerID만으로 조회하는 쿼리는 모든 파티션 탐색
D. 선생님(학교)마다 DB 분리 chocomae_school_123 데이터베이스 매우 큼 (DB 단위) 있음 + 연결 전환 대형 기관 전용 서비스(SaaS 격리 요구)일 때만
E. 연도별 테이블 best_record_2025 작음 (연 1개) 있음 (연도 넘는 조회) 안3 아카이빙이 같은 목적을 더 안전하게 달성

2-5. 결론

  • 현재 규모와 운영 여건에서는 비권장합니다. 인덱스가 같은 효과를 거의 비용 없이 주고, 테이블 수 증가로 생기는 문제(스키마 변경, 보안, 전체 조회 불가)가 훨씬 큽니다.
  • 재검토 조건: 교육청·대형 학교와 "학교별 데이터 물리적 격리 및 삭제 증명"을 계약해야 할 때 → 테이블 분할이 아니라 D(학교별 DB) 또는 학교별 DB 서버를 별도 서비스 구조로 설계.

3. 다른 저장소를 함께 쓰기

3-1. Redis — 랭킹에 잘 맞는 자료구조

무엇인가

메모리에 데이터를 저장하는 초고속 키-값 저장소입니다. 특히 Sorted Set(점수 순으로 자동 정렬되는 집합)이 랭킹과 정확히 맞습니다.

적용 모습

키:     rank:{MaestroID}:{AppID}:hour:2026091413
        rank:{MaestroID}:{AppID}:day:20260914
        rank:{MaestroID}:{AppID}:month:202609
멤버:   PlayerID
점수:   기록
# 기록 저장 시 (MariaDB 저장 후) — 기존 점수보다 높을 때만 갱신 (Redis 6.2+의 GT 옵션)
ZADD rank:123:5:hour:2026091413  GT 15460 "9876"
ZADD rank:123:5:day:20260914     GT 15460 "9876"
ZADD rank:123:5:month:202609     GT 15460 "9876"
EXPIRE rank:123:5:hour:2026091413 172800      # 2일 뒤 자동 삭제

# 낮을수록 좋은 앱(105)은 LT 옵션, 조회 방향도 반대

# 랭킹 조회 — 상위 30명
ZREVRANGE rank:123:5:day:20260914 0 29 WITHSCORES
  • 조회는 MariaDB 쿼리 없이 메모리에서 끝나므로, 랭킹 화면의 5초 폴링이 수십 개 열려 있어도 부담이 거의 없습니다.
  • 랭킹 키에 들어가는 학생 수가 작아 메모리는 수십 MB 수준(예상)입니다.

문제점

문제 설명 대응
이중 쓰기 정합성 MariaDB 저장은 성공, Redis 저장은 실패하면 랭킹이 틀어짐 MariaDB를 원본으로 두고 Redis는 "다시 만들 수 있는 사본"으로 취급. 재구축 스크립트 필요
재시작 시 데이터 메모리 저장이라 설정에 따라 재시작 시 사라짐 RDB/AOF 영속화 설정 또는 시작 시 재구축
과거 날짜 랭킹 ranking.js의 이전 날짜 버튼으로 오래된 랭킹을 볼 수 있음. 만료된 키는 없음 오래된 날짜는 MariaDB로 조회 → 조회 경로가 2개 유지
기록 삭제 관리자가 기록을 지우면, Sorted Set에는 "그 다음으로 좋은 기록"이 없음 해당 기간을 MariaDB에서 다시 계산해 Redis 갱신
학생 이름 멤버는 PlayerID만 저장 이름은 MariaDB에서 조회하거나 Redis Hash에 별도 저장
PHP 연동 phpredis 확장 설치(Docker/서버 이미지 수정) 또는 Predis 라이브러리(현재 composer 미사용) 배포 방식 변경 필요
운영 서비스 1개 추가, 메모리 제한, 비밀번호 설정, 외부 포트 노출 금지 AWS ElastiCache를 쓰면 운영 부담은 줄지만 비용 증가

평가

  • 랭킹 화면 반복 조회라는 한 가지 문제에는 매우 효과적입니다.
  • 하지만 안5의 랭킹 캐시(DB 캐시 테이블)나 5장의 폴링 개선으로 비슷한 효과를 추가 구성 요소 없이 얻을 수 있습니다.
  • 검토 시점: 동시 접속 학급 수가 크게 늘어 캐시 테이블 조회조차 부담이 될 때, 또는 세션·실시간 기능 등 Redis를 쓸 다른 이유가 함께 생길 때.

3-2. MongoDB — 문서형 DB로 기록 이전

무엇인가

표(행·열) 대신 JSON 형태 문서를 저장하는 DB입니다. 스키마를 유연하게 바꿀 수 있고, 시계열 데이터용 Time Series Collection(5.0+)도 있습니다.

적용 모습

// 방법 1: 학생 문서에 기록 배열을 계속 추가 → ❌ 안티패턴
{ playerId: 9876, records: [ {app:5, rec:15460, at:"..."}, ... 수천  ] }
// 문서 1개 최대 16MB 제한, 배열이 커질수록 수정이 느려짐

// 방법 2: 학생·앱·날짜별 묶음 문서 (버킷 패턴)
{ maestroId: 123, playerId: 9876, appId: 5, date: "2026-09-14",
  best: 15460, hours: [ {h:13, best:15460}, {h:14, best:14200} ] }

방법 2는 사실상 안2의 일별 집계 테이블과 같은 구조입니다.

성능에 도움이 될까?

  • 랭킹 = "선생님·앱·기간 범위 조회 → 학생별 최고값 → 정렬". MongoDB에서도 {maestroId, appId, date} 복합 인덱스와 aggregation $group, $sort가 필요합니다. 해야 하는 일이 같습니다.
  • 즉 빨라지는 이유는 MongoDB라서가 아니라 인덱스·미리 집계 때문이며, 그건 MariaDB에서도 똑같이 됩니다.

문제점

문제 설명
DB 간 조인 불가 학생 이름(player)은 MariaDB에 있음 → PHP에서 두 DB 결과를 합쳐야 함
트랜잭션 분리 기록 저장(MongoDB)과 학생 삭제(MariaDB)를 한 트랜잭션으로 묶을 수 없음 → 고아 기록
이전 작업 120만+13만 행 이관, 기록 관련 PHP 코드 전면 재작성
PHP 연동 mongodb 확장 + 공식 라이브러리(composer) 필요
자원 기본 캐시가 메모리를 많이 씀 → 같은 EC2에서 MariaDB와 메모리 경쟁
운영 백업·복원·모니터링 체계를 하나 더 익히고 유지
라이선스·비용 자체 설치는 SSPL 라이선스, 관리형(Atlas)은 유료

평가

비권장. 문제를 해결하는 핵심(인덱스·집계)은 그대로 필요하고, 비용과 위험만 추가됩니다. MongoDB가 빛나는 경우(구조가 제각각인 데이터, 대량 분산 쓰기)와 이 서비스의 특성이 맞지 않습니다.

3-3. Kafka — 이벤트 스트리밍

무엇인가

대량의 이벤트를 순서대로 받아 여러 소비자에게 전달하는 메시지 시스템입니다. "초당 수만 건의 쓰기를 받아 두었다가 천천히 처리"하는 데 강합니다.

적용 모습

게임 종료 → PHP가 "기록 이벤트" 발행 → Kafka
                                         ├→ 소비자 1: MariaDB에 저장
                                         ├→ 소비자 2: 랭킹 집계 갱신
                                         └→ 소비자 3: 통계

성능에 도움이 될까?

  • 아니오. 이 서비스의 병목은 읽기(랭킹·히스토리 조회)입니다. Kafka는 쓰기를 흡수하는 도구이며 읽기 쿼리를 빠르게 하지 않습니다.

문제점

문제 설명
방금 저장한 기록이 늦게 보임 저장이 비동기가 되어, 결과 화면이 랭킹을 조회할 때 아직 반영되지 않았을 수 있음. 현재 결과 화면은 기록 저장 후 0.5초만 기다렸다가(RecordBoard.DELAY_UPDATING_RESULT_RECORD_MS = 500) 랭킹을 조회하므로, 처리가 조금만 밀려도 "방금 낸 기록이 랭킹에 없음"이 바로 드러남
자원 JVM 기반, 브로커에 수 GB 메모리 권장 → 단일 EC2에 부담
운영 복잡도 토픽·파티션·소비자 그룹·재처리·중복 처리(exactly-once) 이해 필요
PHP 연동 rdkafka 확장 설치
장애 지점 Kafka가 멈추면 기록 저장 전체가 멈춤

더 가벼운 대안 (나중에 비동기 처리가 필요해지면)

  • MariaDB 테이블을 작업 대기열로 쓰고 cron이 처리
  • Redis Streams (Redis를 이미 도입했다면)

평가

부적합. 규모·문제 유형·운영 여건 모두 맞지 않습니다.

3-4. 분석 전용 DB (ClickHouse, MariaDB ColumnStore, DuckDB)

무엇인가

데이터를 열(column) 단위로 저장해 수억 행 집계를 초 단위로 처리하는 DB입니다.

평가

  • 현재 기능(선생님별 랭킹, 학생별 히스토리, 기록 1건 저장·삭제)은 소량을 자주 읽고 쓰는 작업(OLTP) 이라 맞지 않습니다. 열 저장 DB는 1건 수정·삭제가 느리고 작은 쿼리에 비효율적입니다.
  • 검토 시점: "전국 학생 월별 타자 속도 변화", "앱별 연간 이용 통계" 같은 관리자 분석 대시보드를 만들 때. 이때도 운영 DB는 그대로 두고, 밤마다 복사한 데이터를 DuckDB(설치 없이 파일 하나로 동작) 등으로 분석하는 방식이 가장 가볍습니다.

3-5. 오래된 기록을 S3 파일로 보관

무엇인가

안3 아카이빙의 변형입니다. 오래된 기록을 DB 테이블이 아니라 S3에 파일(CSV/Parquet)로 저장하고 운영 DB에서 삭제합니다. 필요하면 Amazon Athena로 SQL 조회합니다(조회량 기준 과금).

장점 단점
DB 용량·백업 크기가 실제로 줄어듦 과거 날짜 랭킹, 오래 쉰 학생의 히스토리를 즉시 보여줄 수 없음 (Athena는 초~수십 초, PHP 연동 복잡)
저장 비용이 매우 저렴 AWS 권한·버킷 관리, 파일 형식·경로 규칙 설계
수년 치 보관에 적합 "최근 7일 히스토리 유지" 요구사항은 안2 집계 테이블이 선행되어야 만족

검토 시점: 안2·안3을 적용해 몇 년 운영한 뒤, 아카이브 테이블 자체가 부담이 되고 "N년 이전 기록은 화면에서 안 보여도 된다"는 정책이 정해졌을 때.


4. MariaDB 실행 환경 변경

인덱스·쿼리를 고치기 전에 서버가 제대로 설정되어 있는지 확인하는 것이 가장 먼저입니다. 설정이 기본값이면, 코드 수정 없이 크게 좋아질 수 있습니다.

4-1. 먼저 측정할 것

서버 (EC2에 SSH 접속 후)

nproc
free -h
df -h
docker stats --no-stream
TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 60") && curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/instance-type
  • 인스턴스 타입이 t2/t3/t4g 계열이면 AWS 콘솔 → CloudWatch → EC2 → CPUCreditBalance 그래프를 수업 시간대 기준으로 확인합니다.
  • AWS 콘솔 → EC2 → 볼륨에서 EBS 타입(gp2/gp3)과 크기를 확인합니다.
  • MariaDB가 Docker 컨테이너라면 docker inspect <컨테이너> | grep -i memory로 메모리 제한이 걸려 있는지 확인합니다.

MariaDB

SELECT @@version, @@innodb_buffer_pool_size / 1024 / 1024 AS buffer_pool_mb,
       @@max_connections, @@tmp_table_size / 1024 / 1024 AS tmp_table_mb,
       @@max_heap_table_size / 1024 / 1024 AS heap_table_mb,
       @@table_open_cache, @@innodb_flush_log_at_trx_commit;

-- buffer pool 적중률: 디스크에서 읽은 비율이 1%를 넘으면 메모리 부족 의심
SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read%';
-- Innodb_buffer_pool_reads(디스크) / Innodb_buffer_pool_read_requests(전체)

-- GROUP BY 등에서 디스크 임시 테이블이 많이 만들어지는지
SHOW GLOBAL STATUS LIKE 'Created_tmp%';

-- 연결 상황
SHOW GLOBAL STATUS LIKE 'Threads_connected';
SHOW GLOBAL STATUS LIKE 'Max_used_connections';
SHOW GLOBAL STATUS LIKE 'Connections';
  • 데이터+인덱스 크기는 개요 7-1 쿼리로 확인합니다.

4-2. 설정 튜닝 (서버는 그대로)

설정 MariaDB 기본값 의미 조정 방향
innodb_buffer_pool_size 128MB 테이블·인덱스를 메모리에 올려두는 공간 전체 데이터+인덱스 크기보다 크게. DB 전용 서버면 RAM의 5070%, 웹과 같은 서버면 Apache/PHP 사용량을 빼고 여유 있게 (예: RAM 4GB면 11.5GB 수준에서 시작, 예상)
tmp_table_size, max_heap_table_size 16MB GROUP BY·정렬 임시 테이블을 메모리에서 처리할 크기 Created_tmp_disk_tables가 많으면 둘을 같이 올림 (예: 64MB)
innodb_log_file_size 96MB 쓰기 로그 크기 쓰기가 적어 대개 그대로
innodb_flush_log_at_trx_commit 1 커밋마다 디스크 동기화 (가장 안전) 2로 바꾸면 쓰기가 빨라지지만 서버 전원 장애 시 최근 1초 기록 유실 가능 → 게임 기록 특성상 고려 가능하나, 쓰기가 병목이 아니므로 우선순위 낮음
max_connections 151 동시 연결 수 Max_used_connections가 가까우면 조정. 너무 크면 메모리 과다
slow_query_log, long_query_time 꺼짐 느린 쿼리 기록 켜두고 주기적으로 확인
  • buffer pool이 기본값 128MB인데 기록 테이블+인덱스가 그보다 크다면, 현재 느린 원인의 상당 부분이 "매번 디스크에서 읽기"일 수 있습니다. 이 경우 설정 한 줄로 큰 개선이 가능합니다.
  • MariaDB 10.11은 SET GLOBAL innodb_buffer_pool_size = ...재시작 없이 크기를 바꿀 수 있습니다. 확인 후 설정 파일(my.cnf 또는 Docker 볼륨의 설정)에도 반영해야 재시작 후 유지됩니다.
  • 메모리를 너무 크게 잡으면 Apache/PHP와 경쟁해 서버 전체가 스왑을 쓰며 더 느려집니다. free -h로 여유를 확인하며 단계적으로 올리세요.

4-3. 서버 사양·스토리지

점검 항목 문제가 되는 경우 해결
CPU 크레딧 (t 계열) 평소엔 괜찮다가 수업 시간에만 느림, CPUCreditBalance가 0 근처 t 계열 "무제한(Unlimited)" 모드, 한 단계 큰 인스턴스, 또는 크레딧 없는 계열(m/c 계열)
메모리 buffer pool을 충분히 줄 수 없음, 스왑 사용 메모리 큰 인스턴스로 변경 (EC2는 중지 → 타입 변경 → 시작으로 수 분 내 가능)
EBS gp2 볼륨이 작으면 기본 IOPS가 낮고 크레딧 방식 gp3로 온라인 변경(재시작 없음). gp3는 크기와 무관하게 기본 3,000 IOPS 제공, 일반적으로 gp2보다 저렴
디스크 여유 안1 인덱스 추가, OPTIMIZE TABLE에 공간 필요 볼륨 크기 온라인 확장
ARM(Graviton, t4g/m7g) 같은 성능에 저렴한 경우 많음 Docker 이미지·PHP 확장의 arm64 지원 확인 필요 → 이전 작업이 따름

인스턴스·스토리지 변경은 코드 수정이 없고 되돌리기 쉬운 해결책이지만, 비용이 매달 발생합니다. 정확한 금액은 AWS 요금 계산기로 확인하세요.

4-4. DB를 별도 서버로 분리

장점 단점
웹(Apache/PHP)과 DB가 CPU·메모리를 두고 경쟁하지 않음 서버 비용 추가
DB 서버 메모리를 buffer pool에 넉넉히 할당 네트워크 왕복 추가 (같은 가용 영역이면 1ms 미만, 예상). 메뉴 N+1처럼 쿼리 수가 많은 화면은 누적 지연이 커짐 → 안5와 함께
웹 서버를 늘리거나 교체하기 쉬움 보안 그룹·연결 설정·백업 대상 변경

검토 시점: 4-1 측정에서 수업 시간대 CPU·메모리가 웹과 DB 모두 높게 나올 때.

4-5. Amazon RDS for MariaDB (관리형 DB)로 이전

장점 단점
자동 백업 + 특정 시점 복구(PITR): 현재 NAS 백업 스크립트 대체 가능 비용 (같은 사양 EC2보다 비쌈)
보안 패치·버전 업그레이드 자동화 SUPER 권한 없음, 설정은 파라미터 그룹으로만
Performance Insights로 느린 쿼리를 그래프로 확인 (DB 경험이 적을 때 큰 도움) 이전 작업: 덤프·복원 중 점검 시간 또는 AWS DMS 설정
스토리지 자동 확장, Multi-AZ(장애 시 자동 전환), 읽기 복제본 클릭 생성 RDS MariaDB 10.11 지원 여부·마이너 버전 확인 필요
DB 서버 분리 효과(4-4) 포함 배포 스크립트의 DB 호스트 변경, 연결 암호화 설정
  • Aurora는 MySQL/PostgreSQL 호환이며 MariaDB 호환이 아닙니다. MariaDB 전용 문법을 쓰는 곳이 있으면 수정이 필요하므로, 이 프로젝트에는 RDS for MariaDB가 자연스럽습니다.
  • RDS 자체가 느린 쿼리를 빠르게 해주지는 않습니다. 목적은 성능보다 운영 부담 감소입니다. 인덱스 없는 쿼리는 RDS에서도 느립니다.

검토 시점: 백업·복구·패치·모니터링을 혼자 챙기기 부담스러울 때, 또는 서버 이전 계획이 있을 때.

4-6. 읽기 복제본 (Read Replica)

  • 기록 저장은 원본 DB, 랭킹·관리자 조회는 복제본으로 보내 부하를 나눕니다.
  • 복제 지연(보통 1초 미만이지만 부하 시 늘어남) 때문에 방금 저장한 기록이 랭킹에 늦게 보일 수 있습니다 → 결과 화면 랭킹은 원본, 랭킹 화면 폴링·관리자 목록만 복제본으로 보내는 식의 구분이 필요합니다.
  • PHP에서 연결을 2개로 나누는 코드 수정이 필요합니다.
  • 현재 규모에서는 과함. 인덱스와 폴링 개선이 먼저입니다.

4-7. DB 버전·제품 변경

선택 이 문제와의 관계 평가
MariaDB 11.x로 업그레이드 옵티마이저 비용 모델 개선 등 전반적 향상. 하지만 컬럼에 함수를 씌운 조건은 버전을 올려도 인덱스를 못 씀 문제 해결책은 아님. 장기 지원 버전 계획에 따라
MySQL 8.0으로 이전 8.0.13+의 함수 인덱스(INDEX ((DATE(RecordDateTime))))로 코드 수정 없이 일부 조건에 인덱스 사용 가능. 단 DATE, HOUR, YEAR, MONTH 조합마다 인덱스가 필요하고 식이 정확히 일치해야 함 이전 비용·호환성 위험이 안1 쿼리 수정보다 훨씬 큼. 비권장
MariaDB 10.11 가상 컬럼 인덱스 RecordDate DATE AS (DATE(RecordDateTime)) VIRTUAL + 인덱스. 쿼리가 RecordDate 컬럼을 직접 조건에 써야 함 결국 쿼리 수정 필요 → 안1의 범위 조건이 더 단순
PostgreSQL BRIN 인덱스(시간순 대용량에 매우 작은 인덱스), 부분 인덱스 등 강력 전면 이전. 현재 규모에서 이득 대비 비용 과다

5. 애플리케이션·제품 관점의 해결책

DB를 바꾸지 않고 요청 자체를 줄이거나 가볍게 만드는 방법입니다.

5-1. 랭킹 화면 5초 폴링 개선 (추천)

현재 ranking.js는 화면이 열려 있는 동안 기록이 바뀌지 않아도 5초마다 랭킹 전체를 다시 요청합니다(game.time.events.loop). 화면 1개를 1시간 열어 두면 720회입니다.

방법 내용 효과 난이도
A. 주기 늘리기 5초 → 15~30초 요청 3~6배 감소 매우 쉬움 (상수 1개)
B. 화면이 안 보일 때 멈춤 브라우저 탭이 숨겨지면(document.hidden) 요청 중단 열어두고 방치한 화면 요청 제거 쉬움
C. 변경 확인 후 가져오기 "이 선생님·앱의 마지막 기록 저장 시각"만 가벼운 API로 확인 → 바뀌었을 때만 랭킹 조회 수업 중이 아닐 때 랭킹 쿼리 거의 0 중간
D. 서버 푸시 (SSE/WebSocket) 기록 저장 시 서버가 화면에 알림 가장 즉각적 어려움 (Apache+PHP 구조와 맞지 않음) → 비권장

C의 예시: 마지막 저장 시각은 안2 집계 테이블UpdatedDateTime이나, 선생님·앱별 "마지막 기록 시각" 1행을 관리하는 작은 테이블로 확인합니다.

SELECT MAX(UpdatedDateTime) FROM daily_best_record
WHERE MaestroID = ? AND AppID = ? AND RecordDate = CURDATE();

클라이언트는 이전 값과 같으면 랭킹 요청을 건너뜁니다. A+B만 해도 코드 몇 줄로 반복 조회가 크게 줄어듭니다.

5-2. 랭킹 결과를 파일로 미리 만들어 두기

  • 기록 저장 시 해당 선생님·앱의 랭킹 JSON 파일을 다시 만들고, 화면은 Apache가 파일을 그대로 내려줍니다(PHP·DB 실행 없음).
  • 빠르지만 파일 쓰기 권한, 동시 쓰기 충돌, 오래된 파일 정리, 배포 시 파일 보존 문제가 생깁니다. 안5 캐시 테이블이 더 관리하기 쉽습니다.

5-3. PHP 실행 환경

점검 현재 개선 효과
DB 연결 connect_db.php가 요청마다 new mysqli + USE chocomae 쿼리 추가 실행 이미 DB 이름을 지정해 연결하므로 USE 쿼리 삭제. 영속 연결(호스트에 p: 접두사) 검토 요청마다 연결·왕복 1회씩 절약. 5초 폴링처럼 작은 요청이 많을 때 체감
OPcache 확인 필요 (php -i | grep opcache.enable) 켜져 있지 않으면 활성화 PHP 파일 해석 비용 제거
Apache MPM 확인 필요 prefork + mod_php는 요청당 메모리가 큼 → PHP-FPM 전환 검토 같은 메모리로 더 많은 동시 요청, DB에 메모리 양보

영속 연결은 연결 수가 max_connections에 가깝게 쌓일 수 있고, 트랜잭션·임시 변수가 다음 요청으로 넘어갈 수 있으므로 스테이징에서 먼저 확인하세요.

5-4. 기능 정책 조정

가장 강력한 최적화는 필요 없는 일을 하지 않는 것입니다.

정책 효과
랭킹 화면 과거 날짜 탐색 범위를 최근 1년으로 제한 오래된 기록 조회 경로 제거 → 안3 아카이빙이 단순해짐
관리자 기록 목록 "전체 보기" 제거 또는 최대 기간 제한 대량 조회 제거
시간 랭킹을 "최근 1주일"만 제공 원본 테이블의 조회 범위 축소
일정 기간 미사용 선생님 계정의 기록 정리 정책 (이용약관·안내 필요) 데이터 총량 감소

선생님(사용자)에게 실제로 필요한 기능인지 확인한 뒤 결정하세요.


6. 종합 비교

방법 해결하는 것 예상 효과 추가 운영 부담 월 비용 난이도 되돌리기
MariaDB 설정 튜닝 디스크 읽기, 임시 테이블 중~대 (측정에 따라) 없음 0 쉬움
랭킹 폴링 개선 (A+B, C) 반복 조회 대 (랭킹 부하) 없음 0 하~중 쉬움
PHP 연결·OPcache 요청당 오버헤드 소~중 없음 0 쉬움
인스턴스·gp3 점검 CPU 크레딧, IOPS 상황에 따라 대 없음 증가 가능 쉬움
안1 (인덱스+쿼리) 전체 스캔 없음 0 쉬움
Redis 랭킹 랭킹 조회 대 (랭킹만) Redis 운영 소 (자체) / 중 (ElastiCache) 중간
DB 서버 분리 자원 경쟁 서버 1대 증가 중간
RDS 이전 운영 부담 (백업·패치·모니터링) 성능은 소 감소 증가 어려움
읽기 복제본 읽기 분산 복제 관리 증가 중~상 중간
S3 보관 장기 용량 용량 대 AWS 관리 매우 소 중~상 중간
학교별 테이블 (인덱스와 같은 효과) 인덱스 대비 거의 0 매우 큼 0 매우 어려움
MongoDB (인덱스·집계와 같은 효과) 거의 0 증가 매우 어려움
Kafka 쓰기 흡수 (현재 문제 아님) 0 매우 큼 증가 어려움
분석 DB 대규모 통계 (현재 기능 없음) 0 증가 중간

7. 추천 실행 순서 (안1~안5와 통합)

순서 작업 이유
1 4-1 환경 측정 + 개요 7장 쿼리 측정 원인이 서버 설정·사양인지, 쿼리인지 먼저 구분
2 4-2 설정 튜닝 (buffer pool이 기본값이면 즉시), 4-3 CPU 크레딧·gp3 확인 코드 수정 없이 효과, 되돌리기 쉬움
3 5-1 A+B 폴링 주기·숨김 시 중단, 5-3 USE 쿼리 제거·OPcache 확인 코드 몇 줄
4 안1 + 안5 보안 항목 쿼리 자체의 근본 개선
5 재측정 → 부족하면 5-1 C 또는 안5 랭킹 캐시 반복 조회가 여전히 많을 때
6 안2·안3 장기 성장 대비
7 (선택) RDS 이전 운영 부담을 줄이고 싶을 때
8 (조건부) Redis, DB 분리 8장 조건 충족 시

8. 다른 기술을 다시 검토할 시점

신호 검토할 방법
안1·튜닝 후에도 수업 시간대 DB CPU가 계속 높고, 슬로우 쿼리 대부분이 랭킹 Redis 랭킹, 폴링 방식 C
웹과 DB가 같은 서버에서 메모리 부족·스왑 발생 DB 서버 분리 또는 RDS
백업 실패·복구 경험 부족이 걱정됨, 장애 대응을 혼자 하기 어려움 RDS (자동 백업·PITR·모니터링)
동시 접속 학급이 현재의 10배 이상으로 증가 읽기 복제본, Redis, 웹 서버 여러 대
교육청·대형 기관과 학교별 데이터 격리 계약 학교별 DB(2-4 D) 구조 설계
관리자용 대규모 통계·리포트 기능 기획 분석 DB(DuckDB/ClickHouse)로 야간 복사
수년 치 아카이브가 부담되고 과거 기록 화면 조회 불필요 정책 확정 S3 보관

9. 조사 중 확인한 운영 환경 관련 점검 사항

항목 내용 권장
DB 포트 외부 접근 일일 백업 스크립트가 외부(NAS)에서 운영 DB 3306 포트로 직접 접속 AWS 보안 그룹에서 3306 포트를 NAS의 공인 IP로만 허용하는지 확인. 가능하면 SSH 터널 사용
요청마다 USE chocomae connect_db.php 5-3 참고, 불필요한 쿼리
DB 계정 정보가 코드에 포함 connect_db.php에 설정 파일이 없을 때 쓰는 기본 계정 정보가 들어 있음 저장소에서 제거하고 운영 비밀번호가 같다면 변경
개인 키 파일이 저장소에 포함 저장소 루트의 jinaju.pem(RSA 개인 키)이 git으로 추적되고 있음 서버 접속용 키라면 새 키로 교체 후 기존 키 폐기, 저장소에서 제거하고 .gitignore에 추가. git 이력에도 남아 있으므로 원격 저장소 접근 권한자 확인