DB 개선 계획 작성
This commit is contained in:
@@ -0,0 +1,524 @@
|
||||
# 기록 DB 성능 문제의 다른 해결 방법 연구
|
||||
|
||||
> [개요 문서](01-improvement-overview.md) | 작성일: 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`)에게 속합니다. 따라서 "학교 단위 분할"은 실제로는 **선생님 단위 분할**입니다.
|
||||
|
||||
```text
|
||||
현재: 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. 학교 수만큼 테이블이 늘어날 때의 문제점
|
||||
|
||||
선생님 수를 먼저 확인해 보세요.
|
||||
|
||||
```sql
|
||||
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 파일 | [현황 문서](player-record-tables-and-queries.md)의 쿼리 전부를 동적 테이블 이름으로 바꿔야 함 |
|
||||
|
||||
**장점도 있습니다**
|
||||
|
||||
- 선생님 계정 삭제·백업·복원이 테이블 단위로 쉬움 (현재 [선생님 단위 백업/삭제 스크립트](../../db/260907-backup-delete-maestro/backup-maestro.sh)가 하는 일이 단순해짐)
|
||||
- 계약상 "학교 데이터를 물리적으로 분리해 달라"는 요구가 있으면 설명하기 쉬움
|
||||
|
||||
### 2-4. 변형안 비교
|
||||
|
||||
| 변형 | 방식 | 테이블 수 문제 | 동적 이름·전체 조회 문제 | 평가 |
|
||||
|---|---|---|---|---|
|
||||
| A. 선생님마다 테이블 | `best_record_m123` | **매우 큼** | 있음 | 비권장 |
|
||||
| B. 선생님 그룹(해시)별 고정 개수 | `best_record_00` ~ `_15` (MaestroID % 16) | 없음 (16개 고정) | 있음 | 인덱스보다 나은 점이 거의 없음 |
|
||||
| C. MariaDB 내장 파티셔닝 (선생님 기준) | `PARTITION BY KEY(MaestroID) PARTITIONS 16` | 없음 (DB가 관리) | **없음** (테이블 이름 하나) | 가장 나은 형태지만, [안4](05-option4-partitioning.md)의 제약(외래 키 불가, 기본 키 변경) 동일. `PlayerID`만으로 조회하는 쿼리는 모든 파티션 탐색 |
|
||||
| D. 선생님(학교)마다 DB 분리 | `chocomae_school_123` 데이터베이스 | 매우 큼 (DB 단위) | 있음 + 연결 전환 | 대형 기관 전용 서비스(SaaS 격리 요구)일 때만 |
|
||||
| E. 연도별 테이블 | `best_record_2025` | 작음 (연 1개) | 있음 (연도 넘는 조회) | [안3 아카이빙](04-option3-archiving.md)이 같은 목적을 더 안전하게 달성 |
|
||||
|
||||
### 2-5. 결론
|
||||
|
||||
- **현재 규모와 운영 여건에서는 비권장**합니다. 인덱스가 같은 효과를 거의 비용 없이 주고, 테이블 수 증가로 생기는 문제(스키마 변경, 보안, 전체 조회 불가)가 훨씬 큽니다.
|
||||
- **재검토 조건**: 교육청·대형 학교와 "학교별 데이터 물리적 격리 및 삭제 증명"을 계약해야 할 때 → 테이블 분할이 아니라 **D(학교별 DB) 또는 학교별 DB 서버**를 별도 서비스 구조로 설계.
|
||||
|
||||
---
|
||||
|
||||
## 3. 다른 저장소를 함께 쓰기
|
||||
|
||||
### 3-1. Redis — 랭킹에 잘 맞는 자료구조
|
||||
|
||||
#### 무엇인가
|
||||
|
||||
메모리에 데이터를 저장하는 초고속 키-값 저장소입니다. 특히 **Sorted Set**(점수 순으로 자동 정렬되는 집합)이 랭킹과 정확히 맞습니다.
|
||||
|
||||
#### 적용 모습
|
||||
|
||||
```text
|
||||
키: rank:{MaestroID}:{AppID}:hour:2026091413
|
||||
rank:{MaestroID}:{AppID}:day:20260914
|
||||
rank:{MaestroID}:{AppID}:month:202609
|
||||
멤버: PlayerID
|
||||
점수: 기록
|
||||
```
|
||||
|
||||
```text
|
||||
# 기록 저장 시 (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](../../../src/game/ranking/ranking.js)의 이전 날짜 버튼으로 오래된 랭킹을 볼 수 있음. 만료된 키는 없음 | 오래된 날짜는 MariaDB로 조회 → **조회 경로가 2개** 유지 |
|
||||
| **기록 삭제** | 관리자가 기록을 지우면, Sorted Set에는 "그 다음으로 좋은 기록"이 없음 | 해당 기간을 MariaDB에서 다시 계산해 Redis 갱신 |
|
||||
| 학생 이름 | 멤버는 PlayerID만 저장 | 이름은 MariaDB에서 조회하거나 Redis Hash에 별도 저장 |
|
||||
| PHP 연동 | `phpredis` 확장 설치(Docker/서버 이미지 수정) 또는 Predis 라이브러리(현재 composer 미사용) | 배포 방식 변경 필요 |
|
||||
| 운영 | 서비스 1개 추가, 메모리 제한, 비밀번호 설정, **외부 포트 노출 금지** | AWS ElastiCache를 쓰면 운영 부담은 줄지만 비용 증가 |
|
||||
|
||||
#### 평가
|
||||
|
||||
- **랭킹 화면 반복 조회**라는 한 가지 문제에는 매우 효과적입니다.
|
||||
- 하지만 [안5의 랭킹 캐시](06-option5-application-layer.md)(DB 캐시 테이블)나 5장의 **폴링 개선**으로 비슷한 효과를 추가 구성 요소 없이 얻을 수 있습니다.
|
||||
- **검토 시점**: 동시 접속 학급 수가 크게 늘어 캐시 테이블 조회조차 부담이 될 때, 또는 세션·실시간 기능 등 Redis를 쓸 다른 이유가 함께 생길 때.
|
||||
|
||||
### 3-2. MongoDB — 문서형 DB로 기록 이전
|
||||
|
||||
#### 무엇인가
|
||||
|
||||
표(행·열) 대신 JSON 형태 문서를 저장하는 DB입니다. 스키마를 유연하게 바꿀 수 있고, 시계열 데이터용 **Time Series Collection**(5.0+)도 있습니다.
|
||||
|
||||
#### 적용 모습
|
||||
|
||||
```javascript
|
||||
// 방법 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의 일별 집계 테이블](03-option2-daily-summary-tables.md)과 같은 구조**입니다.
|
||||
|
||||
#### 성능에 도움이 될까?
|
||||
|
||||
- 랭킹 = "선생님·앱·기간 범위 조회 → 학생별 최고값 → 정렬". 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 — 이벤트 스트리밍
|
||||
|
||||
#### 무엇인가
|
||||
|
||||
대량의 이벤트를 순서대로 받아 여러 소비자에게 전달하는 메시지 시스템입니다. "초당 수만 건의 쓰기를 받아 두었다가 천천히 처리"하는 데 강합니다.
|
||||
|
||||
#### 적용 모습
|
||||
|
||||
```text
|
||||
게임 종료 → 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 아카이빙](04-option3-archiving.md)의 변형입니다. 오래된 기록을 DB 테이블이 아니라 **S3에 파일(CSV/Parquet)로 저장**하고 운영 DB에서 삭제합니다. 필요하면 Amazon Athena로 SQL 조회합니다(조회량 기준 과금).
|
||||
|
||||
| 장점 | 단점 |
|
||||
|---|---|
|
||||
| DB 용량·백업 크기가 실제로 줄어듦 | 과거 날짜 랭킹, 오래 쉰 학생의 히스토리를 **즉시** 보여줄 수 없음 (Athena는 초~수십 초, PHP 연동 복잡) |
|
||||
| 저장 비용이 매우 저렴 | AWS 권한·버킷 관리, 파일 형식·경로 규칙 설계 |
|
||||
| 수년 치 보관에 적합 | "최근 7일 히스토리 유지" 요구사항은 [안2 집계 테이블](03-option2-daily-summary-tables.md)이 선행되어야 만족 |
|
||||
|
||||
**검토 시점**: 안2·안3을 적용해 몇 년 운영한 뒤, 아카이브 테이블 자체가 부담이 되고 "N년 이전 기록은 화면에서 안 보여도 된다"는 정책이 정해졌을 때.
|
||||
|
||||
---
|
||||
|
||||
## 4. MariaDB 실행 환경 변경
|
||||
|
||||
> 인덱스·쿼리를 고치기 전에 **서버가 제대로 설정되어 있는지** 확인하는 것이 가장 먼저입니다. 설정이 기본값이면, 코드 수정 없이 크게 좋아질 수 있습니다.
|
||||
|
||||
### 4-1. 먼저 측정할 것
|
||||
|
||||
#### 서버 (EC2에 SSH 접속 후)
|
||||
|
||||
```bash
|
||||
nproc
|
||||
```
|
||||
|
||||
```bash
|
||||
free -h
|
||||
```
|
||||
|
||||
```bash
|
||||
df -h
|
||||
```
|
||||
|
||||
```bash
|
||||
docker stats --no-stream
|
||||
```
|
||||
|
||||
```bash
|
||||
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
|
||||
|
||||
```sql
|
||||
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](01-improvement-overview.md#7-1-테이블인덱스-크기) 쿼리로 확인합니다.
|
||||
|
||||
### 4-2. 설정 튜닝 (서버는 그대로)
|
||||
|
||||
| 설정 | MariaDB 기본값 | 의미 | 조정 방향 |
|
||||
|---|---|---|---|
|
||||
| **`innodb_buffer_pool_size`** | **128MB** | 테이블·인덱스를 메모리에 올려두는 공간 | 전체 데이터+인덱스 크기보다 크게. DB 전용 서버면 RAM의 50~70%, **웹과 같은 서버면** Apache/PHP 사용량을 빼고 여유 있게 (예: RAM 4GB면 1~1.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](../../../src/game/ranking/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 집계 테이블](03-option2-daily-summary-tables.md)의 `UpdatedDateTime`이나, 선생님·앱별 "마지막 기록 시각" 1행을 관리하는 작은 테이블로 확인합니다.
|
||||
|
||||
```sql
|
||||
SELECT MAX(UpdatedDateTime) FROM daily_best_record
|
||||
WHERE MaestroID = ? AND AppID = ? AND RecordDate = CURDATE();
|
||||
```
|
||||
|
||||
클라이언트는 이전 값과 같으면 랭킹 요청을 건너뜁니다. A+B만 해도 코드 몇 줄로 반복 조회가 크게 줄어듭니다.
|
||||
|
||||
### 5-2. 랭킹 결과를 파일로 미리 만들어 두기
|
||||
|
||||
- 기록 저장 시 해당 선생님·앱의 랭킹 JSON 파일을 다시 만들고, 화면은 **Apache가 파일을 그대로** 내려줍니다(PHP·DB 실행 없음).
|
||||
- 빠르지만 파일 쓰기 권한, 동시 쓰기 충돌, 오래된 파일 정리, 배포 시 파일 보존 문제가 생깁니다. [안5 캐시 테이블](06-option5-application-layer.md)이 더 관리하기 쉽습니다.
|
||||
|
||||
### 5-3. PHP 실행 환경
|
||||
|
||||
| 점검 | 현재 | 개선 | 효과 |
|
||||
|---|---|---|---|
|
||||
| DB 연결 | [connect_db.php](../../../src/web/server/setup/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장](01-improvement-overview.md#7-사전-측정-절차-0단계) 쿼리 측정 | 원인이 서버 설정·사양인지, 쿼리인지 먼저 구분 |
|
||||
| 2 | **4-2 설정 튜닝** (buffer pool이 기본값이면 즉시), **4-3** CPU 크레딧·gp3 확인 | 코드 수정 없이 효과, 되돌리기 쉬움 |
|
||||
| 3 | **5-1 A+B** 폴링 주기·숨김 시 중단, **5-3** `USE` 쿼리 제거·OPcache 확인 | 코드 몇 줄 |
|
||||
| 4 | [안1](02-option1-index-and-query-rewrite.md) + [안5 보안 항목](06-option5-application-layer.md) | 쿼리 자체의 근본 개선 |
|
||||
| 5 | 재측정 → 부족하면 5-1 C 또는 [안5 랭킹 캐시](06-option5-application-layer.md) | 반복 조회가 여전히 많을 때 |
|
||||
| 6 | [안2](03-option2-daily-summary-tables.md)·[안3](04-option3-archiving.md) | 장기 성장 대비 |
|
||||
| 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](../../../src/web/server/setup/connect_db.php) | 5-3 참고, 불필요한 쿼리 |
|
||||
| DB 계정 정보가 코드에 포함 | `connect_db.php`에 설정 파일이 없을 때 쓰는 기본 계정 정보가 들어 있음 | 저장소에서 제거하고 운영 비밀번호가 같다면 변경 |
|
||||
| **개인 키 파일이 저장소에 포함** | 저장소 루트의 `jinaju.pem`(RSA 개인 키)이 git으로 추적되고 있음 | 서버 접속용 키라면 **새 키로 교체 후 기존 키 폐기**, 저장소에서 제거하고 `.gitignore`에 추가. git 이력에도 남아 있으므로 원격 저장소 접근 권한자 확인 |
|
||||
Reference in New Issue
Block a user