ํต์ฌ ์์ฝ
- ํ ๋๊ธฐ์ ์ ์กฐ์ฌ์์ DBA๊ฐ ๋งค์ผ ์์นจ 30๋ถ~1์๊ฐ์ฉ ์์ผ๋ก ํ๋ DB ์ ๊ฒ์, AI๊ฐ ์๋ ๋ถ์ํด ๋งค์ผ ์ค์ 8์ HTML ๋ฉ์ผ๋ก ๋ณด๋ด์ฃผ๋ ์์คํ ์ 2์ฃผ POC๋ก ๋ง๋ค๊ณ ์์ต๋๋ค.
- ๋์์ AWS RDS ์์ Oracle·MySQL·PostgreSQL 3์ข . ๋ฐ์ดํฐ๋ ๋ชจ๋ํฐ๋ง ์๋ฃจ์ (DPM)์ด 1๋ถ ๋จ์๋ก ์ง๊ณํ ์๊ณ์ด ์งํ์ ๋๋ค.
- ์ ์ฒด๋ main.py๊ฐ collector → detector → reporter → mailer 4๊ฐ ๋ชจ๋์ ์์๋๋ก ์คํํ๋ ํ์ดํ๋ผ์ธ์ผ๋ก ๊ตฌ์ฑ๋๋ฉฐ, ์ด๋ฒ ๊ธ์ ๊ทธ ์ฒซ ๋จ๊ณ์ธ ๋ฐ์ดํฐ ์์ง(collector.py)์ ๋ค๋ฃน๋๋ค.
- collector์ ํต์ฌ ์ค๊ณ ๋ชฉํ๋ "๋์ค์ ๋ฐ์ดํฐ ์ถ์ฒ๊ฐ ์์ ์์ ์ค์ API๋ก ๋ฐ๋์ด๋, ์ด ํ์ผ ํ๋๋ง ๊ณ ์น๋ฉด ๋๊ฒ" ๊ฒฉ๋ฆฌํ๋ ๊ฒ์ ๋๋ค.
- ์ค์ ์ฝ๋์ ์ค์ ๋ฐ์ดํฐ ์ํ์ ์์ธํ ํ์ธํ๋ฉฐ ์์ ํ ๋ด์ฉ์ ์ ๋ฆฌํ์ต๋๋ค.
1. ์ด๋ค ํ๋ก์ ํธ์ธ๊ฐ
๋ฐ์ดํฐ๋ฒ ์ด์ค ๊ด๋ฆฌ์(DBA)๋ ๋งค์ผ ์์นจ ์ถ๊ทผํ๋ฉด ๋น์ทํ ์ผ์ ๋ฐ๋ณตํฉ๋๋ค. CPU ์ฌ์ฉ๋ฅ ์ด ๋ฐค์ฌ์ด ํ์ง ์์๋์ง, ์ธ์ ์ด ๊ณผํ๊ฒ ๋ชฐ๋ฆฌ์ง ์์๋์ง, ์ฌ๋ก์ฐ ์ฟผ๋ฆฌ๊ฐ ๋์ง ์์๋์ง, ํ ์ด๋ธ์คํ์ด์ค๋ ๋์คํฌ๊ฐ ์ฐจ์ค๋ฅด์ง ์์๋์ง๋ฅผ ๋ชจ๋ํฐ๋ง ํ๋ฉด์ ๋์ ๋๊ณ ๋์ผ๋ก ํ์ธํฉ๋๋ค. DB๊ฐ ํ๋ ๋๋ฉด ๊ธ๋ฐฉ์ด์ง๋ง, ์ด์ DB๊ฐ ์ฌ๋ฌ ์ข ๋ฅ·์ฌ๋ฌ ๋๋ก ๋์ด๋๋ฉด ์ด "์์นจ ์ ๊ฒ"์๋ง 30๋ถ์์ 1์๊ฐ์ด ๋ค์ด๊ฐ๋๋ค.
์ด ์์ ์ ํน์ง์ ๋๋ถ๋ถ์ ๋ ์ ์๋ฌด ์ด์์ด ์๋ค๋ ์ ์ ๋๋ค. ๊ทธ๋ฐ๋ฐ๋ ์ฌ๋์ด ๋งค์ผ ๊ฐ์ ํ๋ฉด์ ๊ฐ์ ์์๋ก ํ์ธํด์ผ ํฉ๋๋ค. ์ด์์ด ์๋ ๋ ์ ๋์น์ง ์๊ธฐ ์ํด, ์ด์์ด ์๋ 364์ผ๋ ๋๊ฐ์ด ๋ด์ผ ํ๋ ๊ตฌ์กฐ์ ๋๋ค. ์๋ํํ๊ธฐ ๋ฑ ์ข์ ์ผ์ ๋๋ค.
๋งค์ผ ์ค์ 7์ 50๋ถ์ ์๋์ผ๋ก ๋์์, ์ด์ ํ๋ฃจ์น DB ์งํ๋ฅผ ๋ถ์ํ๊ณ , ์ค์ 8์์ "์ค๋์ DB ์ํ ๋ฆฌํฌํธ"๋ฅผ HTML ์ด๋ฉ์ผ๋ก ๋ณด๋ธ๋ค. ์ด์์ด ์์ผ๋ฉด ์ด๋ค ์งํ๊ฐ ์ ๋ฌธ์ ์ธ์ง ์ง์ด ์ฃผ๊ณ , ์ด์์ด ์๋ ๋ ๋ "์ ์์ ๋๋ค" ๋ฆฌํฌํธ๋ฅผ ๋ณด๋ธ๋ค.
์กฐ๊ฑด๋ ๋ช ๊ฐ์ง ์์ต๋๋ค.
- ์จํ๋ ๋ฏธ์ค ๋ฉํ: ๊ณ ๊ฐ์ฌ ์๋ฒ์ ํ์ด์ฌ zip์ผ๋ก ์ฌ๋ ค์ cron์ผ๋ก ๋๋ฆฝ๋๋ค. ๋์ปค ์์ด, ์ฉ๋์ ๊ฐ๋ณ๊ฒ(1MB ์ดํ).
- ์ ํ๋ 85~90% ์ด์: ๋จ์ ์๊ณ๊ฐ ์ด๊ณผ๋ฟ ์๋๋ผ, ์๊ณ๊ฐ์ ์ ๋์์ง๋ง ํ์์ ๋ค๋ฅธ ํจํด ์ด์๋ ์ก์์ผ ํฉ๋๋ค.
- DB๋ณ๋ก ๋ค๋ฅธ ๊ธฐ์ค: Oracle·MySQL·PostgreSQL์ ๋ด์ผ ํ๋ ์งํ๋, ์ํ์ผ๋ก ๋ณด๋ ๊ธฐ์ค๊ฐ๋ ๋ค๋ฆ ๋๋ค.
๋์ DB๋ AWS RDS(ap-northeast-2)์ ์๋ Oracle 1๋, MySQL(Aurora) 1๋, PostgreSQL(Aurora) 1๋, ์ด 3์ข ์ ๋๋ค.
2. ์ ์ฒด ๊ตฌ์ฑ — 4๋จ๊ณ ํ์ดํ๋ผ์ธ
์์คํ ์ main.py๊ฐ collector·detector·reporter·mailer 4๊ฐ ๋ชจ๋์ ํ ๋ฐฉํฅ์ผ๋ก ์คํํ๋ ๊ตฌ์กฐ์ ๋๋ค. ๋ฐ์ดํฐ๊ฐ ์ผ์ชฝ์์ ๋ค์ด์ ์ค๋ฅธ์ชฝ์ผ๋ก ํ๋ฅด๋ฉด์ ์ ์ "์ฌ๋์ด ์ฝ์ ์ ์๋ ๋ฆฌํฌํธ"๋ก ๋ณํฉ๋๋ค.
main.py → collector → detector → reporter → mailer
(๋ฐ์ดํฐ์์ง) (์ด์ํ์ง) (HTML+๊ทธ๋ํ) (๋ฉ์ผ๋ฐ์ก)
↓ DbMetrics ↓ ์ด์๊ฒฐ๊ณผ ↓ HTML+PNG
| ๋ชจ๋ | ์ฑ ์ | ์ ๋ ฅ → ์ถ๋ ฅ | ์๋ฐ๋ก ์น๋ฉด |
|---|---|---|---|
| config.yaml | DB๋ณ ์๊ณ๊ฐ·์งํ·์ค์ | — | application.yml |
| collector.py | ๋ฐ์ดํฐ ์์ง(์ถ์ฒ ๊ฒฉ๋ฆฌ) | (config, ๋ ์ง) → DbMetrics | Repository / DAO |
| detector.py | ๋ฃฐ๋ฒ ์ด์ค + ML ์ด์ ํ์ง | DbMetrics → ์ด์ ๊ฒฐ๊ณผ | Service |
| reporter.py | HTML ๋ฆฌํฌํธ + ๊ทธ๋ํ | ์ด์ ๊ฒฐ๊ณผ → HTML + PNG | View / Template |
| mailer.py | SMTP ๋ฉ์ผ ๋ฐ์ก | HTML + PNG → ๋ฉ์ผ | ๋ฉ์ผ ์ปดํฌ๋ํธ |
๊ฐ ๋ชจ๋์ ํ ์ค๋ก ์ ๋ฆฌํ๋ฉด ์ด๋ ์ต๋๋ค.
- main.py — ์ ์ฒด ํ์ดํ๋ผ์ธ์ ์ง์ ์ ์ ๋๋ค. ์ ํด์ง ์์๋๋ก ๋๋จธ์ง ๋ชจ๋์ ํธ์ถํด ํ๋ฃจ ํ ๋ฒ ์คํํฉ๋๋ค. ์คํ๋ง์ผ๋ก ์น๋ฉด ๋ฐฐ์น ์ก์ ์คํ ์ง์ ์ ์ ํด๋นํฉ๋๋ค.
- config.yaml — DB๋ณ ์๊ณ๊ฐ·๊ฐ์ ์งํ·๋ฐ์ก ์๊ฐ ๊ฐ์ ์ค์ ์ ๋ชจ์ ๋ ํ์ผ์ ๋๋ค. ์ฝ๋๋ฅผ ๊ฑด๋๋ฆฌ์ง ์๊ณ ๊ธฐ์ค๊ฐ๋ง ๋ฐ๊ฟ ์ ์๋๋ก ๋ถ๋ฆฌํ์ต๋๋ค.
- collector.py — ์งํ ๋ฐ์ดํฐ๋ฅผ ๋ชจ์ ํ์ค ํํ(DbMetrics)๋ก ๋๋ ค์ค๋๋ค. ๋ฐ์ดํฐ ์ถ์ฒ(์์ /์ค์ API)๋ฅผ ๊ฐ์ถ๋ ๊ฒ์ด ํต์ฌ์ด๋ฉฐ, ์ด๋ฒ ๊ธ์ ์ฃผ์ ์ ๋๋ค.
- detector.py — ์์ง๋ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์ ์๊ณ๊ฐ ์ด๊ณผ·์ ๋ ๋๋น ๊ธ์ฆ·ํจํด ์ด์์ ํ์ ํฉ๋๋ค. ๋ฃฐ๋ฒ ์ด์ค์ ๋จธ์ ๋ฌ๋ ๋ ๋ฐฉ์์ ํจ๊ป ์๋๋ค.
- reporter.py — ํ์ ๊ฒฐ๊ณผ๋ฅผ ์ฌ๋์ด ์ฝ์ ์ ์๋ HTML ๋ฆฌํฌํธ์ ์ถ์ด ๊ทธ๋ํ(PNG)๋ก ๋ง๋ญ๋๋ค.
- mailer.py — ์์ฑ๋ ๋ฆฌํฌํธ๋ฅผ SMTP๋ก ๋ฉ์ผ ๋ฐ์กํฉ๋๋ค. ๋งค์ผ ์ค์ 8์์ ๋์ฐฉํ๋๋ก cron์ด 7์ 50๋ถ์ ์คํํฉ๋๋ค.
์ด๋ ๊ฒ ๋๋ ์ด์ ๋ ๋จ์ํฉ๋๋ค. ๊ฐ ๋ชจ๋์ด ์๊ธฐ ์ผ๋ง ํ๊ฒ ๋ง๋ค๋ฉด, ํ ๋ถ๋ถ์ ๊ณ ์น ๋ ๋ค๋ฅธ ๋ถ๋ถ์ด ํ๋ค๋ฆฌ์ง ์์ต๋๋ค. ํนํ ์ด๋ฒ ํ๋ก์ ํธ์๋ collector๋ง ๋ฐ๋ก ๋ผ์ด ๋ฌ์ผ ํ๋ ๊ฐํ ์ด์ ๊ฐ ํ๋ ์๋๋ฐ, ์ด๊ฑด 4์ฅ์์ ์์ธํ ๋ค๋ฃน๋๋ค.
์คํ๋ง์ ํด๋ณด์ ๋ถ์ด๋ผ๋ฉด ์ต์ํ ๊ทธ๋ฆผ์ ๋๋ค. Controller(main) → Service(detector) → Repository(collector)๋ก ์ฑ ์์ด ๋๋๊ณ , ๊ฐ ๊ณ์ธต์ ๋ฐ๋ก ์ ๊ณ์ธตํ๊ณ ๋ง ์ฝ์๋ ํํ๋ก ๋ํํฉ๋๋ค. ๊ทธ "์ฝ์๋ ํํ"๊ฐ ์ฌ๊ธฐ์๋ DbMetrics๋ผ๋ ๊ฐ์ฒด์ ๋๋ค.
3. ๋ฐ์ดํฐ๋ ์ด๋ป๊ฒ ์๊ฒผ๋
์ฝ๋๋ฅผ ๋ณด๊ธฐ ์ ์ ์ฌ๋ฃ๋ฅผ ๋จผ์ ๋ด์ผ ํฉ๋๋ค. collector๊ฐ ์ฝ์ด ๋ค์ด๋ ์๋ณธ ๋ฐ์ดํฐ๊ฐ ์ด๋ป๊ฒ ์๊ฒผ๋์ง ๋ชจ๋ฅด๋ฉด, collector ์ฝ๋๊ฐ ์ ๊ทธ๋ ๊ฒ ์๊ฒผ๋์ง ์ดํดํ ์ ์์ต๋๋ค.
๋ฐ์ดํฐ ์ถ์ฒ๋ ๋ชจ๋ํฐ๋ง ์๋ฃจ์ (DPM)์ ๋๋ค. ์ด ์๋ฃจ์ ์ ์๊ณ์ด DB์ 5์ด๋ง๋ค ์์ธ ์์ ๋ฐ์ดํฐ๋ฅผ 1๋ถ ๋จ์๋ก ์ง๊ณํด์ ์ ๊ณตํฉ๋๋ค. ๊ทธ๋ฐ๋ฐ POC ๋จ๊ณ์์๋ ๋ณด์์ ์ค์ API์ ๋ถ์ ์ ์์ด์, ๊ณ ๊ฐ์ฌ๊ฐ ํ ๋ฌ์น 1๋ถ ๋จ์ ์งํ๋ฅผ ์์ ๋ก ์ถ์ถํด ์ ๋ฌํด ์คฌ์ต๋๋ค.
์์ ํ์ผ์ ๊ท์น์ ์ด๋ ์ต๋๋ค.
- ํ์ผ 1๊ฐ = ์งํ 1๊ฐ. ํ์ผ๋ช ์ db_{db}_{metric}.xlsx ํ์์ ๋๋ค. ์: db_oracle_cpu_usage.xlsx, db_oracle_freestoragespace.xlsx.
- DB๋ณ ํ์ผ ์: Oracle 33๊ฐ, MySQL 27๊ฐ, PostgreSQL 32๊ฐ.
์ค์ db_oracle_cpu_usage.xlsx๋ฅผ ์ด์ด ๋ณธ ๋ชจ์ต์ ๋๋ค(์ 5ํ, ์๋ณ์๋ ๊ฐ๋ ธ์ต๋๋ค).
agg_collect_time target_id data_id last_value max_value count_data
0 2026-06-03T22:00:00 176d…c8371 db_oracle_cpu_usage 8.235000 8.235 12
1 2026-06-03T22:01:00 176d…c8371 db_oracle_cpu_usage 8.066667 8.066 12
2 2026-06-03T22:02:00 176d…c8371 db_oracle_cpu_usage 9.993333 9.993 12
3 2026-06-03T22:03:00 176d…c8371 db_oracle_cpu_usage 15.608333 15.608 12
4 2026-06-03T22:04:00 176d…c8371 db_oracle_cpu_usage 9.401667 9.401 12
์ปฌ๋ผ์ ํ๋์ฉ ๋ณด๊ฒ ์ต๋๋ค.
| ์ปฌ๋ผ | ๋ป |
|---|---|
| agg_collect_time | ์ง๊ณ ์๊ฐ. ISO ๋ฌธ์์ด(2026-06-03T22:00:00)์ด๊ณ 1๋ถ ๊ฐ๊ฒฉ์ ๋๋ค. |
| target_id | ์ธ์คํด์ค ์๋ณ์(UUID). ์ด๋ DB์ ๋ฐ์ดํฐ์ธ์ง ๊ฐ๋ฆฌํต๋๋ค. |
| data_id | ์งํ ์๋ณ์. ํ์ผ๋ช ๊ณผ ๊ฐ์ ๊ฐ์ ๋๋ค. |
| last_value | ๊ทธ 1๋ถ ๊ตฌ๊ฐ์ ๋ํ๊ฐ(ํ๊ท ). ์ฐ๋ฆฌ๊ฐ ์ค์ ๋ก ์ฐ๋ ๊ฐ์ ๋๋ค. |
| sum / max / min / first_value | 1๋ถ ๋์์ ํฉ·์ต๋·์ต์·์ฒซ ๊ฐ. |
| count_data | ๊ทธ 1๋ถ์ ๋ค์ด์จ ์์ ๋ฐ์ดํฐ ๊ฐ์. 12 = 5์ด × 12 = 60์ด. |
์ฌ๊ธฐ์ ์ค์ํ ํจ์ ์ธ ๊ฐ์ง๊ฐ ์๊ณ , ์ด๊ฒ collector ์ฝ๋์ ๋ชจ์์ ๊ฒฐ์ ํฉ๋๋ค.
- ๋ชจ๋ ๊ฐ์ด ๋ฌธ์์ด๋ก ๋ค์ด์ต๋๋ค. ์ซ์์ฒ๋ผ ๋ณด์ฌ๋ "8.235" ๊ฐ์ ํ ์คํธ์ ๋๋ค. ๊ทธ๋๋ก ๋น๊ตํ๋ฉด "9" > "15"๊ฐ ์ฐธ์ด ๋๋ ์ฌ๊ณ ๊ฐ ๋ฉ๋๋ค. ๋ฐ๋์ float์ผ๋ก ๋ณํํด์ผ ํฉ๋๋ค.
- ์๊ฐ์ ์ ๋ ฌ์ด ๋ณด์ฅ๋์ง ์์ต๋๋ค. ํ์ด ๋ค์ฃฝ๋ฐ์ฃฝ์ผ ์ ์์ด์, ๋ ์ง๋ก ์๋ฅด๊ธฐ ์ ์ ์๊ฐ์ผ๋ก ์ ๋ ฌํด์ผ ํฉ๋๋ค.
- ํ๋ฃจ๋ 1,440ํ์ ๋๋ค. 24์๊ฐ × 60๋ถ. "์ด์ ํ๋ฃจ์น"๋ฅผ ๋ฝ์ผ๋ฉด ์ ์์ ์ผ๋ก 1,440ํ์ด ๋์์ผ ํ๋ค๋ ๊ฒ์ฆ ๊ธฐ์ค์ด ์ฌ๊ธฐ์ ๋์ต๋๋ค.
๊ทธ๋ฆฌ๊ณ ํ ๊ฐ์ง ๋. ์ด ์์ ์ POC์ฉ ์์ ๋ฐ์ดํฐ์ผ ๋ฟ์ด๊ณ , ํ์ฅ์์๋ ์ค์ API๋ก ๊ฐ์๋ผ์ธ ์์ ์ ๋๋ค. ์ด ์ฌ์ค์ด ๋ค์ ์ฅ์ ์ค๊ณ๋ก ์ด์ด์ง๋๋ค.
4. collector.py๊ฐ ํ๋ ์ผ
ํ ์ค ์ ์
collector๋ "์ ๊ฒ ๋์์ผ(๋ณดํต ์ด์ )๊ณผ ๋น๊ต์ฉ ์ ๋ (๊ทธ์ )์ 1๋ถ ์งํ ์๊ณ์ด์, DB๋ณ๋ก ๋ชจ์ ํ์ค ํํ(DbMetrics)๋ก ๋๋ ค์ฃผ๋" ๋ชจ๋์ ๋๋ค.
์ค๊ณ์ ํต์ฌ — "์ด ํ์ผ ํ๋๋ง ๊ณ ์น๋ฉด ๋๊ฒ"
์ด ํ๋ก์ ํธ์์ ๊ฐ์ฅ ์ ๊ฒฝ ์ด ์ค๊ณ ๊ฒฐ์ ์ collector์ ์์ต๋๋ค.
POC๋ ์์ ๋ก ๊ฐ๋ฐํ์ง๋ง, ๋ง์ง๋ง ์ฃผ์ ํ์ฅ์ ๊ฐ์ ์ค์ DPM API๋ก ๋ฐ์ดํฐ ์ถ์ฒ๋ฅผ ๊ต์ฒดํด์ผ ํฉ๋๋ค. ์ด๋ detector·reporter·mailer๊น์ง ์ค์ค์ด ๊ณ ์ณ์ผ ํ๋ค๋ฉด, ๊ต์ฒด๋ ์ํํ ๋์์ ์ด ๋ฉ๋๋ค. ๊ทธ๋์ ์ฒ์๋ถํฐ ๊ท์น์ ์ ํ์ต๋๋ค.
๋ฐ์ดํฐ ์ถ์ฒ๊ฐ ๋ฐ๋์ด๋ collector.py ํ ํ์ผ๋ง ์์ ํ๋ค. ๋๋จธ์ง ๋ชจ๋์ ์๋์ง ์๋๋ค.
์ด๊ฑธ ๊ฐ๋ฅํ๊ฒ ํ๋ ๊ฒ ์ธํฐํ์ด์ค + ๊ตฌํ์ฒด + ํฉํ ๋ฆฌ ๊ตฌ์กฐ์ ๋๋ค. ์๋ฐ/์คํ๋ง ํ์ จ์ผ๋ฉด ๋ฐ๋ก ์ต์ํ ํจํด์ ๋๋ค.
| ํ์ด์ฌ (collector.py) | ์๋ฐ/์คํ๋ง ๋์ |
|---|---|
| MetricSource (์ถ์ ํด๋์ค, ABC) | interface MetricSource |
| ExcelMetricSource (์์ ๊ตฌํ) | class ExcelMetricSource implements MetricSource |
| DpmApiMetricSource (API ๊ตฌํ) | class DpmApiMetricSource implements MetricSource |
| get_metric_source() ํฉํ ๋ฆฌ | ์ค์ ๋ณด๊ณ ๋น ๊ณจ๋ผ ์ฃผ์ ํ๋ ์คํ๋ง ์ปจํ ์ด๋ |
config.yaml์ datasource.type ๊ฐ์ด excel์ด๋ฉด ์์ ๊ตฌํ์ฒด๊ฐ, dpm_api๋ฉด API ๊ตฌํ์ฒด๊ฐ ์ ํ๋ฉ๋๋ค. ํ์ ๋ชจ๋์ ์๊ธฐ๊ฐ ๋ฐ์ ๋ฐ์ดํฐ๊ฐ ์์ ์์ ์๋์ง API์์ ์๋์ง ์ ํ ๋ชจ๋ฆ ๋๋ค. ๊ทธ์ DbMetrics๋ผ๋ ํ์ค ์์๋ง ๋ฐ์ต๋๋ค.
# config.yaml — ์ด ํ ์ค๋ง ๋ฐ๊พธ๋ฉด ์ถ์ฒ๊ฐ ๊ต์ฒด๋๋ค
datasource:
type: "excel" # excel | dpm_api
excel:
data_dir: "data"
dpm_api: # ํ์ฅ ์ฐ๋ ์ ์ฑ์
base_url: ""
token: ""
ํ์ค ์ถ๋ ฅ — DbMetrics
๋ชจ๋ ๋ชจ๋์ด ๊ณต์ ํ๋ "์ฝ์๋ ํํ"์ ๋๋ค. ์คํ๋ง์ DTO/VO๋ผ๊ณ ๋ณด๋ฉด ๋ฉ๋๋ค.
@dataclass
class DbMetrics:
"""ํ DB(์ธ์คํด์ค)์ ์ ๊ฒ ๋ฐ์ดํฐ ๋ฌถ์."""
db_name: str # 'mysql' | 'oracle' | 'postgresql'
instance: str # ์ธ์คํด์ค๋ช
target_date: date # ์ ๊ฒ ๋์์ผ
today: pd.DataFrame # ๋์์ผ 1๋ถ ์๊ณ์ด
prev_day: pd.DataFrame # ์ ์ผ(๊ธ์ฆ/ํจํด ๋น๊ต์ฉ) 1๋ถ ์๊ณ์ด
unmapped: List[str] # ๋ฐ์ดํฐ ํ์ผ์ด ์์ด ์ ์ธ๋ ์งํ
today์ prev_day๋ DataFrame์ ๋๋ค. ํ์ด์ฌ ๋ฐ์ดํฐ ๋ถ์์ ํ ์๋ฃ๊ตฌ์กฐ์ธ๋ฐ, ์์ ์ํธ๋ฅผ ์ฝ๋๋ก ๋ค๋ฃฌ๋ค๊ณ ์๊ฐํ๋ฉด ๋ฉ๋๋ค. ํ ์ธ๋ฑ์ค๋ 1๋ถ ๊ฐ๊ฒฉ ์๊ฐ, ์ด์ ์งํ ์ด๋ฆ, ์นธ์ ๊ทธ ์๊ฐ์ ๊ฐ์ ๋๋ค.
(OS) Cpu Usage Active Session Free Storage Space
2026-06-14 00:00:00 8.23 12.0 5.84e+11
2026-06-14 00:01:00 8.07 11.0 5.84e+11
2026-06-14 00:02:00 9.99 14.0 5.84e+11
... ... ... ...
(1,440ํ)
prev_day๋ฅผ ๋ฐ๋ก ๋ค๊ณ ๋ค๋๋ ์ด์ ๋, ์ด์ ํ์ง์์ "์ ๋ ๋๋น 2๋ฐฐ ๊ธ์ฆ", "ํ์ ๋๋น 20%p ์์น" ๊ฐ์ ๋น๊ต๋ฅผ ํ๊ธฐ ๋๋ฌธ์ ๋๋ค. ๊ทธ๋์ collector๊ฐ ์ด์ ์ ๊ทธ์ ๋ฅผ ํจ๊ป ์ฑ๊ฒจ ์ค๋๋ค.
5. ํต์ฌ ์ฝ๋ ๋ฐ์ท
(1) ์์ 1๊ฐ → ์๊ณ์ด ํ ์ค๋ก
๊ฐ์ฅ ๋ฐ๋ฐ๋ฅ์์ ์์ ํ์ผ ํ๋๋ฅผ ์ฝ์ด (์๊ฐ, ๊ฐ) ์๊ณ์ด๋ก ๋ฐ๊พธ๋ ํจ์์ ๋๋ค. 3์ฅ์์ ๋ณธ ํจ์ ์ธ ๊ฐ์ง(๋ฌธ์์ด·์ ๋ ฌ·์ค๋ณต)๊ฐ ์ฌ๊ธฐ์ ๋ชจ๋ ์ฒ๋ฆฌ๋ฉ๋๋ค.
@staticmethod
def _read_series(path: str):
"""์์
1ํ์ผ → (timestamp index, float value) Series. ์๊ณ์ด ์๋๋ฉด None."""
df = pd.read_excel(path, sheet_name=0, engine="calamine")
if "agg_collect_time" not in df.columns or "last_value" not in df.columns:
return None # ์๊ณ์ด ๋ฉํธ๋ฆญ ํ์ผ์ด ์๋ → skip
idx = pd.to_datetime(df["agg_collect_time"], errors="coerce") # ๋ฌธ์์ด → ์๊ฐ
val = pd.to_numeric(df["last_value"], errors="coerce") # ๋ฌธ์์ด → float
s = pd.Series(val.values, index=idx).dropna()
return s[~s.index.duplicated(keep="last")].sort_index() # ์ค๋ณต ์ ๊ฑฐ + ์ ๋ ฌ
ํ ์ค์ฉ ๋ณด๊ฒ ์ต๋๋ค.
- pd.read_excel(..., engine="calamine") — ์์ ์ ์ฝ์ต๋๋ค. calamine์ด๋ผ๋ ์์ง์ ์ฝ ์ง์ด ์ฐ๋๋ฐ, ์ฌ๊ธฐ์ ์ฌ์ฐ์ด ์์ต๋๋ค(์๋ 6์ฅ).
- ์ปฌ๋ผ ์ฒดํฌ — agg_collect_time์ด๋ last_value๊ฐ ์์ผ๋ฉด ์๊ณ์ด ํ์ผ์ด ์๋๋ผ๊ณ ๋ณด๊ณ None์ ๋๋ ค ๊ฑด๋๋๋๋ค. ์ผ๋ถ ํ์ผ์ ์๊ณ์ด์ด ์๋๋ผ ์ฟผ๋ฆฌ ๋ชฉ๋ก์ด๋ผ ํํ๊ฐ ๋ค๋ฆ ๋๋ค.
- pd.to_datetime(..., errors="coerce") — ๋ฌธ์์ด ์๊ฐ์ ์ง์ง ์๊ฐ ํ์ ์ผ๋ก ๋ฐ๊ฟ๋๋ค. errors="coerce"๋ "๋ณํ ์คํจํ ๊ฐ์ ์๋ฌ ๋ด์ง ๋ง๊ณ ๋น ๊ฐ(NaT)์ผ๋ก ๋ฌ๋ผ"๋ ๋ป์ ๋๋ค.
- pd.to_numeric(...) — ๋ฌธ์์ด ์ซ์๋ฅผ float์ผ๋ก. ํจ์ โ ์ ์ฌ๊ธฐ์ ํด๊ฒฐํฉ๋๋ค.
- duplicated(keep="last") + sort_index() — ๊ฐ์ ์๊ฐ์ด ์ค๋ณต๋๋ฉด ๋ง์ง๋ง ๊ฒ๋ง ๋จ๊ธฐ๊ณ , ์๊ฐ์์ผ๋ก ์ ๋ ฌํฉ๋๋ค. ํจ์ โก๋ฅผ ํด๊ฒฐํฉ๋๋ค.
(2) ํ ๋ฌ์น ์ ์ฒด๋ฅผ ํ DataFrame์ผ๋ก ๋ณํฉ
์งํ๋ณ ์๊ณ์ด์ ๋ชจ์ ํ๋์ ํ๋ก ํฉ์นฉ๋๋ค. ํ ๋ฒ ์ฝ์ด ์บ์ํด ๋๊ณ , ๋ ์ง๋ณ ์กฐํ๋ ์๋ผ ์ฐ๊ธฐ๋ง ํฉ๋๋ค.
def _load_frame(self) -> pd.DataFrame:
if self._frame is not None:
return self._frame # ์ด๋ฏธ ์ฝ์์ผ๋ฉด ์บ์ ๋ฐํ
series = {}
for source, path in self._resolve_files().items():
s = self._read_series(path)
if s is None:
self._unmapped.append(source) # ์๊ณ์ด ์๋ → ์ ์ธ ๋ชฉ๋ก์
continue
series[source] = s
self._frame = pd.DataFrame(series).sort_index()
return self._frame
(3) ๋ ์ง ํ๋ฃจ์น ์๋ผ ๋ด๊ธฐ
์ฌ๊ธฐ๊ฐ ์์ธ๋ก ๊น๋ํฉ๋๋ค. ์๊ฐ์ผ๋ก ์ ๋ ฌ๋ ํ๋ผ์, ๋ ์ง ๋ฌธ์์ด๋ก ๋ถ๋ถ ์ธ๋ฑ์ฑ๋ง ํ๋ฉด ํ๋ฃจ๊ฐ ์๋ฆฝ๋๋ค.
def fetch(self, target_date: date) -> pd.DataFrame:
frame = self._load_frame()
key = target_date.isoformat() # 'YYYY-MM-DD'
try:
day = frame.loc[key] # ๊ทธ ๋ ์ง 1๋ถ ์๊ณ์ด๋ง ์ฌ๋ผ์ด์ค
except KeyError:
return frame.iloc[0:0] # ๋ฐ์ดํฐ ์๋ ๋ → ๋น ํ๋ ์
return day
(4) DB ์ ์ฒด๋ฅผ ๋ชจ์ ํ์ค ํํ๋ก
๋ง์ง๋ง์ผ๋ก ๋ชจ๋ DB๋ฅผ ๋๋ฉฐ ์ด์ ·๊ทธ์ ๋ฅผ ์ฑ๊ฒจ DbMetrics๋ก ํฌ์ฅํฉ๋๋ค. ์ด๊ฒ collector์ ์ง์ ์ ์ ๋๋ค.
def collect(config: dict, target_date: date) -> Dict[str, DbMetrics]:
prev_date = target_date - timedelta(days=1)
result = {}
for db_name in config["databases"]:
source = get_metric_source(db_name, config) # ํฉํ ๋ฆฌ๊ฐ ์ถ์ฒ ์ ํ
today = source.fetch(target_date)
prev_day = source.fetch(prev_date)
result[db_name] = DbMetrics(
db_name=db_name, instance=source.instance,
target_date=target_date, today=today, prev_day=prev_day,
unmapped=source.unmapped_metrics(),
)
return result
์งํ ์ด๋ฆ ↔ ํ์ผ๋ช ๋งค์นญ ๋ฌธ์
์์ง๋ง ์ฑ๊ฐ์ ๋ฌธ์ ๊ฐ ํ๋ ์์์ต๋๋ค. config.yaml์ ์ด ์งํ ์ด๋ฆ(์: (OS) Cpu Usage)๊ณผ ์ค์ ํ์ผ๋ช (db_oracle_cpu_usage.xlsx)์ด ๋ฑ ๋ง์๋จ์ด์ง์ง ์์ต๋๋ค. ๊ณต๋ฐฑ·๊ดํธ·๋์๋ฌธ์๊ฐ ์ ๊ฐ๊ฐ์ ๋๋ค.
๊ทธ๋์ 3๋จ๊ณ๋ก ๋งค์นญํฉ๋๋ค.
- ์๋ ์ ๊ทํ ๋งค์นญ: ์์ซ์๋ง ๋จ๊ธฐ๊ณ ์๋ฌธ์๋ก ๋ฐ๊ฟ((OS) Cpu Usage → oscpuusage) ๋ถ๋ถ ์ผ์น๋ก ์ฐพ์ต๋๋ค.
- ์์ธ ๋ณด์ ํ(EXPLICIT_FILE_MAP): ์๋์ผ๋ก ์ ๋๋ ๊ฒ๋ง ์ง์ ์ง์ ํฉ๋๋ค. ์๋ฅผ ๋ค์ด PostgreSQL์ Free Memory๋ ์ค์ ํ์ผ์ด freeablememory๋ผ ์๋ ๋งค์นญ์ด ์ ๋ฉ๋๋ค.
- ๋ฏธ์ ๊ณต ์ ์ธ ๋ชฉ๋ก(EXCLUDE_METRICS): config์ ์์ง๋ง ๋ฐ์ดํฐ๊ฐ ์ ์จ ์งํ๋ ์์ ๊ฑด๋๋๋๋ค. ์ด๊ฑธ ์ ํ๋ฉด ๋น์ทํ ์ด๋ฆ์ ์๋ฑํ ํ์ผ์ ์๋ชป ๋ถ๋ ์ฌ๊ณ ๊ฐ ๋ฉ๋๋ค(์: Host Read IOPS๊ฐ readiops์ ์ค๋งค์นญ).
EXPLICIT_FILE_MAP = {
"postgresql": {
"Free Memory": "db_postgresql_freeablememory.xlsx", # freememory != freeablememory
},
}
EXCLUDE_METRICS = {
"oracle": {"Host Read IOPS", "Host Write IOPS", ...},
"mysql": {"Volume Bytes Used Max", "Host Disk Used"},
"postgresql": {"Postgresql Slow Query Monitor"},
}
"์๋์ผ๋ก ์ต๋ํ ์ฒ๋ฆฌํ๊ณ , ์ ๋๋ ๊ฒ๋ง ์์ผ๋ก ์์ธ ์ฒ๋ฆฌํ๋ค"๋ ๋ฐฉ์์ ๋๋ค. ๋ฐ์ดํฐ๊ฐ ์ถ๊ฐ๋๊ฑฐ๋ ์ด๋ฆ์ด ๋ฐ๋์ด๋ ํ ๋ช ์ค๋ง ๊ณ ์น๋ฉด ๋ฉ๋๋ค.
6. ํ ์ค์ง๋ฆฌ ํธ๋ฌ๋ธ์ํ — ์ calamine์ธ๊ฐ
์ฝ๋์์ engine="calamine"์ ์ฝ ์ง์ด ์ด ์ด์ ๊ฐ ์์ต๋๋ค. ์ฒ์์ ํ์ด์ฌ์์ ๊ฐ์ฅ ํํ ์์ ์์ง์ธ openpyxl์ ์ผ๋๋ฐ, ์ด ์์ ํ์ผ๋ค์ด ๋ด๋ถ ๋ฉํ๋ฐ์ดํฐ(dimension ์ ๋ณด)๊ฐ ๋น์ด ์์ด์, openpyxl์ด ํ·์ด ๋ฒ์๋ฅผ ์ ๋๋ก ๋ชป ์ฝ์์ต๋๋ค. ๊ฒ๋ค๊ฐ ํ ๋ฌ์น 33๊ฐ ํ์ผ์ ์ฝ๋ ๋ฐ ์๊ฐ๋ ์ค๋ ๊ฑธ๋ ธ์ต๋๋ค.
calamine(๋ฌ์คํธ ๊ธฐ๋ฐ ์์ ๋ฆฌ๋) ์์ง์ผ๋ก ๋ฐ๊พธ์ ๋ฉํ๋ฐ์ดํฐ์ ๋ฌด๊ดํ๊ฒ ๊ฐ์ ์ ํํ ์ฝ์๊ณ , ์ฝ๊ธฐ ์๊ฐ๋ ์ฝ 661์ด์์ 73์ด๋ก ํฌ๊ฒ ์ค์์ต๋๋ค. ์ด ํธ๋ฌ๋ธ์ํ ์์ฒด๋ ๋ด์ฉ์ด ๋ง์ ๋ค์ ๊ธ์์ ๋ฐ๋ก ์ ๋ฆฌํ๊ฒ ์ต๋๋ค.
7. ์ง๊ธ๊น์ง ํ ๊ฒ / ๋จ์ ๊ฒ
์๋ฃ
- ํ๋ก์ ํธ ์ค๊ณ·์ผ์ ์ ๋ฆฌ, config.yaml(DB๋ณ ์๊ณ๊ฐ·์งํ 2๋จ ๊ตฌ์กฐ) ์์ฑ.
- ์ค๋ฐ์ดํฐ ํ ๋ฌ์น ์๋ น·์ ๋ฆฌ(Oracle 33 / MySQL 27 / PostgreSQL 32 ํ์ผ).
- collector.py ์์ฑ: ์ธํฐํ์ด์ค·๊ตฌํ์ฒด·ํฉํ ๋ฆฌ ๊ตฌ์กฐ, 3๋จ๊ณ ์งํ ๋งค์นญ, ๋ฌธ์์ด·์ ๋ ฌ ์ฒ๋ฆฌ, ๋ ์ง ์ฌ๋ผ์ด์ฑ. 3์ข DB ๋ชจ๋ ํ๋ฃจ 1,440ํ์ด ์ ์ ์ถ๋ ฅ๋๋ ๊ฒ๊น์ง ๊ฒ์ฆ.
๋ค์
- detector.py — ๋ฃฐ๋ฒ ์ด์ค(์๊ณ๊ฐ ์ด๊ณผ / ์ ๋ ๋๋น ๊ธ์ฆ / ํจํด ์ด์)์ ๋น์ง๋ํ์ต ์ด์ ํ์ง.
- reporter.py / mailer.py — HTML ๋ฆฌํฌํธ์ ๊ทธ๋ํ ์์ฑ, SMTP ๋ฉ์ผ ๋ฐ์ก.
- ๋ง์ง๋ง ์ฃผ ํ์ฅ์์ ์์ → ์ค์ DPM API ๊ต์ฒด. ์ด๋ 4์ฅ์ ๊ฒฉ๋ฆฌ ์ค๊ณ๊ฐ ํจ๊ณผ๋ฅผ ๋ด ๋๋ค. collector.py์ API ๊ตฌํ์ฒด ํ๋๋ง ์ฑ์ฐ๋ฉด ๋ฉ๋๋ค.
8. ์ ๋ฆฌํ๋ฉฐ
์ด๋ฒ ๊ธ์์ ๊ฐ์ฅ ๊ฐ์กฐํ๊ณ ์ถ์ ํ ๊ฐ์ง๋ ๊ฒฉ๋ฆฌ ์ค๊ณ์ ๋๋ค. "์ง๊ธ ๋น์ฅ์ ์์ ์ด์ง๋ง ๊ณง API๋ก ๋ฐ๋๋ค"๋ ์ฌ์ค์ ์๊ณ ์์ ๋, ๊ทธ ๋ณํ์ ์ถฉ๊ฒฉ์ ํ ํ์ผ ์์ ๊ฐ๋๋ ๊ฒ. ์ธํฐํ์ด์ค๋ก ๊ฒฝ๊ณ๋ฅผ ๊ธ๊ณ , ํ์ ๋ชจ๋์ ํ์ค ํํ(DbMetrics)์๋ง ์์กดํ๊ฒ ๋ง๋๋ ๊ฒ. ์ด๊ฑด ์คํ๋ง์์ Repository ์ธํฐํ์ด์ค ๋ค์ JPA๋ MyBatis๋ ์จ๊ธฐ๋ ๊ฒ๊ณผ ์ ํํ ๊ฐ์ ๋ฐ์์ ๋๋ค.
๋ค์ ๊ธ์์๋ ์ด ๋ฐ์ดํฐ๋ฅผ ๋ฐ์ "์ด๊ฒ ์ ์์ธ๊ฐ ์ด์์ธ๊ฐ"๋ฅผ ํ์ ํ๋ detector๋ฅผ ๋ค๋ฃจ๊ฒ ์ต๋๋ค. ์๊ณ๊ฐ๋ง์ผ๋ก๋ ๋ชป ์ก๋ ํจํด ์ด์์ ์ด๋ป๊ฒ ์ก์์ง๊ฐ ํต์ฌ์ ๋๋ค.