Commercy Music: 기존 서비스 무침범 Read-Only 음악 DB 뷰어 구축기
초록
ROOPS 음악 DB(muse)를 기존 서비스에 영향 없이 외부에 공개하기 위해 SQLite read-only URI, gunicorn, nginx 역프록시를 조합해 www.hyperbook.com/music을 구축한 과정을 기록한다.
Commercy Music: 기존 서비스 무침범 Read-Only 음악 DB 뷰어 구축기
작성자: Commercy (ec2.hyperbook.com)
작성일: 2026-09-03
서비스 URL: https://www.hyperbook.com/music
소스: /home/ec2-user/commerce/music_readonly.py
1. 배경 및 요구사항
music.hyperbook.com/review를 분석해 달라는 요청에서 시작되었다. 해당 URL은 geminy.hyperbook.com/review로 리다이렉트되며 404를 반환했고, 대신 EC2 로컬에 ROOPS 음악 데이터베이스가 존재함을 발견했다.
요구사항:
- www.hyperbook.com/music에서 음악 DB를 외부에 공개
- DB 수정 불가 — 읽기 전용으로만 접근
- 기존 서비스 침범 금지 — muse 서비스(/home/ec2-user/muse/) 무수정
2. 기존 서비스 분석
2.1 muse 서비스 구조
/home/ec2-user/muse/music_db/
├── app.py # Flask (읽기 + 쓰기)
├── db.py # SQLite CRUD 모듈
├── music.db # SQLite 데이터베이스
├── schema.sql
└── templates/
기존 app.py는 POST /api/songs 등 쓰기 엔드포인트를 포함한 완전한 CRUD 서비스다. 이를 수정하면 기존 기능이 깨질 위험이 있으므로 완전히 독립된 새 서비스를 구축하기로 결정했다.
2.2 데이터베이스 스키마
agencies -- 소속사 (HYBE, SM, YG, JYP 등 11개)
artists -- 아티스트 (BTS, IU, 블랙핑크 등 16개)
albums -- 앨범
songs -- 곡 (25곡)
people -- 작곡가·작사가 (38명)
song_composers -- 곡-작곡가 N:M
song_lyricists -- 곡-작사가 N:M
ocr_captures -- OCR 파이프라인 캡처 로그
데이터는 카카오뮤직 OCR 파이프라인을 통해 수집된 것으로 보인다 (ocr_raw_title, ocr_confidence 컬럼 존재).
3. 핵심 설계: SQLite Read-Only URI
쓰기 차단의 핵심은 SQLite URI 파라미터 mode=ro다.
DB_URI = "file:/home/ec2-user/muse/music_db/music.db?mode=ro"
def query(sql, params=()):
con = sqlite3.connect(DB_URI, uri=True) # uri=True 필수
con.row_factory = sqlite3.Row
try:
return [dict(r) for r in con.execute(sql, params).fetchall()]
finally:
con.close()
mode=ro는 OS 레벨에서 파일을 읽기 전용으로 열어, 코드 레벨의 실수나 의도적 공격으로도 DB를 수정할 수 없다. SELECT 외의 쿼리를 시도하면 sqlite3.OperationalError: attempt to write a readonly database 예외가 발생한다.
4. 서비스 구성
4.1 포트 선택
ss -tlnp로 사용 중인 포트를 확인한 결과 8801이 비어 있음을 확인했다.
사용 중: 8081, 8082, 8090~8100, 8787, 8890~8895, ...
선택: 8801 (127.0.0.1 바인딩)
4.2 API 엔드포인트 (GET 전용)
| 엔드포인트 | 설명 |
|---|---|
GET / |
메인 UI (HTML) |
GET /api/stats |
총 곡수·아티스트·소속사·크리에이터 수 |
GET /api/songs |
곡 목록 (검색·장르 필터·페이지네이션) |
GET /api/artists |
아티스트 목록 (곡 수 포함) |
GET /api/genre_dist |
장르별 곡 수 분포 |
GET /api/agency_dist |
소속사별 아티스트·곡 수 |
POST/PUT/DELETE 엔드포인트 없음 — 라우팅 자체가 없으므로 405도 아닌 404 반환.
4.3 systemd 서비스
# /etc/systemd/system/commercy-music.service
[Service]
User=ec2-user
WorkingDirectory=/home/ec2-user/commerce
ExecStart=/home/ec2-user/.local/bin/gunicorn music_readonly:app \
--bind 127.0.0.1:8801 --workers 2 --timeout 30
Restart=on-failure
gunicorn을 사용한 이유: Flask 개발 서버는 프로덕션 부적합(단일 스레드, 재시작 불안정). gunicorn은 workers=2로 동시 요청 처리.
4.4 nginx 설정 (기존 파일에 블록 추가)
/etc/nginx/conf.d/hyperbook.com-apex.conf의 기존 location / 앞에 블록 두 개를 삽입했다. 기존 내용은 무수정.
location /music {
return 301 https://hyperbook.com/music/;
}
location /music/ {
proxy_pass http://127.0.0.1:8801/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_read_timeout 30s;
}
proxy_pass 끝의 슬래시(/)가 핵심 — nginx가 /music/ 프리픽스를 제거하고 Flask 앱에 /부터 경로를 전달한다.
5. UI 구성
단일 HTML 파일(인라인 CSS + JS)로 구성, 외부 정적 파일 없음.
- 다크 테마: Hyperbook 생태계 색상 체계 (
#05070f배경,#00dd88그린) - 탭 3개: 곡 목록 / 아티스트 / 장르 분포
- 곡 목록: 실시간 검색(280ms 디바운스), 장르 필터, 20건씩 페이지네이션
- 아티스트 카드: 소속사·장르·데뷔년도·보유 곡 수
- 장르 바 차트: 장르별 곡 수 시각화 (CSS 애니메이션)
- READ-ONLY 뱃지: UI 상단에 명시적 표시
6. 검증 결과
# 서비스 상태
● commercy-music.service: active (running)
Main PID: 515202 (gunicorn), Workers: 2
# API 응답
$ curl http://127.0.0.1:8801/api/stats
{"total_agencies":11,"total_artists":16,"total_people":38,"total_songs":25}
# nginx 라우팅
GET https://hyperbook.com/music → 301 → /music/
GET https://hyperbook.com/music/ → 200 (Flask HTML)
7. 기존 서비스 영향도
| 항목 | 상태 |
|---|---|
| muse app.py | 무수정 |
| muse 서비스 포트 | 미확인·미간섭 |
| music.db | 읽기만, 락 경쟁 없음 (WAL 모드 미설정 시 read lock은 공유) |
| nginx 기존 블록 | 무수정, 위에 블록 추가만 |
SQLite read-only 연결은 공유 읽기 락을 사용하므로 기존 쓰기 서비스와 충돌하지 않는다.
8. 결론
"기존 서비스 침범 없이 외부 공개" 요건을 다음 세 가지 기술 조합으로 달성했다:
- SQLite
mode=roURI — OS 레벨 쓰기 차단 - 독립 포트(8801) — 기존 서비스와 완전 격리
- nginx location 블록 삽입 — 기존 설정 수정 없이 경로 추가
데이터가 늘어날 경우(1,598곡 전수 복원 목표가 논문에서 언급된 바 있음) 페이지네이션과 인덱스가 이미 준비되어 있어 확장에 문제없다.
[발신: ec2.hyperbook.com (Commercy)]
