FCM으로 서버 디스크 상태를 안드로이드 앱에 푸시 알림 보내기
서버 디스크 사용량이 80%를 초과하면 Firebase Cloud Messaging(FCM)을 통해 안드로이드 앱에 즉시 푸시 알림을 전송하는 방법을 단계별로 설명합니다.
1. 전체 아키텍처 개요
[Linux Server]
├── cron + Python 스크립트 (디스크 사용량 감시)
│ │
│ │ 80% 초과 시 HTTP POST
│ ▼
[Firebase Cloud Messaging (FCM)]
│ │
│ │ Push Notification (Topic 또는 Token 방식)
│ ▼
[Android App] × N명 (5명 이상)
핵심 전송 방식: Topic 구독
클라이언트가 5명 이상인 경우, 각 기기의 토큰을 서버에서 일일이 관리하는 대신 FCM Topic(/topics/disk-alert)을 사용합니다. 앱 설치 시 해당 토픽을 구독하면, 서버는 토픽 하나에만 메시지를 보내면 구독한 모든 기기에 자동으로 전달됩니다.
2. Firebase 프로젝트 설정
2-1. Firebase 프로젝트 생성
- Firebase Console 접속
- 프로젝트 추가 클릭 → 프로젝트 이름 입력 (예:
server-disk-monitor) - Google Analytics는 선택 사항 → 프로젝트 만들기
2-2. 안드로이드 앱 등록
- 프로젝트 홈 → Android 아이콘 클릭
- 패키지 이름 입력 (예:
com.example.diskmonitor) - google-services.json 다운로드 → 안드로이드 프로젝트의
app/폴더에 복사
2-3. 서비스 계정 키(Service Account Key) 발급
서버에서 FCM API를 호출하려면 서비스 계정 키가 필요합니다.
- Firebase Console → 프로젝트 설정 → 서비스 계정 탭
- 새 비공개 키 생성 클릭 → JSON 파일 다운로드
- 서버의 안전한 경로에 저장 (예:
/etc/fcm/service-account.json)
# 파일 권한을 root만 읽을 수 있도록 설정
sudo chmod 600 /etc/fcm/service-account.json
sudo chown root:root /etc/fcm/service-account.json
3. 서버 설정 (Python 모니터링 스크립트)
3-1. 필요 패키지 설치
pip install google-auth requests psutil
3-2. FCM 메시지 전송 스크립트 작성
/usr/local/bin/disk_monitor.py 로 저장합니다.
#!/usr/bin/env python3
"""
서버 디스크 사용량을 감시하고 80% 초과 시 FCM으로 푸시 알림을 전송합니다.
"""
import psutil
import requests
import json
import os
import logging
from google.oauth2 import service_account
from google.auth.transport.requests import Request
# ── 설정 ──────────────────────────────────────────────────────────────
SERVICE_ACCOUNT_FILE = "/etc/fcm/service-account.json"
DISK_THRESHOLD_PERCENT = 80 # 알림 발송 기준 (%)
MONITOR_PATH = "/" # 감시할 마운트 포인트
FCM_TOPIC = "disk-alert" # 구독 토픽 이름
PROJECT_ID = "your-firebase-project-id" # Firebase 프로젝트 ID로 변경
FCM_URL = f"https://fcm.googleapis.com/v1/projects/{PROJECT_ID}/messages:send"
SCOPES = ["https://www.googleapis.com/auth/firebase.messaging"]
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
handlers=[
logging.FileHandler("/var/log/disk_monitor.log"),
logging.StreamHandler()
]
)
# ──────────────────────────────────────────────────────────────────────
def get_access_token() -> str:
"""서비스 계정으로 OAuth2 액세스 토큰 발급"""
credentials = service_account.Credentials.from_service_account_file(
SERVICE_ACCOUNT_FILE, scopes=SCOPES
)
credentials.refresh(Request())
return credentials.token
def get_disk_usage(path: str) -> tuple[float, str, str]:
"""디스크 사용률, 사용량, 전체 용량 반환"""
usage = psutil.disk_usage(path)
percent = usage.percent
used_gb = usage.used / (1024 ** 3)
total_gb = usage.total / (1024 ** 3)
return percent, f"{used_gb:.1f}GB", f"{total_gb:.1f}GB"
def send_fcm_notification(percent: float, used: str, total: str) -> bool:
"""FCM Topic으로 푸시 알림 전송"""
token = get_access_token()
hostname = os.uname().nodename
payload = {
"message": {
"topic": FCM_TOPIC,
"notification": {
"title": f"⚠️ 디스크 경고: {hostname}",
"body": (
f"디스크 사용량이 {percent:.1f}%에 도달했습니다.\n"
f"사용: {used} / 전체: {total}"
)
},
"data": {
"type": "DISK_ALERT",
"hostname": hostname,
"mount_path": MONITOR_PATH,
"percent": str(percent),
"used": used,
"total": total
},
"android": {
"priority": "high",
"notification": {
"channel_id": "disk_alert_channel",
"sound": "default",
"click_action": "DISK_ALERT_ACTIVITY"
}
}
}
}
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
response = requests.post(FCM_URL, headers=headers, json=payload, timeout=10)
if response.status_code == 200:
logging.info(f"FCM 전송 성공: {response.json().get('name')}")
return True
else:
logging.error(f"FCM 전송 실패: {response.status_code} - {response.text}")
return False
def main():
percent, used, total = get_disk_usage(MONITOR_PATH)
logging.info(f"디스크 사용량: {percent:.1f}% ({used}/{total})")
if percent > DISK_THRESHOLD_PERCENT:
logging.warning(f"임계값 초과 ({DISK_THRESHOLD_PERCENT}%)! FCM 알림 전송 중...")
send_fcm_notification(percent, used, total)
else:
logging.info("정상 범위입니다. 알림 불필요.")
if __name__ == "__main__":
main()
PROJECT_ID를 Firebase 프로젝트 설정 페이지에서 확인한 실제 프로젝트 ID로 반드시 변경하세요.
3-3. 스크립트 실행 권한 및 cron 설정
# 실행 권한 부여
sudo chmod +x /usr/local/bin/disk_monitor.py
# cron으로 5분마다 실행 (root 권한)
sudo crontab -e
crontab에 아래 내용을 추가합니다:
# 5분마다 디스크 사용량 감시
*/5 * * * * /usr/bin/python3 /usr/local/bin/disk_monitor.py
3-4. 중복 알림 방지 (선택 사항)
80%를 초과하는 상태가 계속되면 5분마다 알림이 발송됩니다. 중복 발송을 막으려면 플래그 파일을 활용하세요.
FLAG_FILE = "/tmp/disk_alert_sent"
def main():
percent, used, total = get_disk_usage(MONITOR_PATH)
if percent > DISK_THRESHOLD_PERCENT:
if not os.path.exists(FLAG_FILE):
send_fcm_notification(percent, used, total)
open(FLAG_FILE, "w").close() # 플래그 생성
logging.warning("알림 전송 완료. 플래그 파일 생성.")
else:
logging.info("이미 알림 발송됨. 중복 전송 건너뜀.")
else:
# 정상 복귀 시 플래그 삭제
if os.path.exists(FLAG_FILE):
os.remove(FLAG_FILE)
logging.info("디스크 정상 복귀. 플래그 파일 삭제.")
4. 안드로이드 앱 설정
4-1. build.gradle 의존성 추가
프로젝트 수준 build.gradle:
// build.gradle (Project)
plugins {
id 'com.google.gms.google-services' version '4.4.1' apply false
}
앱 수준 app/build.gradle:
// build.gradle (App)
plugins {
id 'com.android.application'
id 'com.google.gms.google-services'
}
dependencies {
// Firebase BoM (버전 통합 관리)
implementation platform('com.google.firebase:firebase-bom:33.1.0')
implementation 'com.google.firebase:firebase-messaging'
}
4-2. FCM 서비스 클래스 구현
MyFirebaseMessagingService.kt:
package com.example.diskmonitor
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Build
import androidx.core.app.NotificationCompat
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
class MyFirebaseMessagingService : FirebaseMessagingService() {
companion object {
const val CHANNEL_ID = "disk_alert_channel"
const val CHANNEL_NAME = "디스크 경고 알림"
const val CHANNEL_DESC = "서버 디스크 사용량 경고 알림"
}
/**
* FCM 메시지 수신 처리
*/
override fun onMessageReceived(remoteMessage: RemoteMessage) {
super.onMessageReceived(remoteMessage)
val title = remoteMessage.notification?.title ?: "디스크 경고"
val body = remoteMessage.notification?.body ?: "서버 디스크를 확인하세요."
val data = remoteMessage.data // 서버에서 보낸 data 필드
sendNotification(title, body, data)
}
/**
* 토큰 갱신 시 호출 (서버에 새 토큰 등록 필요 시 여기서 처리)
*/
override fun onNewToken(token: String) {
super.onNewToken(token)
// 필요 시 토큰을 자체 서버에 전송
// sendTokenToServer(token)
}
private fun sendNotification(
title: String,
body: String,
data: Map<String, String>
) {
createNotificationChannel()
val intent = Intent(this, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
// data 필드를 Intent로 전달
data.forEach { (key, value) -> putExtra(key, value) }
}
val pendingIntent = PendingIntent.getActivity(
this, 0, intent,
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE
)
val hostname = data["hostname"] ?: "서버"
val percent = data["percent"] ?: "?"
val notification = NotificationCompat.Builder(this, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_warning) // 경고 아이콘 (res에 추가 필요)
.setContentTitle(title)
.setContentText(body)
.setStyle(NotificationCompat.BigTextStyle().bigText(body))
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setAutoCancel(true)
.setContentIntent(pendingIntent)
.build()
val notificationManager =
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.notify(hostname.hashCode(), notification)
}
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
CHANNEL_ID,
CHANNEL_NAME,
NotificationManager.IMPORTANCE_HIGH
).apply {
description = CHANNEL_DESC
enableVibration(true)
}
val manager = getSystemService(NotificationManager::class.java)
manager.createNotificationChannel(channel)
}
}
}
4-3. Topic 구독 설정
MainActivity.kt에서 앱 실행 시 자동으로 토픽을 구독합니다:
package com.example.diskmonitor
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.google.firebase.messaging.FirebaseMessaging
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
subscribeToTopic()
}
private fun subscribeToTopic() {
FirebaseMessaging.getInstance().subscribeToTopic("disk-alert")
.addOnCompleteListener { task ->
if (task.isSuccessful) {
// 구독 성공
android.util.Log.d("FCM", "disk-alert 토픽 구독 성공")
} else {
android.util.Log.e("FCM", "토픽 구독 실패", task.exception)
}
}
}
}
4-4. AndroidManifest.xml 설정
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 알림 권한 (Android 13 이상 필수) -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.INTERNET"/>
<application ... >
<!-- FCM 서비스 등록 -->
<service
android:name=".MyFirebaseMessagingService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT"/>
</intent-filter>
</service>
<!-- 알림 아이콘 및 색상 기본값 설정 -->
<meta-data
android:name="com.google.firebase.messaging.default_notification_icon"
android:resource="@drawable/ic_warning"/>
<meta-data
android:name="com.google.firebase.messaging.default_notification_color"
android:resource="@color/colorAccent"/>
<meta-data
android:name="com.google.firebase.messaging.default_notification_channel_id"
android:value="disk_alert_channel"/>
</application>
</manifest>
4-5. 알림 권한 요청 (Android 13 이상)
// MainActivity.kt - onCreate에 추가
private fun requestNotificationPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (checkSelfPermission(android.Manifest.permission.POST_NOTIFICATIONS)
!= android.content.pm.PackageManager.PERMISSION_GRANTED
) {
requestPermissions(
arrayOf(android.Manifest.permission.POST_NOTIFICATIONS),
1001
)
}
}
}
5. 다중 클라이언트(5명 이상) 전송 전략
Topic 방식 vs Token 방식 비교
| 항목 | Topic 방식 (권장) | Token 방식 |
|---|---|---|
| 대상 | 토픽 구독자 전체 | 특정 기기 지정 |
| 서버 관리 | 토큰 저장 불필요 | DB에 모든 토큰 저장 필요 |
| 확장성 | 무제한 | 복잡도 증가 |
| 개인화 | 불가 | 가능 |
| 적합한 경우 | 5명 이상, 공통 알림 | 개인별 맞춤 알림 |
이 가이드에서는 Topic 방식을 사용합니다. 사용자 5명이
disk-alert토픽을 구독하면 서버는 토픽 하나에만 메시지를 보내면 됩니다.
특정 사용자에게만 알림 보내기 (고급)
관리자 그룹과 일반 사용자를 나누어 알림을 제어하려면 여러 토픽을 활용하세요:
# 심각 경고(90% 초과)는 관리자 토픽으로 전송
ADMIN_TOPIC = "disk-alert-admin" # 관리자 전용
USER_TOPIC = "disk-alert" # 전체 구독자
if percent > 90:
send_fcm_notification(percent, used, total, topic=ADMIN_TOPIC)
elif percent > 80:
send_fcm_notification(percent, used, total, topic=USER_TOPIC)
// 안드로이드 - 관리자 앱에서 추가 토픽 구독
FirebaseMessaging.getInstance().subscribeToTopic("disk-alert-admin")
6. 테스트 및 검증
6-1. 스크립트 직접 실행 테스트
# 실제 디스크 사용량으로 테스트
python3 /usr/local/bin/disk_monitor.py
# 임계값을 낮춰서 강제 알림 테스트 (스크립트에서 임시로 변경)
DISK_THRESHOLD_PERCENT = 0 # 항상 알림 발송
6-2. Firebase Console에서 테스트 메시지 전송
- Firebase Console → Messaging → 새 캠페인 → Firebase 알림 메시지
- 알림 제목/내용 입력
- 타겟: 토픽 →
disk-alert입력 - 지금 보내기 클릭
6-3. cURL로 FCM API 직접 테스트
# 액세스 토큰 발급 (gcloud CLI 필요)
ACCESS_TOKEN=$(gcloud auth print-access-token)
curl -X POST \
"https://fcm.googleapis.com/v1/projects/YOUR_PROJECT_ID/messages:send" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"message": {
"topic": "disk-alert",
"notification": {
"title": "테스트 알림",
"body": "FCM 연동 테스트입니다."
}
}
}'
6-4. 로그 확인
# 모니터링 스크립트 로그 확인
tail -f /var/log/disk_monitor.log
# cron 실행 로그 확인
grep CRON /var/log/syslog | tail -20
정리
| 단계 | 작업 내용 |
|---|---|
| ① Firebase | 프로젝트 생성 → 서비스 계정 키 발급 |
| ② 서버 | Python 스크립트로 디스크 감시 → FCM HTTP v1 API 호출 |
| ③ cron | 5분마다 스크립트 실행 |
| ④ Android | FCM SDK 추가 → disk-alert 토픽 구독 → 알림 표시 |
Topic 방식을 사용하면 클라이언트가 몇 명이 되어도 서버 코드 변경 없이 모든 구독자에게 알림을 전달할 수 있습니다. 디스크 경고 외에도 CPU, 메모리 등의 지표도 동일한 구조로 확장할 수 있습니다.